From 54aadd73d7ad2f242426d622bc4893ab0396bd3e Mon Sep 17 00:00:00 2001 From: DasProffi <67233923+DasProffi@users.noreply.github.com> Date: Wed, 16 Jul 2025 01:27:43 +0200 Subject: [PATCH 1/5] feat: adopt hightide version 0.1.21 --- .../mutations/tasks/task_mutations.ts | 275 +++++------------- .../tasks/task_template_mutations.ts | 28 +- api-services/mutations/tasks/util.ts | 3 +- .../mutations/tasks/ward_mutations.ts | 1 - api-services/service/tasks/TaskService.ts | 147 ++++++++++ .../service/tasks/TaskSubtaskService.ts | 40 +++ api-services/service/users/PatientService.ts | 6 +- api-services/types/tasks/patient.ts | 2 +- api-services/types/tasks/task.ts | 39 ++- api-services/types/tasks/tasks_templates.ts | 8 +- tasks/components/PillLabel.tsx | 55 ++-- tasks/components/RoomOverview.tsx | 8 +- tasks/components/SubtaskView.tsx | 6 +- tasks/components/cards/BedCard.tsx | 2 +- tasks/components/cards/DragCard.tsx | 2 +- tasks/components/cards/PatientCard.tsx | 53 ++-- tasks/components/cards/TaskCard.tsx | 79 +++-- tasks/components/cards/TaskTemplateCard.tsx | 2 +- tasks/components/cards/WardCard.tsx | 4 +- tasks/components/layout/DashboardDisplay.tsx | 150 +++++++--- tasks/components/layout/PatientList.tsx | 19 +- tasks/components/layout/TwoColumn.tsx | 79 +++-- tasks/components/modals/TaskDetailModal.tsx | 31 +- tasks/components/pill/PillLabel.tsx | 0 tasks/globals.css | 28 +- tasks/pages/ward/[wardId].tsx | 8 +- 26 files changed, 638 insertions(+), 437 deletions(-) create mode 100644 api-services/service/tasks/TaskService.ts create mode 100644 api-services/service/tasks/TaskSubtaskService.ts delete mode 100644 tasks/components/pill/PillLabel.tsx diff --git a/api-services/mutations/tasks/task_mutations.ts b/api-services/mutations/tasks/task_mutations.ts index 088040311..c655c886d 100644 --- a/api-services/mutations/tasks/task_mutations.ts +++ b/api-services/mutations/tasks/task_mutations.ts @@ -1,298 +1,159 @@ +import type { UseMutationOptions } from '@tanstack/react-query' import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query' -import { noop } from '@helpwave/hightide' -import { - AssignTaskRequest, - CreateSubtaskRequest, - CreateTaskRequest, - DeleteSubtaskRequest, DeleteTaskRequest, - GetTaskRequest, - GetTasksByPatientRequest, - UnassignTaskRequest, - UpdateSubtaskRequest, - UpdateTaskRequest -} from '@helpwave/proto-ts/services/tasks_svc/v1/task_svc_pb' -import type { CreateSubTaskDTO, SubTaskDTO, TaskDTO } from '../../types/tasks/task' -import { emptyTask } from '../../types/tasks/task' +import type { SubTaskDTO, TaskDTO } from '../../types/tasks/task' import { QueryKeys } from '../query_keys' -import { APIServices } from '../../services' -import { getAuthenticatedGrpcMetadata } from '../../authentication/grpc_metadata' -import { GRPCConverter } from '../../util/util' import { roomOverviewsQueryKey } from './room_mutations' -import { GRPCMapper } from './util' +import type { TaskAssignmentRequestProps } from '../../service/tasks/TaskService' +import { TaskService } from '../../service/tasks/TaskService' +import { TaskSubtaskService } from '../../service/tasks/TaskSubtaskService' -type TaskAssignmentRequestProps = { - taskId: string, - userId: string, -} - -export const useTaskQuery = (taskId: string | undefined) => { +export const useTaskQuery = (taskId?: string) => { return useQuery({ queryKey: [QueryKeys.tasks, taskId, 'get'], enabled: !!taskId, queryFn: async () => { - if (!taskId) { - return emptyTask - } - - const req = new GetTaskRequest() - req.setId(taskId) - - const res = await APIServices.task.getTask(req, getAuthenticatedGrpcMetadata()) - - if (!res.toObject()) { - console.error('TasksByPatient query failed') - } - - const task: TaskDTO = { - id: res.getId(), - name: res.getName(), - notes: res.getDescription(), - status: GRPCConverter.taskStatusFromGRPC(res.getStatus()), - assignee: res.getAssignedUserId(), - subtasks: res.getSubtasksList().map(GRPCMapper.subtaskFromGRPC), - createdAt: res.getCreatedAt() ? GRPCConverter.timestampToDate(res.getCreatedAt()!) : new Date(), - dueDate: res.getDueAt() ? GRPCConverter.timestampToDate(res.getCreatedAt()!) : new Date(), - isPublicVisible: true, // TODO set when backend provides it - // use res.getCreatedAt() - } - - return task + return await TaskService.get(taskId!) }, }) } -export const useTasksByPatientQuery = (patientId: string | undefined) => { +export const useTasksByPatientQuery = (patientId?: string) => { return useQuery({ queryKey: [QueryKeys.tasks, QueryKeys.patients, patientId], enabled: !!patientId, queryFn: async () => { - if (!patientId) { - return - } - - const req = new GetTasksByPatientRequest() - req.setPatientId(patientId) - - const res = await APIServices.task.getTasksByPatient(req, getAuthenticatedGrpcMetadata()) - - const tasks: TaskDTO[] = res.getTasksList().map(task => { - const dueAt = task.getDueAt() - return { - id: task.getId(), - name: task.getName(), - status: GRPCConverter.taskStatusFromGRPC(task.getStatus()), - notes: task.getDescription(), - isPublicVisible: task.getPublic(), - assignee: task.getAssignedUserId(), - dueDate: dueAt ? GRPCConverter.timestampToDate(dueAt) : undefined, - subtasks: task.getSubtasksList().map(GRPCMapper.subtaskFromGRPC), - creationDate: task.getCreatedAt() ? GRPCConverter.timestampToDate(task.getCreatedAt()!) : undefined, - creatorId: task.getCreatedBy(), - } - }) + return await TaskService.getByPatientId(patientId!) + } + }) +} - return tasks +export const useMyTasksQuery = () => { + return useQuery({ + queryKey: [QueryKeys.tasks, 'my'], + queryFn: async () => { + return await TaskService.getMyTasks() } }) } -// TODO move patientId to task object -export const useTaskCreateMutation = (callback: (task: TaskDTO) => void = noop, patientId: string) => { +export const useTaskCreateMutation = (options?: UseMutationOptions) => { const queryClient = useQueryClient() return useMutation({ mutationFn: async (task: TaskDTO) => { - const req = new CreateTaskRequest() - .setName(task.name) - .setPatientId(patientId) - .setDescription(task.notes) - .setPublic(task.isPublicVisible) - .setInitialStatus(GRPCConverter.taskStatusToGrpc(task.status)) - .setDueAt(task.dueDate ? GRPCConverter.dateToTimestamp(task.dueDate) : undefined) - .setSubtasksList(task.subtasks.map(subtask => (new CreateTaskRequest.SubTask()) - .setName(subtask.name) - .setDone(subtask.isDone))) - - if(task.assignee) { - req.setAssignedUserId(task.assignee) - } - - const res = await APIServices.task.createTask(req, getAuthenticatedGrpcMetadata()) - const newTask: TaskDTO = { - ...task, - id: res.getId() - } - - callback(newTask) - return newTask + return await TaskService.create(task) }, - onSuccess: () => { + onSuccess: (data, variables, context) => { + if (options?.onSuccess) { + options.onSuccess(data, variables, context) + } queryClient.invalidateQueries([QueryKeys.tasks, QueryKeys.patients]).catch(console.error) queryClient.invalidateQueries([QueryKeys.rooms, roomOverviewsQueryKey]).catch(console.error) } }) } -export const useTaskUpdateMutation = (callback: () => void = noop) => { +export const useTaskUpdateMutation = (options?: UseMutationOptions) => { const queryClient = useQueryClient() return useMutation({ mutationFn: async (task: TaskDTO) => { - const updateTask = new UpdateTaskRequest() - - updateTask.setId(task.id) - updateTask.setDescription(task.notes) - updateTask.setName(task.name) - updateTask.setDueAt(task.dueDate ? GRPCConverter.dateToTimestamp(task.dueDate) : undefined) - updateTask.setStatus(GRPCConverter.taskStatusToGrpc(task.status)) - - await APIServices.task.updateTask(updateTask, getAuthenticatedGrpcMetadata()) - - callback() - return !!updateTask.toObject() + return await TaskService.update(task) }, - onSuccess: () => { + onSuccess: (data, variables, context) => { + if (options?.onSuccess) { + options.onSuccess(data, variables, context) + } queryClient.invalidateQueries([QueryKeys.tasks]).catch(console.error) queryClient.invalidateQueries([QueryKeys.rooms, roomOverviewsQueryKey]).catch(console.error) } }) } -export const useTaskDeleteMutation = (callback: () => void = noop) => { +export const useTaskDeleteMutation = (options?: UseMutationOptions) => { const queryClient = useQueryClient() return useMutation({ mutationFn: async (taskId: string) => { - const req = new DeleteTaskRequest() - req.setId(taskId) - await APIServices.task.deleteTask(req, getAuthenticatedGrpcMetadata()) - - callback() - return req.toObject() + return await TaskService.delete(taskId) }, - onSuccess: () => { + onSuccess: (data, variables, context) => { + if (options?.onSuccess) { + options.onSuccess(data, variables, context) + } queryClient.invalidateQueries([QueryKeys.tasks]).catch(console.error) queryClient.invalidateQueries([QueryKeys.rooms, roomOverviewsQueryKey]).catch(console.error) } }) } -// TODO: taskId: string | undefined => taskId: string -> A taskId is always required to create a SubTask -export const useSubTaskAddMutation = (taskId: string | undefined, callback: (subtask: SubTaskDTO) => void = noop) => { +export const useSubTaskAddMutation = (options?: UseMutationOptions) => { const queryClient = useQueryClient() return useMutation({ - mutationFn: async (subtask: CreateSubTaskDTO) => { - const usedTaskId = subtask.taskId ?? taskId ?? '' - if (!usedTaskId) { - return - } - const req = new CreateSubtaskRequest() - req.setSubtask(new CreateSubtaskRequest.Subtask().setName(subtask.name)) - req.setTaskId(usedTaskId) - const res = await APIServices.task.createSubtask(req, getAuthenticatedGrpcMetadata()) - - const newSubtask: SubTaskDTO = { - id: res.getSubtaskId(), - name: subtask.name, - isDone: false, - } - - callback(newSubtask) - return req.toObject() + mutationFn: async (subtask: SubTaskDTO) => { + return await TaskSubtaskService.create(subtask) }, - onSuccess: () => { + onSuccess: (data, variables, context) => { + if (options?.onSuccess) { + options.onSuccess(data, variables, context) + } queryClient.invalidateQueries([QueryKeys.tasks]).catch(console.error) } }) } -// TODO move taskId parameter to subtask object -export const useSubTaskUpdateMutation = (taskId?: string, callback: (subtask: SubTaskDTO) => void = noop) => { +export const useSubTaskUpdateMutation = (options?: UseMutationOptions) => { const queryClient = useQueryClient() return useMutation({ mutationFn: async (subtask: SubTaskDTO) => { - if (!taskId) { - throw Error('SubTaskUpdateMutation: A taskId must be provided') - } - const req = new UpdateSubtaskRequest() - req.setSubtaskId(subtask.id) - .setTaskId(taskId) - .setSubtask(new UpdateSubtaskRequest.Subtask() - .setName(subtask.name) - .setDone(subtask.isDone)) - await APIServices.task.updateSubtask(req, getAuthenticatedGrpcMetadata()) - - const newSubtask: SubTaskDTO = { ...subtask } - - callback(newSubtask) - return req.toObject() + return await TaskSubtaskService.update(subtask) }, - onSuccess: () => { + onSuccess: (data, variables, context) => { + if (options?.onSuccess) { + options.onSuccess(data, variables, context) + } queryClient.invalidateQueries([QueryKeys.tasks]).catch(console.error) } }) } -export const useSubTaskDeleteMutation = (callback: () => void = noop) => { +export const useSubTaskDeleteMutation = (options?: UseMutationOptions) => { const queryClient = useQueryClient() return useMutation({ mutationFn: async (subtaskId: string) => { - const req = new DeleteSubtaskRequest() - // req.setTaskId() - req.setSubtaskId(subtaskId) - await APIServices.task.deleteSubtask(req, getAuthenticatedGrpcMetadata()) - - callback() - return req.toObject() + return await TaskSubtaskService.delete(subtaskId) }, - onSuccess: () => { + onSuccess: (data, variables, context) => { + if (options?.onSuccess) { + options.onSuccess(data, variables, context) + } queryClient.invalidateQueries([QueryKeys.tasks]).catch(console.error) } }) } -export const useAssignTaskMutation = (callback: () => void = noop) => { +export const useAssignTaskMutation = (options?: UseMutationOptions) => { const queryClient = useQueryClient() return useMutation({ - mutationFn: async ({ - taskId, - userId - }: TaskAssignmentRequestProps) => { - const req = new AssignTaskRequest() - req.setTaskId(taskId) - req.setUserId(userId) - const res = await APIServices.task.assignTask(req, getAuthenticatedGrpcMetadata()) - - if (!res.toObject()) { - console.error('error in AssignTaskToUser') - } - - callback() - return req.toObject() + mutationFn: async (props: TaskAssignmentRequestProps) => { + return await TaskService.assign(props) }, - onSuccess: () => { + onSuccess: (data, variables, context) => { + if (options?.onSuccess) { + options.onSuccess(data, variables, context) + } queryClient.invalidateQueries([QueryKeys.tasks]).catch(console.error) } }) } -export const useUnassignTaskMutation = (callback: () => void = noop) => { +export const useUnassignTaskMutation = (options?: UseMutationOptions) => { const queryClient = useQueryClient() return useMutation({ - mutationFn: async ({ - taskId, - userId - }: TaskAssignmentRequestProps) => { - const req = new UnassignTaskRequest() - req.setTaskId(taskId) - req.setUserId(userId) - const res = await APIServices.task.unassignTask(req, getAuthenticatedGrpcMetadata()) - - if (!res.toObject()) { - console.error('error in UnAssignTaskToUser') - } - - callback() - return req.toObject() + mutationFn: async (props: TaskAssignmentRequestProps) => { + return await TaskService.unassign(props) }, - onSuccess: () => { + onSuccess: (data, variables, context) => { + if (options?.onSuccess) { + options.onSuccess(data, variables, context) + } queryClient.invalidateQueries([QueryKeys.tasks]).catch(console.error) } }) diff --git a/api-services/mutations/tasks/task_template_mutations.ts b/api-services/mutations/tasks/task_template_mutations.ts index abfa5f2c3..1ba23b1b8 100644 --- a/api-services/mutations/tasks/task_template_mutations.ts +++ b/api-services/mutations/tasks/task_template_mutations.ts @@ -5,7 +5,8 @@ import { CreateTaskTemplateSubTaskRequest, DeleteTaskTemplateRequest, DeleteTaskTemplateSubTaskRequest, - GetAllTaskTemplatesRequest, UpdateTaskTemplateRequest, + GetAllTaskTemplatesRequest, + UpdateTaskTemplateRequest, UpdateTaskTemplateSubTaskRequest } from '@helpwave/proto-ts/services/tasks_svc/v1/task_template_svc_pb' import type { TaskTemplateDTO, TaskTemplateFormType } from '../../types/tasks/tasks_templates' @@ -22,7 +23,7 @@ export const useWardTaskTemplateQuery = (wardId?: string) => { let wardTaskTemplates: TaskTemplateDTO[] = [] if (wardId !== undefined) { const req = new GetAllTaskTemplatesRequest() - req.setWardId(wardId) + .setWardId(wardId) const res = await APIServices.taskTemplates.getAllTaskTemplates(req, getAuthenticatedGrpcMetadata()) wardTaskTemplates = res.getTemplatesList().map((template) => ({ id: template.getId(), @@ -32,7 +33,8 @@ export const useWardTaskTemplateQuery = (wardId?: string) => { subtasks: template.getSubtasksList().map((subtask) => ({ id: subtask.getId(), name: subtask.getName(), - isDone: false + isDone: false, + taskId: template.getId(), })), isPublicVisible: template.getIsPublic() })) @@ -46,14 +48,12 @@ export const useWardTaskTemplateQuery = (wardId?: string) => { type UseAllTaskTemplatesByCreatorProps = { createdBy?: string, - onSuccess: (data: TaskTemplateDTO[]) => void, type: QueryKey, } export const useAllTaskTemplatesByCreator = ({ - createdBy, - onSuccess = noop, - type = 'wardTaskTemplates' -}: UseAllTaskTemplatesByCreatorProps) => { + createdBy, + type = 'wardTaskTemplates' + }: UseAllTaskTemplatesByCreatorProps) => { const queryKey = type const onlyPrivate = type === 'personalTaskTemplates' return useQuery({ @@ -73,7 +73,8 @@ export const useAllTaskTemplatesByCreator = ({ subtasks: template.getSubtasksList().map((subtask) => ({ id: subtask.getId(), name: subtask.getName(), - isDone: false + isDone: false, + taskId: template.getId(), })), isPublicVisible: template.getIsPublic() })) @@ -81,12 +82,11 @@ export const useAllTaskTemplatesByCreator = ({ } return personalTaskTemplates }, - onSuccess }) } -export const usePersonalTaskTemplateQuery = (createdBy?: string, onSuccess: (data: TaskTemplateDTO[]) => void = noop) => { - return useAllTaskTemplatesByCreator({ createdBy, onSuccess, type: 'personalTaskTemplates' }) +export const usePersonalTaskTemplateQuery = (createdBy?: string) => { + return useAllTaskTemplatesByCreator({ createdBy, type: 'personalTaskTemplates' }) } export const useUpdateMutation = (queryKey: QueryKey, setTemplate: (taskTemplate?: TaskTemplateDTO) => void) => { @@ -147,7 +147,7 @@ export const useUpdateMutation = (queryKey: QueryKey, setTemplate: (taskTemplate queryClient.setQueryData( [queryKey], (old) => old -) + ) return { previousTaskTemplates } }, onError: (_, newTodo, context) => { @@ -214,7 +214,7 @@ export const useDeleteMutation = (queryKey: QueryKey, setTemplate: (task?: TaskT queryClient.setQueryData( [queryKey], (old) => old -) + ) return { previousTaskTemplate } }, onError: (_, newTodo, context) => { diff --git a/api-services/mutations/tasks/util.ts b/api-services/mutations/tasks/util.ts index 4b2eded2b..ddac460c6 100644 --- a/api-services/mutations/tasks/util.ts +++ b/api-services/mutations/tasks/util.ts @@ -7,9 +7,10 @@ interface GRPCSubTask { } export const GRPCMapper = { - subtaskFromGRPC: (subTask: GRPCSubTask): SubTaskDTO => ({ + subtaskFromGRPC: (subTask: GRPCSubTask, taskId: string): SubTaskDTO => ({ id: subTask.getId(), name: subTask.getName(), isDone: subTask.getDone(), + taskId }) } diff --git a/api-services/mutations/tasks/ward_mutations.ts b/api-services/mutations/tasks/ward_mutations.ts index e537122c5..ae38f64c3 100644 --- a/api-services/mutations/tasks/ward_mutations.ts +++ b/api-services/mutations/tasks/ward_mutations.ts @@ -81,7 +81,6 @@ export const useWardQuery = (id: string) => useQuery({ if (!res.toObject()) { console.error('error in Ward query') } - return { id: res.getId(), name: res.getName(), diff --git a/api-services/service/tasks/TaskService.ts b/api-services/service/tasks/TaskService.ts new file mode 100644 index 000000000..1c1e9ba72 --- /dev/null +++ b/api-services/service/tasks/TaskService.ts @@ -0,0 +1,147 @@ +import type { TaskDTO } from '../../types/tasks/task' +import { APIServices } from '../../services' +import { + AssignTaskRequest, + CreateTaskRequest, + DeleteTaskRequest, + GetAssignedTasksRequest, + GetTaskRequest, + GetTasksByPatientRequest, + UnassignTaskRequest, + UpdateTaskRequest +} from '@helpwave/proto-ts/services/tasks_svc/v1/task_svc_pb' +import { GRPCConverter } from '../../util/util' +import { getAuthenticatedGrpcMetadata } from '../../authentication/grpc_metadata' +import { GRPCMapper } from '../../mutations/tasks/util' + +export type TaskAssignmentRequestProps = { + taskId: string, + userId: string, +} + +export const TaskService = { + get: async function (taskId: string): Promise { + const req = new GetTaskRequest() + req.setId(taskId) + + const res = await APIServices.task.getTask(req, getAuthenticatedGrpcMetadata()) + + if (!res.toObject()) { + console.error('TasksByPatient query failed') + } + + return { + id: res.getId(), + name: res.getName(), + notes: res.getDescription(), + status: GRPCConverter.taskStatusFromGRPC(res.getStatus()), + assignee: res.getAssignedUserId(), + subtasks: res.getSubtasksList().map(value => GRPCMapper.subtaskFromGRPC(value, res.getId())), + creatorId: res.getCreatedBy(), + createdAt: res.getCreatedAt() ? GRPCConverter.timestampToDate(res.getCreatedAt()!) : new Date(), + dueDate: res.getDueAt() ? GRPCConverter.timestampToDate(res.getCreatedAt()!) : new Date(), + isPublicVisible: res.getPublic(), + patientId: res.getPatient()!.getId(), + // use res.getCreatedAt() + } + }, + getByPatientId: async function (patientId: string): Promise { + const req = new GetTasksByPatientRequest() + req.setPatientId(patientId) + + const res = await APIServices.task.getTasksByPatient(req, getAuthenticatedGrpcMetadata()) + + return res.getTasksList().map(task => { + const dueAt = task.getDueAt() + return { + id: task.getId(), + name: task.getName(), + status: GRPCConverter.taskStatusFromGRPC(task.getStatus()), + notes: task.getDescription(), + isPublicVisible: task.getPublic(), + assignee: task.getAssignedUserId(), + dueDate: dueAt ? GRPCConverter.timestampToDate(dueAt) : undefined, + subtasks: task.getSubtasksList().map(value => GRPCMapper.subtaskFromGRPC(value, task.getId())), + creationDate: task.getCreatedAt() ? GRPCConverter.timestampToDate(task.getCreatedAt()!) : undefined, + creatorId: task.getCreatedBy(), + patientId: task.getPatientId(), + } + }) + }, + getMyTasks: async function (): Promise { + const req = new GetAssignedTasksRequest() + + const res = await APIServices.task.getAssignedTasks(req, getAuthenticatedGrpcMetadata()) + + return res.getTasksList().map(task => { + const dueAt = task.getDueAt() + return { + id: task.getId(), + name: task.getName(), + status: GRPCConverter.taskStatusFromGRPC(task.getStatus()), + notes: task.getDescription(), + isPublicVisible: task.getPublic(), + assignee: task.getAssignedUserId(), + dueDate: dueAt ? GRPCConverter.timestampToDate(dueAt) : undefined, + subtasks: task.getSubtasksList().map(value => GRPCMapper.subtaskFromGRPC(value, task.getId())), + creationDate: task.getCreatedAt() ? GRPCConverter.timestampToDate(task.getCreatedAt()!) : undefined, + creatorId: task.getCreatedBy(), + patientId: task.getPatient()!.getId(), + } + }) + }, + create: async function (task: TaskDTO): Promise { + const req = new CreateTaskRequest() + .setName(task.name) + .setPatientId(task.patientId) + .setDescription(task.notes) + .setPublic(task.isPublicVisible) + .setInitialStatus(GRPCConverter.taskStatusToGrpc(task.status)) + .setDueAt(task.dueDate ? GRPCConverter.dateToTimestamp(task.dueDate) : undefined) + .setSubtasksList(task.subtasks.map(subtask => (new CreateTaskRequest.SubTask()) + .setName(subtask.name) + .setDone(subtask.isDone))) + + if (task.assignee) { + req.setAssignedUserId(task.assignee) + } + + const res = await APIServices.task.createTask(req, getAuthenticatedGrpcMetadata()) + return { + ...task, + id: res.getId() + } + }, + update: async function (task: TaskDTO): Promise { + const req = new UpdateTaskRequest() + .setId(task.id) + .setDescription(task.notes) + .setName(task.name) + .setDueAt(task.dueDate ? GRPCConverter.dateToTimestamp(task.dueDate) : undefined) + .setStatus(GRPCConverter.taskStatusToGrpc(task.status)) + + const res = await APIServices.task.updateTask(req, getAuthenticatedGrpcMetadata()) + + return !!res.toObject() + }, + delete: async function (taskId: string): Promise { + const req = new DeleteTaskRequest() + .setId(taskId) + await APIServices.task.deleteTask(req, getAuthenticatedGrpcMetadata()) + return !!req.toObject() + }, + assign: async function ({ taskId, userId }: TaskAssignmentRequestProps): Promise { + const req = new AssignTaskRequest() + .setTaskId(taskId) + .setUserId(userId) + const res = await APIServices.task.assignTask(req, getAuthenticatedGrpcMetadata()) + return !!res.toObject() + }, + unassign: async function ({ taskId, userId }: TaskAssignmentRequestProps): Promise { + const req = new UnassignTaskRequest() + .setTaskId(taskId) + .setUserId(userId) + const res = await APIServices.task.unassignTask(req, getAuthenticatedGrpcMetadata()) + return !!res.toObject() + } +} diff --git a/api-services/service/tasks/TaskSubtaskService.ts b/api-services/service/tasks/TaskSubtaskService.ts new file mode 100644 index 000000000..8bfb1f796 --- /dev/null +++ b/api-services/service/tasks/TaskSubtaskService.ts @@ -0,0 +1,40 @@ +import type { SubTaskDTO } from '../../types/tasks/task' +import { APIServices } from '../../services' +import { + CreateSubtaskRequest, + DeleteSubtaskRequest, + UpdateSubtaskRequest +} from '@helpwave/proto-ts/services/tasks_svc/v1/task_svc_pb' +import { getAuthenticatedGrpcMetadata } from '../../authentication/grpc_metadata' + +export const TaskSubtaskService = { + create: async function (subtask: SubTaskDTO): Promise { + const req = new CreateSubtaskRequest() + .setSubtask(new CreateSubtaskRequest.Subtask().setName(subtask.name)) + .setTaskId(subtask.taskId) + const res = await APIServices.task.createSubtask(req, getAuthenticatedGrpcMetadata()) + + return { + ...subtask, + id: res.getSubtaskId(), + isDone: false, + } + }, + update: async function (subtask: SubTaskDTO): Promise { + const req = new UpdateSubtaskRequest() + req.setSubtaskId(subtask.id) + .setTaskId(subtask.taskId) + .setSubtask(new UpdateSubtaskRequest.Subtask() + .setName(subtask.name) + .setDone(subtask.isDone)) + const res = await APIServices.task.updateSubtask(req, getAuthenticatedGrpcMetadata()) + + return !!res.toObject() + }, + delete: async function (subtaskId: string): Promise { + const req = new DeleteSubtaskRequest() + .setSubtaskId(subtaskId) + const res = await APIServices.task.deleteSubtask(req, getAuthenticatedGrpcMetadata()) + return !!res.toObject() + }, +} diff --git a/api-services/service/users/PatientService.ts b/api-services/service/users/PatientService.ts index 4c16413f4..62baafba7 100644 --- a/api-services/service/users/PatientService.ts +++ b/api-services/service/users/PatientService.ts @@ -41,8 +41,10 @@ export const PatientService = { subtasks: task.getSubtasksList().map(subtask => ({ id: subtask.getId(), name: subtask.getName(), - isDone: subtask.getDone() - })) + isDone: subtask.getDone(), + taskId: task.getId(), + })), + patientId: res.getId(), })) } }, diff --git a/api-services/types/tasks/patient.ts b/api-services/types/tasks/patient.ts index 83d81d158..f9309cbef 100644 --- a/api-services/types/tasks/patient.ts +++ b/api-services/types/tasks/patient.ts @@ -18,7 +18,7 @@ export const emptyPatient: PatientDTO = { } export type PatientWithTasksNumberDTO = PatientMinimalDTO & { - tasksUnscheduled: number, + tasksTodo: number, tasksInProgress: number, tasksDone: number, } diff --git a/api-services/types/tasks/task.ts b/api-services/types/tasks/task.ts index 8bffa8af5..93265d98f 100644 --- a/api-services/types/tasks/task.ts +++ b/api-services/types/tasks/task.ts @@ -4,12 +4,10 @@ export type SubTaskDTO = { id: string, name: string, isDone: boolean, + taskId: string, } -export type CreateSubTaskDTO = SubTaskDTO & { - taskId?: string, -} - +// The order in the array defines the sorting order const taskStatus = ['done', 'inProgress', 'todo'] as const export type TaskStatus = typeof taskStatus[number] @@ -28,9 +26,38 @@ const taskStatusTranslation: Translation = { done: 'Fertig' } } + +type TaskStatusColor = { + background: string, + icon: string, + text: string, +} + +const taskColorMapping: Record = { + todo: { + background: 'bg-todo-background', + text: 'text-todo-text', + icon: 'text-todo-icon', + }, + inProgress: { + background: 'bg-inprogress-background', + text: 'text-inprogress-text', + icon: 'text-inprogress-icon', + }, + done: { + background: 'bg-done-background', + text: 'text-done-text', + icon: 'text-done-icon', + }, +} + export const TaskStatusUtil = { taskStatus, - translation: taskStatusTranslation + translation: taskStatusTranslation, + compare: (a: TaskStatus, b: TaskStatus): number => { + return taskStatus.indexOf(a) - taskStatus.indexOf(b) + }, + colors: taskColorMapping } export type TaskDTO = { @@ -43,6 +70,7 @@ export type TaskDTO = { dueDate?: Date, createdAt?: Date, creatorId?: string, + patientId: string, isPublicVisible: boolean, } @@ -52,6 +80,7 @@ export const emptyTask: TaskDTO = { notes: '', status: 'todo', subtasks: [], + patientId: '', isPublicVisible: false } diff --git a/api-services/types/tasks/tasks_templates.ts b/api-services/types/tasks/tasks_templates.ts index 2eb10de51..07c9e886d 100644 --- a/api-services/types/tasks/tasks_templates.ts +++ b/api-services/types/tasks/tasks_templates.ts @@ -1,13 +1,11 @@ +import type { SubTaskDTO } from './task' + export type TaskTemplateDTO = { wardId?: string, id: string, name: string, notes: string, - subtasks: { - isDone: boolean, - id: string, - name: string, - }[], + subtasks: SubTaskDTO[], isPublicVisible: boolean, } diff --git a/tasks/components/PillLabel.tsx b/tasks/components/PillLabel.tsx index ad6777a65..22b4ec313 100644 --- a/tasks/components/PillLabel.tsx +++ b/tasks/components/PillLabel.tsx @@ -12,21 +12,6 @@ type PillLabelTranslation = TaskStatusTranslationType const defaultPillLabelTranslation: Translation = TaskStatusUtil.translation -const mapping = { - todo: { - mainClassName: 'bg-tag-red-background text-tag-red-text', - iconClassName: 'bg-tag-red-icon', - }, - inProgress: { - mainClassName: 'bg-tag-yellow-background text-tag-yellow-text', - iconClassName: 'bg-tag-yellow-icon', - }, - done: { - mainClassName: 'bg-tag-green-background text-tag-green-text', - iconClassName: 'bg-tag-green-icon', - }, -} as const - export type PillLabelProps = { count?: number, taskStatus?: TaskStatus, @@ -40,13 +25,18 @@ export const PillLabel = ({ count, taskStatus = 'todo' }: PropsForTranslation) => { - const state = mapping[taskStatus] + const taskStatusColor = TaskStatusUtil.colors[taskStatus] + const iconColor: Record = { + done: 'bg-done-icon', + inProgress: 'bg-inprogress-icon', + todo: 'bg-todo-icon', + } const translation = useTranslation([defaultPillLabelTranslation], overwriteTranslation) return ( -
+
-
+
{translation(taskStatus)}
{count ?? '-'} @@ -59,7 +49,7 @@ export const PillLabel = ({ // export type PillLabelsColumnProps = { - unscheduledCount?: number, + todoCount?: number, inProgressCount?: number, doneCount?: number, } @@ -67,10 +57,10 @@ export type PillLabelsColumnProps = { /** * A column showing the all TaskStates with a PillLabel for each */ -export const PillLabelsColumn = ({ unscheduledCount, inProgressCount, doneCount }: PillLabelsColumnProps) => { +export const PillLabelsColumn = ({ todoCount, inProgressCount, doneCount }: PillLabelsColumnProps) => { return (
- +
@@ -101,37 +91,38 @@ export const PillLabelBox = ({ unscheduled, inProgress, done }: PillLabelBoxProp borderLeftWidth: between, borderRightWidth: between, } + return ( -
+
-
+
{unscheduled}
-
-
+
+
{inProgress}
-
+ className="row bg-done-background rounded-r-md pl-1 pr-2 items-center text-done-text"> +
{done}
diff --git a/tasks/components/RoomOverview.tsx b/tasks/components/RoomOverview.tsx index e8ac5a4fe..6a39bb052 100644 --- a/tasks/components/RoomOverview.tsx +++ b/tasks/components/RoomOverview.tsx @@ -48,9 +48,11 @@ export const RoomOverview = ({ room }: RoomOverviewProps) => { { event.stopPropagation() if (bed.patient) { diff --git a/tasks/components/SubtaskView.tsx b/tasks/components/SubtaskView.tsx index 3e46d5a71..7a831bfaf 100644 --- a/tasks/components/SubtaskView.tsx +++ b/tasks/components/SubtaskView.tsx @@ -57,9 +57,9 @@ export const SubtaskView = ({ const translation = useTranslation([defaultSubtaskViewTranslation], overwriteTranslation) const isCreatingTask = taskId === '' - const addSubtaskMutation = useSubTaskAddMutation(taskId) + const addSubtaskMutation = useSubTaskAddMutation() const deleteSubtaskMutation = useSubTaskDeleteMutation() - const updateSubtaskMutation = useSubTaskUpdateMutation(taskId) + const updateSubtaskMutation = useSubTaskUpdateMutation() const scrollableRef = useRef(null) const [scrollToBottomFlag, setScrollToBottom] = useState(false) @@ -94,7 +94,7 @@ export const SubtaskView = ({ {translation('subtasks')} { - const newSubtask = { id: '', name: `${translation('newSubtask')} ${subtasks.length + 1}`, isDone: false } + const newSubtask: SubTaskDTO = { id: '', name: `${translation('newSubtask')} ${subtasks.length + 1}`, isDone: false, taskId: taskId ?? '' } onChange([...subtasks, newSubtask]) if (!isCreatingTask) { addSubtaskMutation.mutate(newSubtask) diff --git a/tasks/components/cards/BedCard.tsx b/tasks/components/cards/BedCard.tsx index a1544bcf4..ccc667aee 100644 --- a/tasks/components/cards/BedCard.tsx +++ b/tasks/components/cards/BedCard.tsx @@ -47,7 +47,7 @@ export const BedCard = ({ {...restCardProps} >
- {bedName} + {bedName} {translation('nobody')}
diff --git a/tasks/components/cards/DragCard.tsx b/tasks/components/cards/DragCard.tsx index 586927217..c68687392 100644 --- a/tasks/components/cards/DragCard.tsx +++ b/tasks/components/cards/DragCard.tsx @@ -27,7 +27,7 @@ export const DragCard = ({ }: DragCardProps) => { return (
= { } } +type TaskCounts = { + todo: number, + inProgress: number, + done: number, +} + export type PatientCardProps = DragCardProps & { bedName?: string, patientName: string, - unscheduledTasks?: number, - inProgressTasks?: number, - doneTasks?: number, + taskCounts?: TaskCounts, } /** * A Card for displaying a Patient and the tasks */ export const PatientCard = ({ - overwriteTranslation, - bedName, - patientName, - unscheduledTasks, - inProgressTasks, - doneTasks, - isSelected, - onClick, - className, - ...restCardProps -}: PropsForTranslation) => { + overwriteTranslation, + bedName, + patientName, + taskCounts, + isSelected, + onClick, + className, + ...restCardProps + }: PropsForTranslation) => { const translation = useTranslation([defaultPatientCardTranslations], overwriteTranslation) return ( - +
- {bedName ?? translation('bedNotAssigned')} + {bedName ?? translation('bedNotAssigned')} {patientName}
-
- -
+ {taskCounts && ( +
+ +
+ )}
) } diff --git a/tasks/components/cards/TaskCard.tsx b/tasks/components/cards/TaskCard.tsx index 537c3b353..ad5c641e6 100644 --- a/tasks/components/cards/TaskCard.tsx +++ b/tasks/components/cards/TaskCard.tsx @@ -1,29 +1,35 @@ import clsx from 'clsx' -import { ProgressIndicator } from '@helpwave/hightide' -import { LockIcon } from 'lucide-react' import type { Translation } from '@helpwave/hightide' -import { useTranslation, type PropsForTranslation } from '@helpwave/hightide' -import { Avatar } from '@helpwave/hightide' -import type { TaskDTO } from '@helpwave/api-services/types/tasks/task' +import { Avatar, noop, ProgressIndicator, type PropsForTranslation, useTranslation } from '@helpwave/hightide' +import { LockIcon, UserIcon } from 'lucide-react' +import type { TaskDTO, TaskStatusTranslationType } from '@helpwave/api-services/types/tasks/task' +import { TaskStatusUtil } from '@helpwave/api-services/types/tasks/task' import { useUserQuery } from '@helpwave/api-services/mutations/users/user_mutations' type TaskCardTranslation = { assigned: string, + noDescription: string, } const defaultTaskCardTranslations: Translation = { en: { - assigned: 'assigned' + assigned: 'assigned', + noDescription: 'No description', }, de: { - assigned: 'zugewiesen' + assigned: 'zugewiesen', + noDescription: 'Keine Beschreibung', } } +type TranslationType = TaskCardTranslation & TaskStatusTranslationType + export type TaskCardProps = { task: TaskDTO, onClick: () => void, - isSelected: boolean, + isSelected?: boolean, + showStatus?: boolean, + className?: string, } /** @@ -33,9 +39,11 @@ export const TaskCard = ({ overwriteTranslation, task, isSelected = false, - onClick = () => undefined - }: PropsForTranslation) => { - const translation = useTranslation([defaultTaskCardTranslations], overwriteTranslation) + showStatus = false, + onClick = noop, + className, + }: PropsForTranslation) => { + const translation = useTranslation([TaskStatusUtil.translation, defaultTaskCardTranslations], overwriteTranslation) const progress = task.subtasks.length === 0 ? 1 : task.subtasks.filter(value => value.isDone).length / task.subtasks.length const isOverDue = task.dueDate && task.dueDate < new Date() && task.status !== 'done' @@ -44,27 +52,46 @@ export const TaskCard = ({ return (
-
-
- {!task.isPublicVisible &&
} - {task.name} +
+
+
+ {!task.isPublicVisible &&
} + {task.name} +
+ + {task.notes ? task.notes : translation('noDescription')} +
- - {task.notes} - -
-
- {assignee && assignee.avatarUrl && - } - {task.subtasks.length > 0 && ( - + {showStatus && ( +
+ {translation(task.status)} +
)}
+
+ {(assignee && assignee.avatarUrl) ? + (): + () + } + 0 ? progress : 1}/> +
) } diff --git a/tasks/components/cards/TaskTemplateCard.tsx b/tasks/components/cards/TaskTemplateCard.tsx index 5562554a5..72dba55f7 100644 --- a/tasks/components/cards/TaskTemplateCard.tsx +++ b/tasks/components/cards/TaskTemplateCard.tsx @@ -50,7 +50,7 @@ export const TaskTemplateCard = ({ >
-

{name}

+

{name}

{typeForLabel && ( { return ( -
+
- {ward.name} + {ward.name}
diff --git a/tasks/components/layout/DashboardDisplay.tsx b/tasks/components/layout/DashboardDisplay.tsx index 8da9a2718..295752ef9 100644 --- a/tasks/components/layout/DashboardDisplay.tsx +++ b/tasks/components/layout/DashboardDisplay.tsx @@ -10,6 +10,13 @@ import { PatientCard } from '../cards/PatientCard' import { AddCard } from '@/components/cards/AddCard' import { useAuth } from '@helpwave/api-services/authentication/useAuth' import { ColumnTitle } from '@/components/ColumnTitle' +import { useMyTasksQuery } from '@helpwave/api-services/mutations/tasks/task_mutations' +import { Scrollbars } from 'react-custom-scrollbars-2' +import { TaskCard } from '@/components/cards/TaskCard' +import { useMemo, useState } from 'react' +import type { TaskDTO } from '@helpwave/api-services/types/tasks/task' +import { TaskStatusUtil } from '@helpwave/api-services/types/tasks/task' +import { TaskDetailModal } from '@/components/modals/TaskDetailModal' type DashboardDisplayTranslation = { patients: string, @@ -18,6 +25,8 @@ type DashboardDisplayTranslation = { recent: string, showAllOrganizations: string, addWard: string, + myTasks: string, + noTasks: string, } const defaultDashboardDisplayTranslations: Translation = { @@ -28,6 +37,8 @@ const defaultDashboardDisplayTranslations: Translation() // TODO replace with recent wards later const { @@ -60,49 +74,111 @@ export const DashboardDisplay = ({ data: patients, isLoading: isLoadingPatients } = useRecentPatientsQuery() + const { + data: tasks, + isLoading: isTasksLoading + } = useMyTasksQuery() + + const sortedTasks = useMemo(() => { + return [...(tasks ?? [])].sort((a, b) => TaskStatusUtil.compare(a.status, b.status) * -1) + }, [tasks]) + + const cardWidth = 'min-w-64 max-w-64' return ( -
+
+ setTaskDetailModalId(undefined)} + /> - - {patients && patients.length > 0 && ( - <> - -
- {patients?.map(patient => ( - patient.wardId ? router.push(`/ward/${patient.wardId}`) : undefined} +
+ +
+ + +
+ {sortedTasks.length > 0 && sortedTasks.map(task => ( + { + setTaskDetailModalId(task.id) + }} + isSelected={false} + showStatus={true} + className={cardWidth} + /> + ))} + {sortedTasks.length === 0 && ( +
+ {translation('noTasks')} +
+ )} +
+
+
+
+ +
+ + +
+ {wards && wards.length > 0 && wards?.map(ward => ( + router.push(`/ward/${ward.id}`)} + className={cardWidth} + /> + ))} + router.push(`/organizations/${organization?.id}`)} + className={cardWidth} /> - ))} -
- - )} - - -
- -
- {wards && wards.length > 0 && wards?.map(ward => ( - router.push(`/ward/${ward.id}`)} - /> - ))} - router.push(`/organizations/${organization?.id}`)} - /> +
+
-
-
+ + + {patients && patients.length > 0 && ( +
+ + +
+
+ {patients?.filter((_, index) => index % 2 == 0).map(patient => ( + patient.wardId ? router.push(`/ward/${patient.wardId}`) : undefined} + /> + ))} +
+
+ {patients?.filter((_, index) => index % 2 == 1).map(patient => ( + patient.wardId ? router.push(`/ward/${patient.wardId}`) : undefined} + /> + ))} +
+
+
+
+ )} +
+
) } diff --git a/tasks/components/layout/PatientList.tsx b/tasks/components/layout/PatientList.tsx index 1aeb259a0..c4bd7f706 100644 --- a/tasks/components/layout/PatientList.tsx +++ b/tasks/components/layout/PatientList.tsx @@ -204,6 +204,7 @@ export const PatientList = ({ label={{`${translation('active')} (${filteredActive.length})`}} className={clsx('border-2 border-transparent bg-transparent !shadow-none')} headerClassName="bg-transparent" + contentClassName="!px-0" > {filteredActive.map(patient => ( {() => (
updateContext({ ...context, patientId: patient.id, @@ -228,7 +229,7 @@ export const PatientList = ({ bedId: patient.bed.id })} > - {patient.name} + {patient.name}
{activeLabelText(patient)} @@ -269,18 +270,18 @@ export const PatientList = ({ patient, discharged: false }} - className="not-last:border-b-2 not-last:pb-2 border-b-gray-300" + className="not-last:border-b-2 not-last:pb-2 border-b-divider" > {() => (
updateContext({ wardId: context.wardId, patientId: patient.id })} > - {patient.name} + {patient.name}
{`${translation('unassigned')}`} @@ -323,18 +324,18 @@ export const PatientList = ({ patient, discharged: true }} - className="not-last:border-b-2 not-last:pb-2 border-b-gray-300" + className="not-last:border-b-2 not-last:pb-2 border-b-divider" > {() => (
updateContext({ wardId: context.wardId, patientId: patient.id })} > - {patient.name} + {patient.name}
{ event.stopPropagation() diff --git a/tasks/components/layout/TwoColumn.tsx b/tasks/components/layout/TwoColumn.tsx index 4ba6a735c..51ec82c40 100644 --- a/tasks/components/layout/TwoColumn.tsx +++ b/tasks/components/layout/TwoColumn.tsx @@ -1,7 +1,9 @@ -import { createRef, useEffect, useState, type ReactNode } from 'react' +import { type ReactNode, useCallback, useEffect, useRef, useState } from 'react' import { Scrollbars } from 'react-custom-scrollbars-2' import { ChevronLeft, ChevronRight, GripVertical } from 'lucide-react' import clsx from 'clsx' +import { useResizeCallbackWrapper } from '@helpwave/hightide' + /** * Only px and % * e.g. 250px or 10% @@ -12,7 +14,7 @@ type Constraint = { } type ColumnConstraints = { - left? : Constraint, + left?: Constraint, right?: Constraint, } @@ -39,18 +41,16 @@ type TwoColumnProps = { * of the columns can be changed via the initialLayoutState */ export const TwoColumn = ({ - right, - left, - baseLayoutValue = '50%', - disableResize = true, - constraints = defaultConstraint -}: TwoColumnProps) => { - const ref = createRef() + right, + left, + baseLayoutValue = '50%', + disableResize = true, + constraints = defaultConstraint + }: TwoColumnProps) => { + const ref = useRef(null) const [fullWidth, setFullWidth] = useState(0) const [leftWidth, setLeftWidth] = useState(0) const [isDragging, setIsDragging] = useState(false) - const headerHeight = 64 - const dividerHitBoxWidth = 24 constraints = { ...defaultConstraint, ...constraints } const convertToLeftWidth = (constraint: string, usedFullWidth: number) => { @@ -81,7 +81,7 @@ export const TwoColumn = ({ let left = dragPosition if (dragPosition < leftMin) { left = leftMin - } else if (fullWidth - dragPosition - dividerHitBoxWidth < rightMin) { + } else if (fullWidth - dragPosition < rightMin) { left = fullWidth - rightMin } return left @@ -97,29 +97,29 @@ export const TwoColumn = ({ setFullWidth(newFullWidth) }, [ref, baseLayoutValue]) // eslint-disable-line react-hooks/exhaustive-deps - const leftFocus = convertToLeftWidth(baseLayoutValue, fullWidth) < leftWidth - dividerHitBoxWidth / 2 + const leftFocus = convertToLeftWidth(baseLayoutValue, fullWidth) < leftWidth / 2 const [scrollbarsBarMaxHeight, setScrollbarsBarMaxHeight] = useState(800) - const handleWindowResize = () => { - setScrollbarsBarMaxHeight(window.innerHeight - headerHeight) - setFullWidth(window.innerWidth) - } + useResizeCallbackWrapper(useCallback(() => { + if (ref.current?.scrollHeight) { + setScrollbarsBarMaxHeight(ref.current.scrollHeight) + } + if (ref.current?.offsetWidth) { + setFullWidth(ref.current.offsetWidth) + } + }, [ref])) useEffect(() => { - window.addEventListener('resize', handleWindowResize) - return () => { - window.removeEventListener('resize', handleWindowResize) + if (ref.current?.scrollHeight) { + setScrollbarsBarMaxHeight(ref.current.scrollHeight) } }, []) - useEffect(handleWindowResize) - - const rightWidth = fullWidth - leftWidth - dividerHitBoxWidth + const rightWidth = fullWidth - leftWidth return (
isDragging ? setLeftWidth(calcPosition(event.pageX)) : undefined} onMouseUp={() => setIsDragging(false)} onTouchEnd={() => setIsDragging(false)} @@ -130,41 +130,38 @@ export const TwoColumn = ({ { /* TODO maybe use the loading animation or something else to create a smoother transition from showing nothing */} {fullWidth !== 0 && ( <> -
+
{left(leftWidth)}
disableResize ? undefined : setIsDragging(true)} - onTouchStart={() => disableResize ? undefined : setIsDragging(true)} - className={clsx(`row relative h-full justify-center`, { '!cursor-col-resize': !disableResize })} - style={{ width: `${dividerHitBoxWidth}px` }} + className={clsx(`absolute flex-col-0 h-full justify-center`)} + style={{ left: leftWidth + 'px' }} > -
+
{!disableResize && (
disableResize ? undefined : setIsDragging(true)} + onTouchStart={() => disableResize ? undefined : setIsDragging(true)} > - +
)} {!disableResize && ( )}
-
+
{right(rightWidth)} diff --git a/tasks/components/modals/TaskDetailModal.tsx b/tasks/components/modals/TaskDetailModal.tsx index 6b7844448..fc3afb36e 100644 --- a/tasks/components/modals/TaskDetailModal.tsx +++ b/tasks/components/modals/TaskDetailModal.tsx @@ -255,15 +255,17 @@ const TaskDetailViewSidebar = ({ ) } -export type TaskDetailModalProps = Omit & { +type CreateInformation = { + wardId: string, + patientId: string, + initialStatus: TaskStatus, +} +export type TaskDetailModalProps = Omit & Pick & { /** * A not set or empty taskId is seen as creating a new task */ taskId?: string, - wardId: string, - patientId: string, - onClose: () => void, - initialStatus?: TaskStatus, + createInformation?: CreateInformation, } /** @@ -271,11 +273,9 @@ export type TaskDetailModalProps = Omit & */ export const TaskDetailModal = ({ overwriteTranslation, - patientId, taskId = '', - wardId, - initialStatus, onClose, + createInformation, className, ...modalProps }: PropsForTranslation) => { @@ -297,14 +297,15 @@ export const TaskDetailModal = ({ const [task, setTask] = useState({ ...emptyTask, - status: initialStatus ?? 'todo' + patientId: createInformation?.patientId ?? '', + status: createInformation?.initialStatus ?? 'todo' }) const deleteTaskMutation = useTaskDeleteMutation() const updateTaskMutation = useTaskUpdateMutation() - const createTaskMutation = useTaskCreateMutation(() => { - onClose() - }, patientId) + const createTaskMutation = useTaskCreateMutation({ + onSuccess: () => onClose() + }) useEffect(() => { if (data && taskId) { @@ -321,7 +322,7 @@ export const TaskDetailModal = ({ data: wardTaskTemplatesData, isLoading: wardTaskTemplatesIsLoading, error: wardTaskTemplatesError - } = useWardTaskTemplateQuery(wardId) + } = useWardTaskTemplateQuery(createInformation?.wardId) const taskNameMinimumLength = 1 const isValid = task.name.length >= taskNameMinimumLength @@ -395,7 +396,7 @@ export const TaskDetailModal = ({ const templateSidebar = (
{personalTaskTemplatesData && wardTaskTemplatesData && ( router.push(`/ward/${wardId}/templates`)} + onColumnEditClick={() => router.push(`/ward/${createInformation?.wardId}/templates`)} /> )} {(personalTaskTemplatesIsLoading || wardTaskTemplatesIsLoading || personalTaskTemplatesError || wardTaskTemplatesError) ? diff --git a/tasks/components/pill/PillLabel.tsx b/tasks/components/pill/PillLabel.tsx deleted file mode 100644 index e69de29bb..000000000 diff --git a/tasks/globals.css b/tasks/globals.css index fbf0d4e62..01769ef78 100644 --- a/tasks/globals.css +++ b/tasks/globals.css @@ -4,9 +4,30 @@ @source "./node_modules/@helpwave/hightide"; @theme { + /* Colors */ + --color-todo-background: var(--color-tag-red-background); + --color-todo-text: var(--color-tag-red-text); + --color-todo-icon: var(--color-tag-red-icon); + + --color-inprogress-background: var(--color-tag-yellow-background); + --color-inprogress-text: var(--color-tag-yellow-text); + --color-inprogress-icon: var(--color-tag-yellow-icon); + + --color-done-background: var(--color-tag-green-background); + --color-done-text: var(--color-tag-green-text); + --color-done-icon: var(--color-tag-green-icon); + /* Header */ --color-header-background: var(--color-white); - --color-header-text: var(--color-text-dark); + --color-header-text: var(--color-text-default); + + /* TaskModalSidebar */ + --color-task-modal-sidebar-background: var(--color-background); + --color-task-modal-sidebar-text: var(--color-text-default); + + /* TwoColumn */ + --color-twocolum-background: #C6C6C6; + --color-twocolum-text: #FFFFFF; } @layer base { @@ -14,6 +35,9 @@ @variant dark { /* Header */ --color-header-background: var(--color-black); - --color-header-text: var(--color-text-light); + + /* TwoColumn */ + --color-twocolum-background: #363636; + --color-twocolum-text: var(--color-description); } } diff --git a/tasks/pages/ward/[wardId].tsx b/tasks/pages/ward/[wardId].tsx index 65c53e205..9a8b68495 100644 --- a/tasks/pages/ward/[wardId].tsx +++ b/tasks/pages/ward/[wardId].tsx @@ -329,9 +329,11 @@ const WardOverview: NextPage = ({ overwriteTranslation }: PropsForTranslation ) )} From be9b7005a2b3aa403cfef56cf67619d064f0eb25 Mon Sep 17 00:00:00 2001 From: Felix Thape Date: Thu, 17 Jul 2025 22:40:29 +0200 Subject: [PATCH 2/5] feat: implement hightide version 0.1.24 --- api-services/authentication/useAuth.tsx | 2 +- .../properties/property_mutations.ts | 193 ++------------- .../properties/property_value_mutations.ts | 160 ++---------- .../properties/property_view_src_mutations.ts | 59 ++--- api-services/mutations/tasks/bed_mutations.ts | 90 ++----- .../mutations/tasks/patient_mutations.ts | 26 +- .../mutations/tasks/room_mutations.ts | 2 +- .../mutations/tasks/task_mutations.ts | 8 + .../tasks/task_template_mutations.ts | 25 +- .../AttachedPropertyValueService.ts | 146 +++++++++++ .../service/properties/PropertyService.ts | 167 +++++++++++++ .../properties/PropertyViewSourceService.ts | 48 ++++ api-services/service/tasks/BedService.ts | 47 ++++ api-services/service/tasks/PatientService.ts | 213 ++++++++++++++++ .../service/tasks/TaskTemplateService.ts | 25 ++ api-services/service/users/PatientService.ts | 227 ------------------ .../types/properties/attached_property.ts | 4 +- api-services/types/properties/property.ts | 10 +- api-services/util/util.ts | 6 +- customer/pages/settings/index.tsx | 2 +- customer/pages/team/index.tsx | 2 +- pnpm-lock.yaml | 10 +- tasks/components/KanbanHeader.tsx | 2 + tasks/components/OrganizationForm.tsx | 1 - .../components/OrganizationInvitationList.tsx | 1 - tasks/components/OrganizationMemberList.tsx | 3 +- tasks/components/RoomList.tsx | 1 - tasks/components/TaskTemplateWardPreview.tsx | 1 - tasks/components/UserInvitationList.tsx | 3 +- tasks/components/UserMenu.tsx | 5 +- tasks/components/cards/OrganizationCard.tsx | 17 +- tasks/components/cards/TaskCard.tsx | 4 +- tasks/components/cards/UserCard.tsx | 3 +- tasks/components/layout/DashboardDisplay.tsx | 6 +- tasks/components/layout/PatientDetails.tsx | 8 +- tasks/components/layout/PatientList.tsx | 1 - tasks/components/layout/TasksKanbanBoard.tsx | 1 - tasks/components/layout/WardDetails.tsx | 1 - .../layout/property/PropertyDetails.tsx | 11 +- .../layout/property/PropertyDetailsField.tsx | 12 +- .../layout/property/PropertyDetailsRules.tsx | 18 +- .../layout/property/PropertyDisplay.tsx | 9 +- .../layout/property/PropertyEntry.tsx | 62 ++++- .../layout/property/PropertyList.tsx | 64 +++-- .../property/PropertySubjectTypeSelect.tsx | 6 +- .../layout/property/SubjectTypeIcon.tsx | 4 +- tasks/components/modals/ManageBedsModal.tsx | 1 - tasks/components/modals/TaskDetailModal.tsx | 1 - tasks/components/selects/AssigneeSelect.tsx | 3 +- tasks/next.config.js | 1 + tasks/package.json | 2 +- tasks/pages/_app.tsx | 17 +- tasks/pages/index.tsx | 1 - tasks/pages/properties.tsx | 6 +- tasks/pages/templates.tsx | 1 - tasks/pages/ward/[wardId].tsx | 2 +- 56 files changed, 945 insertions(+), 806 deletions(-) create mode 100644 api-services/service/properties/AttachedPropertyValueService.ts create mode 100644 api-services/service/properties/PropertyService.ts create mode 100644 api-services/service/properties/PropertyViewSourceService.ts create mode 100644 api-services/service/tasks/BedService.ts create mode 100644 api-services/service/tasks/PatientService.ts create mode 100644 api-services/service/tasks/TaskTemplateService.ts delete mode 100644 api-services/service/users/PatientService.ts diff --git a/api-services/authentication/useAuth.tsx b/api-services/authentication/useAuth.tsx index 61a53a481..64ccec1b4 100644 --- a/api-services/authentication/useAuth.tsx +++ b/api-services/authentication/useAuth.tsx @@ -26,7 +26,7 @@ const UserFromIdTokenClaims = IdTokenClaimsSchema.transform((obj) => ({ email: obj.email, name: obj.name, nickname: obj.preferred_username, - avatarUrl: `https://cdn.helpwave.de/boringavatar.svg#${obj.sub}`, + avatarUrl: `https://cdn.helpwave.de/boringavatar.svg`, organization: obj.organization })) diff --git a/api-services/mutations/properties/property_mutations.ts b/api-services/mutations/properties/property_mutations.ts index 0b5ddb3ea..d564fcdb6 100644 --- a/api-services/mutations/properties/property_mutations.ts +++ b/api-services/mutations/properties/property_mutations.ts @@ -1,53 +1,14 @@ +import type { UseMutationOptions } from '@tanstack/react-query' import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query' -import { noop } from '@helpwave/hightide' -import { - CreatePropertyRequest, - GetPropertiesRequest, - GetPropertyRequest, - UpdatePropertyRequest -} from '@helpwave/proto-ts/services/property_svc/v1/property_svc_pb' -import { FieldType } from '@helpwave/proto-ts/services/property_svc/v1/types_pb' -import { APIServices } from '../../services' -import { getAuthenticatedGrpcMetadata } from '../../authentication/grpc_metadata' import { QueryKeys } from '../query_keys' -import type { Property, SelectData, SelectOption, SubjectType } from '../../types/properties/property' -import { GRPCConverter } from '../../util/util' +import type { Property, SelectOption, PropertySubjectType } from '../../types/properties/property' +import { PropertyService } from '../../service/properties/PropertyService' -export const usePropertyListQuery = (subjectType?: SubjectType) => { +export const usePropertyListQuery = (subjectType?: PropertySubjectType) => { return useQuery({ queryKey: [QueryKeys.properties, subjectType ?? 'all'], queryFn: async (): Promise => { - const req = new GetPropertiesRequest() - if (subjectType) { - req.setSubjectType(GRPCConverter.subjectTypeMapperToGRPC(subjectType)) - } - const result = await APIServices.property.getProperties(req, getAuthenticatedGrpcMetadata()) - return result.getPropertiesList().filter(value => value.getFieldType() !== FieldType.FIELD_TYPE_UNSPECIFIED).map(property => { - const fieldType = GRPCConverter.fieldTypeMapperFromGRPC(property.getFieldType()) - const selectData = property.getSelectData() - const mustHaveSelectData = fieldType === 'singleSelect' || fieldType === 'multiSelect' - if (!selectData && mustHaveSelectData) { - throw Error('usePropertyListQuery could not find selectData') - } - return { - id: property.getId(), - name: property.getName(), - description: property.getDescription(), - subjectType: GRPCConverter.subjectTypeMapperFromGRPC(property.getSubjectType()), - fieldType, - isArchived: property.getIsArchived(), - setId: property.getSetId(), - selectData: mustHaveSelectData ? { - isAllowingFreetext: selectData!.getAllowFreetext(), - options: selectData!.getOptionsList().map(option => ({ - id: option.getId(), - name: option.getName(), - description: option.getDescription(), - isCustom: option.getIsCustom() - })) - } : undefined - } - }) + return await PropertyService.getList(subjectType) }, }) } @@ -57,89 +18,22 @@ export const usePropertyQuery = (id?: string) => { queryKey: [QueryKeys.properties, id], enabled: !!id, queryFn: async (): Promise => { - if (!id) { - throw Error('usePropertyQuery no id in mutate') - } - const req = new GetPropertyRequest() - req.setId(id) - - const result = await APIServices.property.getProperty(req, getAuthenticatedGrpcMetadata()) - - const fieldType = GRPCConverter.fieldTypeMapperFromGRPC(result.getFieldType()) - let selectData: SelectData | undefined - if (fieldType === 'singleSelect' || fieldType === 'multiSelect') { - const responseSelectData = result.getSelectData() - if (!responseSelectData) { - throw Error('usePropertyQuery could not find selectData') - } - selectData = { - isAllowingFreetext: responseSelectData.getAllowFreetext(), - options: responseSelectData.getOptionsList().map(option => ({ - id: option.getId(), - name: option.getName(), - description: option.getDescription(), - isCustom: option.getIsCustom() - })) - } - } - return { - id: result.getId(), - name: result.getName(), - description: result.getDescription(), - subjectType: GRPCConverter.subjectTypeMapperFromGRPC(result.getSubjectType()), - fieldType, - isArchived: result.getIsArchived(), - setId: result.getSetId(), - alwaysIncludeForViewSource: result.getAlwaysIncludeForViewSource(), - selectData, - } + return await PropertyService.get(id!) }, }) } -export const usePropertyCreateMutation = (callback: (property: Property) => void = noop) => { +export const usePropertyCreateMutation = (options?: UseMutationOptions) => { const queryClient = useQueryClient() return useMutation({ + ...options, mutationFn: async (property: Property) => { - const req = new CreatePropertyRequest() - req.setName(property.name) - if (property.description) { - req.setDescription(property.description) - } - req.setSubjectType(GRPCConverter.subjectTypeMapperToGRPC(property.subjectType)) - req.setFieldType(GRPCConverter.fieldTypeMapperToGRPC(property.fieldType)) - if (property.setId) { - req.setSetId(property.setId) - } - if (property.fieldType === 'singleSelect' || property.fieldType === 'multiSelect') { - if (!property.selectData) { - throw Error('Select FieldType, but select data not set') - } - const selectDataVal = new CreatePropertyRequest.SelectData() - selectDataVal.setAllowFreetext(property.selectData.isAllowingFreetext) - selectDataVal.setOptionsList(property.selectData.options.map(option => { - const optionVal = new CreatePropertyRequest.SelectData.SelectOption() - optionVal.setName(option.name) - if (option.description) { - optionVal.setDescription(option.description) - } - return optionVal - })) - req.setSelectData(selectDataVal) - } - - const result = await APIServices.property.createProperty(req, getAuthenticatedGrpcMetadata()) - - const id = result.getPropertyId() - - const newValue = { - ...property, - id - } - callback(newValue) - return newValue + return await PropertyService.create(property) }, - onSuccess: () => { + onSuccess: (data, variables, context) => { + if(options?.onSuccess) { + options.onSuccess(data, variables, context) + } queryClient.invalidateQueries([QueryKeys.properties]).catch(console.error) } }) @@ -156,62 +50,17 @@ export type PropertyUpdateType = { selectUpdate?: PropertySelectDataUpdate, } -export const usePropertyUpdateMutation = (callback: (property: Property) => void = noop) => { +export const usePropertyUpdateMutation = (options?: UseMutationOptions) => { const queryClient = useQueryClient() return useMutation({ - mutationFn: async ({ property, selectUpdate }: PropertyUpdateType) => { - const req = new UpdatePropertyRequest() - req.setId(property.id) - req.setName(property.name) - req.setIsArchived(property.isArchived) - req.setSubjectType(GRPCConverter.subjectTypeMapperToGRPC(property.subjectType)) - if (property.description) { - req.setDescription(property.description) - } - if (property.setId) { - req.setSetId(property.setId) - } - if (property.fieldType === 'singleSelect' || property.fieldType === 'multiSelect') { - if (!property.selectData) { - throw Error('Select FieldType, but select data not set') - } - const selectDataVal = new UpdatePropertyRequest.SelectData() - selectDataVal.setAllowFreetext(property.selectData.isAllowingFreetext) - if(selectUpdate) { - const createList = selectUpdate.add.map(option => { - const optionVal = new UpdatePropertyRequest.SelectData.SelectOption() - optionVal.setId('') - optionVal.setName(option.name) - if (option.description) { - optionVal.setDescription(option.description) - } - optionVal.setIsCustom(option.isCustom) - return optionVal - }) - const updateList = selectUpdate.update.map(option => { - const optionVal = new UpdatePropertyRequest.SelectData.SelectOption() - optionVal.setId(option.id) - optionVal.setName(option.name) - if (option.description) { - optionVal.setDescription(option.description) - } - optionVal.setIsCustom(option.isCustom) - return optionVal - }) - selectDataVal.setUpsertOptionsList([...updateList, ...createList]) - selectDataVal.setRemoveOptionsList(selectUpdate.remove) - } - req.setSelectData(selectDataVal) - } - - const result = await APIServices.property.updateProperty(req, getAuthenticatedGrpcMetadata()) - if (!result.toObject()) { - throw Error('usePropertyUpdateMutation: error in result') - } - callback(property) - return property + ...options, + mutationFn: async (props) => { + return await PropertyService.update(props) }, - onSuccess: () => { + onSuccess: (data, variables, context) => { + if(options?.onSuccess) { + options.onSuccess(data, variables, context) + } queryClient.invalidateQueries([QueryKeys.properties]).catch(console.error) } }) diff --git a/api-services/mutations/properties/property_value_mutations.ts b/api-services/mutations/properties/property_value_mutations.ts index 69ed366fb..fcad1adc8 100644 --- a/api-services/mutations/properties/property_value_mutations.ts +++ b/api-services/mutations/properties/property_value_mutations.ts @@ -1,164 +1,38 @@ -import { noop } from '@helpwave/hightide' +import type { UseMutationOptions } from '@tanstack/react-query' import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query' -import { - AttachPropertyValueRequest, - GetAttachedPropertyValuesRequest, - GetAttachedPropertyValuesResponse, - PatientPropertyMatcher, TaskPropertyMatcher -} from '@helpwave/proto-ts/services/property_svc/v1/property_value_svc_pb' -import { - Date as ProtoDate -} from '@helpwave/proto-ts/services/property_svc/v1/types_pb' -import { ArrayUtil } from '@helpwave/hightide' -import { APIServices } from '../../services' -import { getAuthenticatedGrpcMetadata } from '../../authentication/grpc_metadata' import { QueryKeys } from '../query_keys' -import type { FieldType, SubjectType } from '../../types/properties/property' -import type { AttachedProperty, DisplayableAttachedProperty } from '../../types/properties/attached_property' -import { GRPCConverter } from '../../util/util' -import type { Update } from '../../types/update' -import { emptyPropertyValue } from '../../types/properties/attached_property' +import type { PropertySubjectType } from '../../types/properties/property' +import type { AttachedProperty } from '../../types/properties/attached_property' +import type { + AttachedPropertyMutationUpdate } from '../../service/properties/AttachedPropertyValueService' +import { + AttachedPropertyValueService +} from '../../service/properties/AttachedPropertyValueService' -export const usePropertyWithValueListQuery = (subjectId: string | undefined, subjectType: SubjectType, wardId?: string) => { +export const usePropertyWithValueListQuery = (subjectId: string | undefined, subjectType: PropertySubjectType, wardId?: string) => { return useQuery({ queryKey: [QueryKeys.properties, QueryKeys.attachedProperties, subjectId], enabled: !!subjectId, queryFn: async () => { - if (!subjectId) { - return undefined - } - const req = new GetAttachedPropertyValuesRequest() - - switch (subjectType) { - case 'task': { - const taskMatcher = new TaskPropertyMatcher() - taskMatcher.setTaskId(subjectId) - if (wardId) taskMatcher.setWardId(wardId) - req.setTaskMatcher(taskMatcher) - break - } - case 'patient': { - const patientMatcher = new PatientPropertyMatcher() - patientMatcher.setPatientId(subjectId) - if (wardId) patientMatcher.setWardId(wardId) - req.setPatientMatcher(patientMatcher) - break - } - } - - const res = await APIServices.propertyValues.getAttachedPropertyValues(req, getAuthenticatedGrpcMetadata()) - - const results: DisplayableAttachedProperty[] = res.getValuesList().map(result => { - const selectValue = result.getSelectValue() - const valueCase = result.getValueCase() - const isValueNotSet = valueCase === GetAttachedPropertyValuesResponse.Value.ValueCase.VALUE_NOT_SET - - return { - propertyId: result.getPropertyId(), - subjectId, - subjectType, - name: result.getName(), - description: result.getDescription(), - fieldType: GRPCConverter.fieldTypeMapperFromGRPC(result.getFieldType()), - value: isValueNotSet ? emptyPropertyValue : { - textValue: valueCase !== GetAttachedPropertyValuesResponse.Value.ValueCase.TEXT_VALUE ? undefined : result.getTextValue(), - numberValue: valueCase !== GetAttachedPropertyValuesResponse.Value.ValueCase.NUMBER_VALUE ? undefined : result.getNumberValue(), - boolValue: valueCase !== GetAttachedPropertyValuesResponse.Value.ValueCase.BOOL_VALUE ? undefined : result.getBoolValue(), - dateValue: valueCase !== GetAttachedPropertyValuesResponse.Value.ValueCase.DATE_VALUE ? undefined : result.getDateValue()!.getDate()!.toDate(), - dateTimeValue: valueCase !== GetAttachedPropertyValuesResponse.Value.ValueCase.DATE_TIME_VALUE ? undefined : result.getDateTimeValue()!.toDate(), - singleSelectValue: valueCase !== GetAttachedPropertyValuesResponse.Value.ValueCase.SELECT_VALUE ? undefined : { - id: selectValue!.getId(), - name: selectValue!.getName(), - description: selectValue!.getDescription(), - }, - multiSelectValue: (valueCase !== GetAttachedPropertyValuesResponse.Value.ValueCase.MULTI_SELECT_VALUE ? [] : result.getMultiSelectValue()!.getSelectValuesList()).map(value => ({ - id: value.getId(), - name: value.getName(), - description: value.getDescription() - })) ?? [], - } - } - }) - return results + return await AttachedPropertyValueService.get({ subjectId: subjectId!, subjectType, wardId }) }, }) } -type AttachedPropertyMutationUpdate = Update & { - fieldType: FieldType, -} - /** * Mutation to insert or update a properties value for a properties attached to a subject */ -export const useAttachPropertyMutation = (callback: (property: T) => void = noop) => { +export const useAttachPropertyMutation = (options?: UseMutationOptions>) => { const queryClient = useQueryClient() return useMutation({ + ...options, mutationFn: async (update: AttachedPropertyMutationUpdate) => { - const req = new AttachPropertyValueRequest() - const { update: property, previous, fieldType } = update - - req.setPropertyId(property.propertyId) - .setSubjectId(property.subjectId) - - switch (fieldType) { - case 'text': - if (property.value.textValue !== undefined) { - req.setTextValue(property.value.textValue) - } - break - case 'number': - if (property.value.numberValue !== undefined) { - req.setNumberValue(property.value.numberValue) - } - break - case 'checkbox': - if (property.value.boolValue !== undefined) { - req.setBoolValue(property.value.boolValue) - } - break - case 'date': - if (property.value.dateValue !== undefined) { - const protoDate = new ProtoDate().setDate(GRPCConverter.dateToTimestamp(property.value.dateValue)) - req.setDateValue(protoDate) - } - break - case 'dateTime': - if (property.value.dateTimeValue !== undefined) { - req.setDateTimeValue(GRPCConverter.dateToTimestamp(property.value.dateTimeValue)) - } - break - case 'singleSelect': - if (property.value.singleSelectValue !== undefined) { - req.setSelectValue(property.value.singleSelectValue.id) - } - break - case 'multiSelect': - if (property.value.multiSelectValue !== undefined) { - const previousOptions = previous?.value.multiSelectValue?.map(value => value.id) ?? [] - const newOptions = property.value.multiSelectValue.map(value => value.id) - const addIds = ArrayUtil.difference(newOptions, previousOptions) - const deleteIds = ArrayUtil.difference(previousOptions, newOptions) - - req.setMultiSelectValue(new AttachPropertyValueRequest.MultiSelectValue() - .setSelectValuesList(addIds) - .setRemoveSelectValuesList(deleteIds)) - } - break - default: - console.warn('invalid type for property value mutation') - } - - await APIServices.propertyValues.attachPropertyValue(req, getAuthenticatedGrpcMetadata()) - - const newProperty: T = { - ...property, - } - - callback(newProperty) - return newProperty + return await AttachedPropertyValueService.create(update) }, - onSuccess: (data) => { + onSuccess: (data, variables, context) => { + if (options?.onSuccess) { + options.onSuccess(data, variables, context) + } queryClient.invalidateQueries([QueryKeys.properties, QueryKeys.attachedProperties, data.subjectId]).catch(console.error) }, }) diff --git a/api-services/mutations/properties/property_view_src_mutations.ts b/api-services/mutations/properties/property_view_src_mutations.ts index 14c3f2e0f..4d0492294 100644 --- a/api-services/mutations/properties/property_view_src_mutations.ts +++ b/api-services/mutations/properties/property_view_src_mutations.ts @@ -1,54 +1,25 @@ +import type { UseMutationOptions } from '@tanstack/react-query' import { useMutation, useQueryClient } from '@tanstack/react-query' -import { - FilterUpdate, - UpdatePropertyViewRuleRequest -} from '@helpwave/proto-ts/services/property_svc/v1/property_views_svc_pb' -import { - PatientPropertyMatcher, - TaskPropertyMatcher -} from '@helpwave/proto-ts/services/property_svc/v1/property_value_svc_pb' import { QueryKeys } from '../query_keys' -import type { SubjectType } from '../../types/properties/property' -import { APIServices } from '../../services' -import { getAuthenticatedGrpcMetadata } from '../../authentication/grpc_metadata' - -type PropertyViewRuleFilterUpdate = { - subjectId: string, - appendToAlwaysInclude?: string[], - removeFromAlwaysInclude?: string[], - appendToDontAlwaysInclude?: string[], - removeFromDontAlwaysInclude?: string[], -} +import type { PropertySubjectType } from '../../types/properties/property' +import type { + PropertyViewRuleFilterUpdate +} from '../../service/properties/PropertyViewSourceService' +import { + PropertyViewSourceService +} from '../../service/properties/PropertyViewSourceService' -export const useUpdatePropertyViewRuleRequest = (subjectType: SubjectType, wardId?: string) => { +export const useUpdatePropertyViewRuleRequest = (subjectType: PropertySubjectType, wardId?: string, options?: UseMutationOptions) => { const queryClient = useQueryClient() return useMutation({ + ...options, mutationFn: async (update: PropertyViewRuleFilterUpdate) => { - const req = new UpdatePropertyViewRuleRequest() - if (subjectType === 'patient') { - const matcher = new PatientPropertyMatcher().setPatientId(update.subjectId) - if (wardId) { - matcher.setWardId(wardId) - } - req.setPatientMatcher(matcher) - } - if (subjectType === 'task') { - const matcher = new TaskPropertyMatcher().setTaskId(update.subjectId) - if (wardId) { - matcher.setWardId(wardId) - } - req.setTaskMatcher(matcher) - } - - req.setFilterUpdate(new FilterUpdate() - .setAppendToAlwaysIncludeList(update.appendToAlwaysInclude ?? []) - .setRemoveFromAlwaysIncludeList(update.removeFromAlwaysInclude ?? []) - .setAppendToDontAlwaysIncludeList(update.removeFromAlwaysInclude ?? []) - .setRemoveFromDontAlwaysIncludeList(update.removeFromDontAlwaysInclude ?? [])) - - await APIServices.propertyViewSource.updatePropertyViewRule(req, getAuthenticatedGrpcMetadata()) + return await PropertyViewSourceService.update(update, subjectType, wardId) }, - onSuccess: () => { + onSuccess: (data, variables, context) => { + if(options?.onSuccess) { + options.onSuccess(data, variables, context) + } queryClient.invalidateQueries([QueryKeys.properties]).catch(console.error) } }) diff --git a/api-services/mutations/tasks/bed_mutations.ts b/api-services/mutations/tasks/bed_mutations.ts index 74a79d1c4..378ff628f 100644 --- a/api-services/mutations/tasks/bed_mutations.ts +++ b/api-services/mutations/tasks/bed_mutations.ts @@ -1,54 +1,32 @@ +import type { UseMutationOptions } from '@tanstack/react-query' import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query' -import { - CreateBedRequest, - DeleteBedRequest, - GetBedRequest, - UpdateBedRequest -} from '@helpwave/proto-ts/services/tasks_svc/v1/bed_svc_pb' import { QueryKeys } from '../query_keys' import { APIServices } from '../../services' -import { getAuthenticatedGrpcMetadata } from '../../authentication/grpc_metadata' import type { BedWithRoomId } from '../../types/tasks/bed' import { roomOverviewsQueryKey } from './room_mutations' +import { BedService } from '../../service/tasks/BedService' -export const useBedQuery = (bedId: string | undefined) => { +export const useBedQuery = (id?: string) => { return useQuery({ queryKey: [QueryKeys.beds], - enabled: !!bedId, + enabled: !!id, queryFn: async () => { - const req = new GetBedRequest() - if (bedId) { - req.setId(bedId) - } - const res = await APIServices.bed.getBed(req, getAuthenticatedGrpcMetadata()) - - const bed: BedWithRoomId = { - id: res.getId(), - name: res.getName(), - roomId: res.getRoomId() - } - - return bed + return await BedService.get(id!) }, }) } -export const useBedCreateMutation = () => { +export const useBedCreateMutation = (options?: UseMutationOptions) => { const queryClient = useQueryClient() return useMutation({ + ...options, mutationFn: async (bed: BedWithRoomId) => { - const req = new CreateBedRequest() - req.setRoomId(bed.roomId) - req.setName(bed.name) - const res = await APIServices.bed.createBed(req, getAuthenticatedGrpcMetadata()) - - if (!res.toObject()) { - console.error('error in BedCreate') - } - - return { id: res.getId(), name: bed.name } + return await BedService.create(bed) }, - onSuccess: () => { + onSuccess: (data, variables, context) => { + if (options?.onSuccess) { + options.onSuccess(data, variables, context) + } queryClient.invalidateQueries([QueryKeys.beds]).catch(console.error) queryClient.invalidateQueries([QueryKeys.rooms, roomOverviewsQueryKey]).catch(console.error) queryClient.invalidateQueries([QueryKeys.wards]).catch(console.error) @@ -56,50 +34,34 @@ export const useBedCreateMutation = () => { }) } -export const useBedUpdateMutation = () => { +export const useBedUpdateMutation = (options?: UseMutationOptions) => { const queryClient = useQueryClient() return useMutation({ + ...options, mutationFn: async (bed: BedWithRoomId) => { - const req = new UpdateBedRequest() - req.setId(bed.id) - req.setName(bed.name) - req.setRoomId(bed.roomId) - - const res = await APIServices.bed.updateBed(req, getAuthenticatedGrpcMetadata()) - - const obj = res.toObject() // TODO: what is the type of this? - - if (!obj) { - throw new Error('error in BedUpdate') - } - - return obj + return await BedService.update(bed) }, - onSuccess: () => { + onSuccess: (data, variables, context) => { + if (options?.onSuccess) { + options.onSuccess(data, variables, context) + } queryClient.invalidateQueries([APIServices.bed]).catch(console.error) queryClient.invalidateQueries([QueryKeys.rooms, roomOverviewsQueryKey]).catch(console.error) }, }) } -export const useBedDeleteMutation = () => { +export const useBedDeleteMutation = (options?: UseMutationOptions) => { const queryClient = useQueryClient() return useMutation({ + ...options, mutationFn: async (bedId: string) => { - const req = new DeleteBedRequest() - req.setId(bedId) - - const res = await APIServices.bed.deleteBed(req, getAuthenticatedGrpcMetadata()) - - const obj = res.toObject() // TODO: what is the type of this? - - if (!obj) { - throw new Error('error in BedDelete') - } - - return obj + return await BedService.delete(bedId) }, - onSuccess: () => { + onSuccess: (data, variables, context) => { + if (options?.onSuccess) { + options.onSuccess(data, variables, context) + } queryClient.invalidateQueries([APIServices.bed]).catch(console.error) queryClient.invalidateQueries([QueryKeys.rooms, roomOverviewsQueryKey]).catch(console.error) queryClient.invalidateQueries([QueryKeys.wards]).catch(console.error) diff --git a/api-services/mutations/tasks/patient_mutations.ts b/api-services/mutations/tasks/patient_mutations.ts index 646b2dcc9..9a8698603 100644 --- a/api-services/mutations/tasks/patient_mutations.ts +++ b/api-services/mutations/tasks/patient_mutations.ts @@ -4,18 +4,14 @@ import type { PatientDTO } from '../../types/tasks/patient' import { QueryKeys } from '../query_keys' import type { BedWithPatientId } from '../../types/tasks/bed' import { roomOverviewsQueryKey } from './room_mutations' -import { PatientService } from '../../service/users/PatientService' +import { PatientService } from '../../service/tasks/PatientService' export const usePatientDetailsQuery = (patientId?: string) => { return useQuery({ queryKey: [QueryKeys.patients, patientId], enabled: !!patientId, queryFn: async () => { - if (!patientId) { - return - } - - return await PatientService.getPatientDetails(patientId) + return await PatientService.getDetails(patientId!) }, }) } @@ -57,33 +53,41 @@ export const useRecentPatientsQuery = () => { }) } -export const usePatientCreateMutation = () => { +export const usePatientCreateMutation = (options?: UseMutationOptions) => { const queryClient = useQueryClient() return useMutation({ + ...options, mutationFn: async (patient: PatientDTO) => { return await PatientService.create(patient) }, - onSuccess: () => { + onSuccess: (data, variables, context) => { + if(options?.onSuccess) { + options.onSuccess(data, variables, context) + } queryClient.invalidateQueries([QueryKeys.rooms]).catch(reason => console.error(reason)) queryClient.invalidateQueries([QueryKeys.patients]).catch(reason => console.error(reason)) } }) } -export const usePatientUpdateMutation = () => { +export const usePatientUpdateMutation = (options?: UseMutationOptions) => { const queryClient = useQueryClient() return useMutation({ + ...options, mutationFn: async (patient: PatientDTO) => { return await PatientService.update(patient) }, - onSuccess: () => { + onSuccess: (data, variables, context) => { + if(options?.onSuccess) { + options.onSuccess(data, variables, context) + } queryClient.invalidateQueries([QueryKeys.patients]).catch(reason => console.error(reason)) queryClient.invalidateQueries([QueryKeys.rooms]).catch(reason => console.error(reason)) } }) } -export const useAssignBedMutation = (options?: UseMutationOptions) => { +export const useAssignBedMutation = (options?: UseMutationOptions) => { const queryClient = useQueryClient() return useMutation({ ...options, diff --git a/api-services/mutations/tasks/room_mutations.ts b/api-services/mutations/tasks/room_mutations.ts index 9d395b0a8..5a43babfa 100644 --- a/api-services/mutations/tasks/room_mutations.ts +++ b/api-services/mutations/tasks/room_mutations.ts @@ -60,7 +60,7 @@ export const useRoomOverviewsQuery = (wardId: string | undefined) => { patient: !patient ? undefined : { id: patient.getId(), name: patient.getHumanReadableIdentifier(), - tasksUnscheduled: patient.getTasksUnscheduled(), + tasksTodo: patient.getTasksUnscheduled(), tasksInProgress: patient.getTasksInProgress(), tasksDone: patient.getTasksDone() } diff --git a/api-services/mutations/tasks/task_mutations.ts b/api-services/mutations/tasks/task_mutations.ts index c655c886d..442aac343 100644 --- a/api-services/mutations/tasks/task_mutations.ts +++ b/api-services/mutations/tasks/task_mutations.ts @@ -39,6 +39,7 @@ export const useMyTasksQuery = () => { export const useTaskCreateMutation = (options?: UseMutationOptions) => { const queryClient = useQueryClient() return useMutation({ + ...options, mutationFn: async (task: TaskDTO) => { return await TaskService.create(task) }, @@ -55,6 +56,7 @@ export const useTaskCreateMutation = (options?: UseMutationOptions) => { const queryClient = useQueryClient() return useMutation({ + ...options, mutationFn: async (task: TaskDTO) => { return await TaskService.update(task) }, @@ -71,6 +73,7 @@ export const useTaskUpdateMutation = (options?: UseMutationOptions) => { const queryClient = useQueryClient() return useMutation({ + ...options, mutationFn: async (taskId: string) => { return await TaskService.delete(taskId) }, @@ -87,6 +90,7 @@ export const useTaskDeleteMutation = (options?: UseMutationOptions) => { const queryClient = useQueryClient() return useMutation({ + ...options, mutationFn: async (subtask: SubTaskDTO) => { return await TaskSubtaskService.create(subtask) }, @@ -102,6 +106,7 @@ export const useSubTaskAddMutation = (options?: UseMutationOptions) => { const queryClient = useQueryClient() return useMutation({ + ...options, mutationFn: async (subtask: SubTaskDTO) => { return await TaskSubtaskService.update(subtask) }, @@ -117,6 +122,7 @@ export const useSubTaskUpdateMutation = (options?: UseMutationOptions) => { const queryClient = useQueryClient() return useMutation({ + ...options, mutationFn: async (subtaskId: string) => { return await TaskSubtaskService.delete(subtaskId) }, @@ -132,6 +138,7 @@ export const useSubTaskDeleteMutation = (options?: UseMutationOptions) => { const queryClient = useQueryClient() return useMutation({ + ...options, mutationFn: async (props: TaskAssignmentRequestProps) => { return await TaskService.assign(props) }, @@ -147,6 +154,7 @@ export const useAssignTaskMutation = (options?: UseMutationOptions) => { const queryClient = useQueryClient() return useMutation({ + ...options, mutationFn: async (props: TaskAssignmentRequestProps) => { return await TaskService.unassign(props) }, diff --git a/api-services/mutations/tasks/task_template_mutations.ts b/api-services/mutations/tasks/task_template_mutations.ts index 1ba23b1b8..77f056db9 100644 --- a/api-services/mutations/tasks/task_template_mutations.ts +++ b/api-services/mutations/tasks/task_template_mutations.ts @@ -13,35 +13,16 @@ import type { TaskTemplateDTO, TaskTemplateFormType } from '../../types/tasks/ta import { APIServices } from '../../services' import { getAuthenticatedGrpcMetadata } from '../../authentication/grpc_metadata' import type { SubTaskDTO } from '../../types/tasks/task' +import { TaskTemplateService } from '../../service/tasks/TaskTemplateService' type QueryKey = 'personalTaskTemplates' | 'wardTaskTemplates' export const useWardTaskTemplateQuery = (wardId?: string) => { return useQuery({ queryKey: ['wardTaskTemplates', wardId], + enabled: !!wardId, queryFn: async () => { - let wardTaskTemplates: TaskTemplateDTO[] = [] - if (wardId !== undefined) { - const req = new GetAllTaskTemplatesRequest() - .setWardId(wardId) - const res = await APIServices.taskTemplates.getAllTaskTemplates(req, getAuthenticatedGrpcMetadata()) - wardTaskTemplates = res.getTemplatesList().map((template) => ({ - id: template.getId(), - wardId, - name: template.getName(), - notes: template.getDescription(), - subtasks: template.getSubtasksList().map((subtask) => ({ - id: subtask.getId(), - name: subtask.getName(), - isDone: false, - taskId: template.getId(), - })), - isPublicVisible: template.getIsPublic() - })) - return wardTaskTemplates - } - - return wardTaskTemplates + return await TaskTemplateService.getWard(wardId!) }, }) } diff --git a/api-services/service/properties/AttachedPropertyValueService.ts b/api-services/service/properties/AttachedPropertyValueService.ts new file mode 100644 index 000000000..e10a9430e --- /dev/null +++ b/api-services/service/properties/AttachedPropertyValueService.ts @@ -0,0 +1,146 @@ +import type { AttachedProperty, DisplayableAttachedProperty } from '../../types/properties/attached_property' +import { emptyPropertyValue } from '../../types/properties/attached_property' +import type { FieldType, PropertySubjectType } from '../../types/properties/property' +import { + AttachPropertyValueRequest, + GetAttachedPropertyValuesRequest, + GetAttachedPropertyValuesResponse, + PatientPropertyMatcher, + TaskPropertyMatcher +} from '@helpwave/proto-ts/services/property_svc/v1/property_value_svc_pb' +import { APIServices } from '../../services' +import { getAuthenticatedGrpcMetadata } from '../../authentication/grpc_metadata' +import { GRPCConverter } from '../../util/util' +import type { Update } from '../../types/update' +import { Date as ProtoDate } from '@helpwave/proto-ts/services/property_svc/v1/types_pb' +import { ArrayUtil } from '@helpwave/hightide' + +export type AttachedPropertyValueIdentifier = { + subjectId: string, + subjectType: PropertySubjectType, + wardId?: string, +} + +export type AttachedPropertyMutationUpdate = Update & { + fieldType: FieldType, +} + +export const AttachedPropertyValueService = { + get: async (propertyValueIdentifier: AttachedPropertyValueIdentifier): Promise => { + const { subjectId , subjectType, wardId } = propertyValueIdentifier + + const req = new GetAttachedPropertyValuesRequest() + + switch (subjectType) { + case 'task': { + const taskMatcher = new TaskPropertyMatcher() + taskMatcher.setTaskId(subjectId) + if (wardId) taskMatcher.setWardId(wardId) + req.setTaskMatcher(taskMatcher) + break + } + case 'patient': { + const patientMatcher = new PatientPropertyMatcher() + patientMatcher.setPatientId(subjectId) + if (wardId) patientMatcher.setWardId(wardId) + req.setPatientMatcher(patientMatcher) + break + } + } + + const res = await APIServices.propertyValues.getAttachedPropertyValues(req, getAuthenticatedGrpcMetadata()) + + return res.getValuesList().map(result => { + const selectValue = result.getSelectValue() + const valueCase = result.getValueCase() + const isValueNotSet = valueCase === GetAttachedPropertyValuesResponse.Value.ValueCase.VALUE_NOT_SET + + return { + propertyId: result.getPropertyId(), + subjectId, + subjectType, + name: result.getName(), + description: result.getDescription(), + fieldType: GRPCConverter.fieldTypeMapperFromGRPC(result.getFieldType()), + value: isValueNotSet ? emptyPropertyValue : { + textValue: valueCase !== GetAttachedPropertyValuesResponse.Value.ValueCase.TEXT_VALUE ? undefined : result.getTextValue(), + numberValue: valueCase !== GetAttachedPropertyValuesResponse.Value.ValueCase.NUMBER_VALUE ? undefined : result.getNumberValue(), + boolValue: valueCase !== GetAttachedPropertyValuesResponse.Value.ValueCase.BOOL_VALUE ? undefined : result.getBoolValue(), + dateValue: valueCase !== GetAttachedPropertyValuesResponse.Value.ValueCase.DATE_VALUE ? undefined : result.getDateValue()!.getDate()!.toDate(), + dateTimeValue: valueCase !== GetAttachedPropertyValuesResponse.Value.ValueCase.DATE_TIME_VALUE ? undefined : result.getDateTimeValue()!.toDate(), + singleSelectValue: valueCase !== GetAttachedPropertyValuesResponse.Value.ValueCase.SELECT_VALUE ? undefined : { + id: selectValue!.getId(), + name: selectValue!.getName(), + description: selectValue!.getDescription(), + }, + multiSelectValue: (valueCase !== GetAttachedPropertyValuesResponse.Value.ValueCase.MULTI_SELECT_VALUE ? [] : result.getMultiSelectValue()!.getSelectValuesList()).map(value => ({ + id: value.getId(), + name: value.getName(), + description: value.getDescription() + })) ?? [], + } + } + }) + }, + create: async (update: AttachedPropertyMutationUpdate): Promise => { + const req = new AttachPropertyValueRequest() + const { update: property, previous, fieldType } = update + + req.setPropertyId(property.propertyId) + .setSubjectId(property.subjectId) + + switch (fieldType) { + case 'text': + if (property.value.textValue !== undefined) { + req.setTextValue(property.value.textValue) + } + break + case 'number': + if (property.value.numberValue !== undefined) { + req.setNumberValue(property.value.numberValue) + } + break + case 'checkbox': + if (property.value.boolValue !== undefined) { + req.setBoolValue(property.value.boolValue) + } + break + case 'date': + if (property.value.dateValue !== undefined) { + const protoDate = new ProtoDate().setDate(GRPCConverter.dateToTimestamp(property.value.dateValue)) + req.setDateValue(protoDate) + } + break + case 'dateTime': + if (property.value.dateTimeValue !== undefined) { + req.setDateTimeValue(GRPCConverter.dateToTimestamp(property.value.dateTimeValue)) + } + break + case 'singleSelect': + if (property.value.singleSelectValue !== undefined) { + req.setSelectValue(property.value.singleSelectValue.id) + } + break + case 'multiSelect': + if (property.value.multiSelectValue !== undefined) { + const previousOptions = previous?.value.multiSelectValue?.map(value => value.id) ?? [] + const newOptions = property.value.multiSelectValue.map(value => value.id) + const addIds = ArrayUtil.difference(newOptions, previousOptions) + const deleteIds = ArrayUtil.difference(previousOptions, newOptions) + + req.setMultiSelectValue(new AttachPropertyValueRequest.MultiSelectValue() + .setSelectValuesList(addIds) + .setRemoveSelectValuesList(deleteIds)) + } + break + default: + console.warn('invalid type for property value mutation') + } + + await APIServices.propertyValues.attachPropertyValue(req, getAuthenticatedGrpcMetadata()) + + return { + ...property, + } + } +} diff --git a/api-services/service/properties/PropertyService.ts b/api-services/service/properties/PropertyService.ts new file mode 100644 index 000000000..40a0822c3 --- /dev/null +++ b/api-services/service/properties/PropertyService.ts @@ -0,0 +1,167 @@ +import type { Property, PropertySelectData, PropertySubjectType } from '../../types/properties/property' +import { + CreatePropertyRequest, + GetPropertiesRequest, + GetPropertyRequest, UpdatePropertyRequest +} from '@helpwave/proto-ts/services/property_svc/v1/property_svc_pb' +import { GRPCConverter } from '../../util/util' +import { APIServices } from '../../services' +import { getAuthenticatedGrpcMetadata } from '../../authentication/grpc_metadata' +import { FieldType } from '@helpwave/proto-ts/services/property_svc/v1/types_pb' +import type { PropertyUpdateType } from '../../mutations/properties/property_mutations' + +export const PropertyService = { + get: async (id: string): Promise => { + const req = new GetPropertyRequest() + req.setId(id) + + const result = await APIServices.property.getProperty(req, getAuthenticatedGrpcMetadata()) + + const fieldType = GRPCConverter.fieldTypeMapperFromGRPC(result.getFieldType()) + let selectData: PropertySelectData | undefined + if (fieldType === 'singleSelect' || fieldType === 'multiSelect') { + const responseSelectData = result.getSelectData() + if (!responseSelectData) { + throw Error('usePropertyQuery could not find selectData') + } + selectData = { + isAllowingFreetext: responseSelectData.getAllowFreetext(), + options: responseSelectData.getOptionsList().map(option => ({ + id: option.getId(), + name: option.getName(), + description: option.getDescription(), + isCustom: option.getIsCustom() + })) + } + } + return { + id: result.getId(), + name: result.getName(), + description: result.getDescription(), + subjectType: GRPCConverter.subjectTypeMapperFromGRPC(result.getSubjectType()), + fieldType, + isArchived: result.getIsArchived(), + setId: result.getSetId(), + alwaysIncludeForViewSource: result.getAlwaysIncludeForViewSource(), + selectData, + } + }, + getList: async (subjectType?: PropertySubjectType): Promise => { + const req = new GetPropertiesRequest() + if (subjectType) { + req.setSubjectType(GRPCConverter.subjectTypeMapperToGRPC(subjectType)) + } + const result = await APIServices.property.getProperties(req, getAuthenticatedGrpcMetadata()) + return result.getPropertiesList().filter(value => value.getFieldType() !== FieldType.FIELD_TYPE_UNSPECIFIED).map(property => { + const fieldType = GRPCConverter.fieldTypeMapperFromGRPC(property.getFieldType()) + const selectData = property.getSelectData() + const mustHaveSelectData = fieldType === 'singleSelect' || fieldType === 'multiSelect' + if (!selectData && mustHaveSelectData) { + throw Error('usePropertyListQuery could not find selectData') + } + return { + id: property.getId(), + name: property.getName(), + description: property.getDescription(), + subjectType: GRPCConverter.subjectTypeMapperFromGRPC(property.getSubjectType()), + fieldType, + isArchived: property.getIsArchived(), + setId: property.getSetId(), + selectData: mustHaveSelectData ? { + isAllowingFreetext: selectData!.getAllowFreetext(), + options: selectData!.getOptionsList().map(option => ({ + id: option.getId(), + name: option.getName(), + description: option.getDescription(), + isCustom: option.getIsCustom() + })) + } : undefined + } + }) + }, + create: async (property: Property): Promise => { + const req = new CreatePropertyRequest() + req.setName(property.name) + if (property.description) { + req.setDescription(property.description) + } + req.setSubjectType(GRPCConverter.subjectTypeMapperToGRPC(property.subjectType)) + req.setFieldType(GRPCConverter.fieldTypeMapperToGRPC(property.fieldType)) + if (property.setId) { + req.setSetId(property.setId) + } + if (property.fieldType === 'singleSelect' || property.fieldType === 'multiSelect') { + if (!property.selectData) { + throw Error('Select FieldType, but select data not set') + } + const selectDataVal = new CreatePropertyRequest.SelectData() + selectDataVal.setAllowFreetext(property.selectData.isAllowingFreetext) + selectDataVal.setOptionsList(property.selectData.options.map(option => { + const optionVal = new CreatePropertyRequest.SelectData.SelectOption() + optionVal.setName(option.name) + if (option.description) { + optionVal.setDescription(option.description) + } + return optionVal + })) + req.setSelectData(selectDataVal) + } + + const result = await APIServices.property.createProperty(req, getAuthenticatedGrpcMetadata()) + + const id = result.getPropertyId() + + return { + ...property, + id + } + }, + update: async ({ property, selectUpdate }: PropertyUpdateType): Promise => { + const req = new UpdatePropertyRequest() + req.setId(property.id) + req.setName(property.name) + req.setIsArchived(property.isArchived) + req.setSubjectType(GRPCConverter.subjectTypeMapperToGRPC(property.subjectType)) + if (property.description) { + req.setDescription(property.description) + } + if (property.setId) { + req.setSetId(property.setId) + } + if (property.fieldType === 'singleSelect' || property.fieldType === 'multiSelect') { + if (!property.selectData) { + throw Error('Select FieldType, but select data not set') + } + const selectDataVal = new UpdatePropertyRequest.SelectData() + selectDataVal.setAllowFreetext(property.selectData.isAllowingFreetext) + if (selectUpdate) { + const createList = selectUpdate.add.map(option => { + const optionVal = new UpdatePropertyRequest.SelectData.SelectOption() + optionVal.setId('') + optionVal.setName(option.name) + if (option.description) { + optionVal.setDescription(option.description) + } + optionVal.setIsCustom(option.isCustom) + return optionVal + }) + const updateList = selectUpdate.update.map(option => { + const optionVal = new UpdatePropertyRequest.SelectData.SelectOption() + optionVal.setId(option.id) + optionVal.setName(option.name) + if (option.description) { + optionVal.setDescription(option.description) + } + optionVal.setIsCustom(option.isCustom) + return optionVal + }) + selectDataVal.setUpsertOptionsList([...updateList, ...createList]) + selectDataVal.setRemoveOptionsList(selectUpdate.remove) + } + req.setSelectData(selectDataVal) + } + + const result = await APIServices.property.updateProperty(req, getAuthenticatedGrpcMetadata()) + return !!result + } +} diff --git a/api-services/service/properties/PropertyViewSourceService.ts b/api-services/service/properties/PropertyViewSourceService.ts new file mode 100644 index 000000000..5de55a872 --- /dev/null +++ b/api-services/service/properties/PropertyViewSourceService.ts @@ -0,0 +1,48 @@ +import { APIServices } from '../../services' +import { getAuthenticatedGrpcMetadata } from '../../authentication/grpc_metadata' +import { + FilterUpdate, + UpdatePropertyViewRuleRequest +} from '@helpwave/proto-ts/services/property_svc/v1/property_views_svc_pb' +import { + PatientPropertyMatcher, + TaskPropertyMatcher +} from '@helpwave/proto-ts/services/property_svc/v1/property_value_svc_pb' +import type { PropertySubjectType } from '../../types/properties/property' + +export type PropertyViewRuleFilterUpdate = { + subjectId: string, + appendToAlwaysInclude?: string[], + removeFromAlwaysInclude?: string[], + appendToDontAlwaysInclude?: string[], + removeFromDontAlwaysInclude?: string[], +} + +export const PropertyViewSourceService = { + update: async (update: PropertyViewRuleFilterUpdate, subjectType: PropertySubjectType, wardId?: string): Promise => { + const req = new UpdatePropertyViewRuleRequest() + if (subjectType === 'patient') { + const matcher = new PatientPropertyMatcher().setPatientId(update.subjectId) + if (wardId) { + matcher.setWardId(wardId) + } + req.setPatientMatcher(matcher) + } + if (subjectType === 'task') { + const matcher = new TaskPropertyMatcher().setTaskId(update.subjectId) + if (wardId) { + matcher.setWardId(wardId) + } + req.setTaskMatcher(matcher) + } + + req.setFilterUpdate(new FilterUpdate() + .setAppendToAlwaysIncludeList(update.appendToAlwaysInclude ?? []) + .setRemoveFromAlwaysIncludeList(update.removeFromAlwaysInclude ?? []) + .setAppendToDontAlwaysIncludeList(update.removeFromAlwaysInclude ?? []) + .setRemoveFromDontAlwaysIncludeList(update.removeFromDontAlwaysInclude ?? [])) + + await APIServices.propertyViewSource.updatePropertyViewRule(req, getAuthenticatedGrpcMetadata()) + return true + }, +} diff --git a/api-services/service/tasks/BedService.ts b/api-services/service/tasks/BedService.ts new file mode 100644 index 000000000..b42546b95 --- /dev/null +++ b/api-services/service/tasks/BedService.ts @@ -0,0 +1,47 @@ +import type { BedWithRoomId } from '../../types/tasks/bed' +import { + CreateBedRequest, + DeleteBedRequest, + GetBedRequest, + UpdateBedRequest +} from '@helpwave/proto-ts/services/tasks_svc/v1/bed_svc_pb' +import { APIServices } from '../../services' +import { getAuthenticatedGrpcMetadata } from '../../authentication/grpc_metadata' + +export const BedService = { + get: async (id: string): Promise => { + const req = new GetBedRequest() + .setId(id) + const res = await APIServices.bed.getBed(req, getAuthenticatedGrpcMetadata()) + + return { + id: res.getId(), + name: res.getName(), + roomId: res.getRoomId() + } + }, + create: async (bed: BedWithRoomId): Promise => { + const req = new CreateBedRequest() + .setRoomId(bed.roomId) + .setName(bed.name) + const res = await APIServices.bed.createBed(req, getAuthenticatedGrpcMetadata()) + + return { ...bed, id: res.getId() } + }, + update: async (bed: BedWithRoomId): Promise => { + const req = new UpdateBedRequest() + .setId(bed.id) + .setName(bed.name) + .setRoomId(bed.roomId) + + await APIServices.bed.updateBed(req, getAuthenticatedGrpcMetadata()) + return true + }, + delete: async (id: string): Promise => { + const req = new DeleteBedRequest() + req.setId(id) + + await APIServices.bed.deleteBed(req, getAuthenticatedGrpcMetadata()) + return true + } +} diff --git a/api-services/service/tasks/PatientService.ts b/api-services/service/tasks/PatientService.ts new file mode 100644 index 000000000..b47c719cf --- /dev/null +++ b/api-services/service/tasks/PatientService.ts @@ -0,0 +1,213 @@ +import type { + PatientDetailsDTO, PatientDTO, + PatientListDTO, + PatientMinimalDTO, + PatientWithBedIdDTO, + RecentPatientDTO +} from '../../types/tasks/patient' +import { GRPCConverter } from '../../util/util' +import { APIServices } from '../../services' +import { getAuthenticatedGrpcMetadata } from '../../authentication/grpc_metadata' +import { + AssignBedRequest, + CreatePatientRequest, DeletePatientRequest, DischargePatientRequest, + GetPatientAssignmentByWardRequest, + GetPatientDetailsRequest, GetPatientListRequest, + GetPatientsByWardRequest, GetRecentPatientsRequest, ReadmitPatientRequest, UnassignBedRequest, UpdatePatientRequest +} from '@helpwave/proto-ts/services/tasks_svc/v1/patient_svc_pb' +import type { RoomWithMinimalBedAndPatient } from '../../types/tasks/room' +import type { BedWithPatientId } from '../../types/tasks/bed' + +export const PatientService = { + getDetails: async function (patientId: string): Promise { + const req = new GetPatientDetailsRequest() + req.setId(patientId) + + const res = await APIServices.patient.getPatientDetails(req, getAuthenticatedGrpcMetadata()) + + return { + id: res.getId(), + note: res.getNotes(), + name: res.getHumanReadableIdentifier(), + discharged: res.getIsDischarged(), + tasks: res.getTasksList().map(task => ({ + id: task.getId(), + name: task.getName(), + status: GRPCConverter.taskStatusFromGRPC(task.getStatus()), + notes: task.getDescription(), + isPublicVisible: task.getPublic(), + assignee: task.getAssignedUserId(), + dueDate: new Date(), // TODO replace later + subtasks: task.getSubtasksList().map(subtask => ({ + id: subtask.getId(), + name: subtask.getName(), + isDone: subtask.getDone(), + taskId: task.getId(), + })), + patientId: res.getId(), + })) + } + }, + getPatientsByWard: async function (wardId: string): Promise { + const req = new GetPatientsByWardRequest() + req.setWardId(wardId) + const res = await APIServices.patient.getPatientsByWard(req, getAuthenticatedGrpcMetadata()) + + return res.getPatientsList().map((patient) => ({ + id: patient.getId(), + name: patient.getHumanReadableIdentifier(), + note: patient.getNotes(), + bedId: patient.getBedId(), + })) + }, + getPatientAssignmentByWard: async function (wardId: string): Promise { + const req = new GetPatientAssignmentByWardRequest() + req.setWardId(wardId) + const res = await APIServices.patient.getPatientAssignmentByWard(req, getAuthenticatedGrpcMetadata()) + + return res.getRoomsList().map((room) => ({ + id: room.getId(), + name: room.getName(), + beds: room.getBedsList().map(bed => { + let patient: PatientMinimalDTO | undefined + const objectPatient = bed.getPatient() + if (objectPatient) { + patient = { + id: objectPatient.getId(), + name: objectPatient.getName() + } + } + return { + id: bed.getId(), + name: bed.getName(), + patient + } + }) + })) + }, + getPatientList: async function (wardId?: string): Promise { + const req = new GetPatientListRequest() + if (wardId) { + req.setWardId(wardId) + } + const res = await APIServices.patient.getPatientList(req, getAuthenticatedGrpcMetadata()) + + return { + active: res.getActiveList().map(value => { + const room = value.getRoom() + const bed = value.getBed() + + if (!room) { + console.error('no room for active patient in PatientList') + } + + if (!bed) { + console.error('no room for active patient in PatientList') + } + return ({ + id: value.getId(), + name: value.getHumanReadableIdentifier(), + bed: { + id: bed?.getId() ?? '', + name: bed?.getName() ?? '' + }, + room: { + id: room?.getId() ?? '', + name: room?.getName() ?? '', + wardId: room?.getWardId() ?? '' + } + }) + }), + discharged: res.getDischargedPatientsList().map(value => ({ + id: value.getId(), + name: value.getHumanReadableIdentifier(), + })), + unassigned: res.getUnassignedPatientsList().map(value => ({ + id: value.getId(), + name: value.getHumanReadableIdentifier(), + })) + } + }, + getRecentPatients: async function (): Promise { + const req = new GetRecentPatientsRequest() + const res = await APIServices.patient.getRecentPatients(req, getAuthenticatedGrpcMetadata()) + + const patients: RecentPatientDTO[] = [] + for (const patient of res.getRecentPatientsList()) { + const room = patient.getRoom() + const bed = patient.getBed() + let wardId: string | undefined + if (room) { + wardId = room?.getWardId() + } + + patients.push({ + id: patient.getId(), + name: patient.getHumanReadableIdentifier(), + wardId, + bed: bed ? { id: bed.getId(), name: bed.getName() } : undefined, + room: room ? { id: room.getId(), name: room.getId() } : undefined + }) + } + return patients + }, + create: async function (patient: PatientDTO): Promise { + const req = new CreatePatientRequest() + req.setNotes(patient.note) + req.setHumanReadableIdentifier(patient.name) + const res = await APIServices.patient.createPatient(req, getAuthenticatedGrpcMetadata()) + + const id = res.getId() + + if (!id) { + throw new Error('create room failed') + } + + return { ...patient, id } + }, + update: async function (patient: PatientDTO): Promise { + const req = new UpdatePatientRequest() + req.setId(patient.id) + req.setNotes(patient.note) + req.setHumanReadableIdentifier(patient.name) + + await APIServices.patient.updatePatient(req, getAuthenticatedGrpcMetadata()) + return true + }, + delete: async function (patientId: string): Promise { + const req = new DeletePatientRequest() + req.setId(patientId) + + await APIServices.patient.deletePatient(req, getAuthenticatedGrpcMetadata()) + return true + }, + assignToBed: async function (bedWithPatientId: BedWithPatientId): Promise { + const req = new AssignBedRequest() + req.setId(bedWithPatientId.patientId) + req.setBedId(bedWithPatientId.id) + + await APIServices.patient.assignBed(req, getAuthenticatedGrpcMetadata()) + return true + }, + unassignFromBed: async function (patientId: string): Promise { + const req = new UnassignBedRequest() + req.setId(patientId) + + await APIServices.patient.unassignBed(req, getAuthenticatedGrpcMetadata()) + return true + }, + discharge: async function (patientId: string): Promise { + const req = new DischargePatientRequest() + req.setId(patientId) + + await APIServices.patient.dischargePatient(req, getAuthenticatedGrpcMetadata()) + return true + }, + reAdmit: async function (patientId: string): Promise { + const req = new ReadmitPatientRequest() + req.setPatientId(patientId) + + await APIServices.patient.readmitPatient(req, getAuthenticatedGrpcMetadata()) + return true + } +} diff --git a/api-services/service/tasks/TaskTemplateService.ts b/api-services/service/tasks/TaskTemplateService.ts new file mode 100644 index 000000000..7e4ec352a --- /dev/null +++ b/api-services/service/tasks/TaskTemplateService.ts @@ -0,0 +1,25 @@ +import type { TaskTemplateDTO } from '../../types/tasks/tasks_templates' +import { GetAllTaskTemplatesRequest } from '@helpwave/proto-ts/services/tasks_svc/v1/task_template_svc_pb' +import { APIServices } from '../../services' +import { getAuthenticatedGrpcMetadata } from '../../authentication/grpc_metadata' + +export const TaskTemplateService = { + getWard: async (wardId: string): Promise => { + const req = new GetAllTaskTemplatesRequest() + .setWardId(wardId) + const res = await APIServices.taskTemplates.getAllTaskTemplates(req, getAuthenticatedGrpcMetadata()) + return res.getTemplatesList().map((template) => ({ + id: template.getId(), + wardId, + name: template.getName(), + notes: template.getDescription(), + subtasks: template.getSubtasksList().map((subtask) => ({ + id: subtask.getId(), + name: subtask.getName(), + isDone: false, + taskId: template.getId(), + })), + isPublicVisible: template.getIsPublic() + })) + } +} diff --git a/api-services/service/users/PatientService.ts b/api-services/service/users/PatientService.ts deleted file mode 100644 index 62baafba7..000000000 --- a/api-services/service/users/PatientService.ts +++ /dev/null @@ -1,227 +0,0 @@ -import type { - PatientDetailsDTO, PatientDTO, - PatientListDTO, - PatientMinimalDTO, - PatientWithBedIdDTO, - RecentPatientDTO -} from '../../types/tasks/patient' -import { GRPCConverter } from '../../util/util' -import { APIServices } from '../../services' -import { getAuthenticatedGrpcMetadata } from '../../authentication/grpc_metadata' -import { - AssignBedRequest, - CreatePatientRequest, DeletePatientRequest, DischargePatientRequest, - GetPatientAssignmentByWardRequest, - GetPatientDetailsRequest, GetPatientListRequest, - GetPatientsByWardRequest, GetRecentPatientsRequest, ReadmitPatientRequest, UnassignBedRequest, UpdatePatientRequest -} from '@helpwave/proto-ts/services/tasks_svc/v1/patient_svc_pb' -import type { RoomWithMinimalBedAndPatient } from '../../types/tasks/room' -import type { BedWithPatientId } from '../../types/tasks/bed' - -export const PatientService = { - getPatientDetails: async function(patientId: string): Promise { - const req = new GetPatientDetailsRequest() - req.setId(patientId) - - const res = await APIServices.patient.getPatientDetails(req, getAuthenticatedGrpcMetadata()) - - return { - id: res.getId(), - note: res.getNotes(), - name: res.getHumanReadableIdentifier(), - discharged: res.getIsDischarged(), - tasks: res.getTasksList().map(task => ({ - id: task.getId(), - name: task.getName(), - status: GRPCConverter.taskStatusFromGRPC(task.getStatus()), - notes: task.getDescription(), - isPublicVisible: task.getPublic(), - assignee: task.getAssignedUserId(), - dueDate: new Date(), // TODO replace later - subtasks: task.getSubtasksList().map(subtask => ({ - id: subtask.getId(), - name: subtask.getName(), - isDone: subtask.getDone(), - taskId: task.getId(), - })), - patientId: res.getId(), - })) - } - }, - getPatientsByWard: async function(wardId: string): Promise { - const req = new GetPatientsByWardRequest() - req.setWardId(wardId) - const res = await APIServices.patient.getPatientsByWard(req, getAuthenticatedGrpcMetadata()) - - return res.getPatientsList().map((patient) => ({ - id: patient.getId(), - name: patient.getHumanReadableIdentifier(), - note: patient.getNotes(), - bedId: patient.getBedId(), - })) - }, - getPatientAssignmentByWard: async function(wardId: string): Promise { - const req = new GetPatientAssignmentByWardRequest() - req.setWardId(wardId) - const res = await APIServices.patient.getPatientAssignmentByWard(req, getAuthenticatedGrpcMetadata()) - - return res.getRoomsList().map((room) => ({ - id: room.getId(), - name: room.getName(), - beds: room.getBedsList().map(bed => { - let patient: PatientMinimalDTO | undefined - const objectPatient = bed.getPatient() - if (objectPatient) { - patient = { - id: objectPatient.getId(), - name: objectPatient.getName() - } - } - return { - id: bed.getId(), - name: bed.getName(), - patient - } - }) - })) - }, - getPatientList: async function(wardId?: string): Promise { - const req = new GetPatientListRequest() - if (wardId) { - req.setWardId(wardId) - } - const res = await APIServices.patient.getPatientList(req, getAuthenticatedGrpcMetadata()) - - return { - active: res.getActiveList().map(value => { - const room = value.getRoom() - const bed = value.getBed() - - if (!room) { - console.error('no room for active patient in PatientList') - } - - if (!bed) { - console.error('no room for active patient in PatientList') - } - return ({ - id: value.getId(), - name: value.getHumanReadableIdentifier(), - bed: { - id: bed?.getId() ?? '', - name: bed?.getName() ?? '' - }, - room: { - id: room?.getId() ?? '', - name: room?.getName() ?? '', - wardId: room?.getWardId() ?? '' - } - }) - }), - discharged: res.getDischargedPatientsList().map(value => ({ - id: value.getId(), - name: value.getHumanReadableIdentifier(), - })), - unassigned: res.getUnassignedPatientsList().map(value => ({ - id: value.getId(), - name: value.getHumanReadableIdentifier(), - })) - } - }, - getRecentPatients: async function(): Promise { - const req = new GetRecentPatientsRequest() - const res = await APIServices.patient.getRecentPatients(req, getAuthenticatedGrpcMetadata()) - - const patients: RecentPatientDTO[] = [] - for (const patient of res.getRecentPatientsList()) { - const room = patient.getRoom() - const bed = patient.getBed() - let wardId: string | undefined - if (room) { - wardId = room?.getWardId() - } - - patients.push({ - id: patient.getId(), - name: patient.getHumanReadableIdentifier(), - wardId, - bed: bed ? { id: bed.getId(), name: bed.getName() } : undefined, - room: room ? { id: room.getId(), name: room.getId() } : undefined - }) - } - return patients - }, - create: async function(patient: PatientDTO): Promise { - const req = new CreatePatientRequest() - req.setNotes(patient.note) - req.setHumanReadableIdentifier(patient.name) - const res = await APIServices.patient.createPatient(req, getAuthenticatedGrpcMetadata()) - - const id = res.getId() - - if (!id) { - throw new Error('create room failed') - } - - return { ...patient, id } - }, - update: async function(patient: PatientDTO): Promise { - const req = new UpdatePatientRequest() - req.setId(patient.id) - req.setNotes(patient.note) - req.setHumanReadableIdentifier(patient.name) - - const res = await APIServices.patient.updatePatient(req, getAuthenticatedGrpcMetadata()) - - if (!res.toObject()) { - throw new Error('error in PatientUpdate') - } - - return patient - }, - delete: async function(patientId: string): Promise { - const req = new DeletePatientRequest() - req.setId(patientId) - - const res = await APIServices.patient.deletePatient(req, getAuthenticatedGrpcMetadata()) - - return !!res.toObject() - }, - assignToBed: async function(bedWithPatientId: BedWithPatientId): Promise { - const req = new AssignBedRequest() - req.setId(bedWithPatientId.patientId) - req.setBedId(bedWithPatientId.id) - - const res = await APIServices.patient.assignBed(req, getAuthenticatedGrpcMetadata()) - - if (!res.toObject()) { - throw new Error('assign bed request failed') - } - - return bedWithPatientId - }, - unassignFromBed: async function(patientId: string): Promise { - const req = new UnassignBedRequest() - req.setId(patientId) - - const res = await APIServices.patient.unassignBed(req, getAuthenticatedGrpcMetadata()) - - return !!res.toObject() - }, - discharge: async function(patientId: string): Promise { - const req = new DischargePatientRequest() - req.setId(patientId) - - const res = await APIServices.patient.dischargePatient(req, getAuthenticatedGrpcMetadata()) - - return !!res.toObject() - }, - reAdmit: async function(patientId: string): Promise { - const req = new ReadmitPatientRequest() - req.setPatientId(patientId) - - const res = await APIServices.patient.readmitPatient(req, getAuthenticatedGrpcMetadata()) - - return !!res.toObject() - } -} diff --git a/api-services/types/properties/attached_property.ts b/api-services/types/properties/attached_property.ts index 23996c1f8..c3e7a840c 100644 --- a/api-services/types/properties/attached_property.ts +++ b/api-services/types/properties/attached_property.ts @@ -1,4 +1,4 @@ -import type { FieldType, SelectOption, SubjectType } from './property' +import type { FieldType, SelectOption, PropertySubjectType } from './property' export type AttachPropertySelectValue = Omit @@ -39,7 +39,7 @@ export type AttachedProperty = { } export type DisplayableAttachedProperty = AttachedProperty & { - subjectType: SubjectType, + subjectType: PropertySubjectType, fieldType: FieldType, name: string, description?: string, diff --git a/api-services/types/properties/property.ts b/api-services/types/properties/property.ts index 1d1b2427a..a8bb3a947 100644 --- a/api-services/types/properties/property.ts +++ b/api-services/types/properties/property.ts @@ -1,5 +1,5 @@ export const subjectTypeList = ['patient', 'task'] as const -export type SubjectType = typeof subjectTypeList[number] +export type PropertySubjectType = typeof subjectTypeList[number] export const fieldTypeList = ['multiSelect', 'singleSelect', 'number', 'text', 'date', 'dateTime', 'checkbox'] as const export type FieldType = typeof fieldTypeList[number] @@ -11,20 +11,20 @@ export type SelectOption = { isCustom: boolean, } -export type SelectData = { +export type PropertySelectData = { isAllowingFreetext: boolean, options: SelectOption[], } export type Property = { id: string, - subjectType: SubjectType, + subjectType: PropertySubjectType, fieldType: FieldType, name: string, description?: string, isArchived: boolean, setId?: string, - selectData?: SelectData, + selectData?: PropertySelectData, alwaysIncludeForViewSource?: boolean, } @@ -35,7 +35,7 @@ export const emptySelectOption: SelectOption = { isCustom: false, } -export const emptySelectData: SelectData = { +export const emptySelectData: PropertySelectData = { isAllowingFreetext: true, options: [ { diff --git a/api-services/util/util.ts b/api-services/util/util.ts index 4a5af4207..8126a92b4 100644 --- a/api-services/util/util.ts +++ b/api-services/util/util.ts @@ -5,7 +5,7 @@ import { SubjectType as GRPCSubjectType } from '@helpwave/proto-ts/services/property_svc/v1/types_pb' import type { TaskStatus } from '../types/tasks/task' -import type { FieldType, SubjectType } from '../types/properties/property' +import type { FieldType, PropertySubjectType } from '../types/properties/property' export const GRPCConverter = { taskStatusFromGRPC: (status: ProtoTaskStatus): TaskStatus => { @@ -46,7 +46,7 @@ export const GRPCConverter = { return timestamp }, - subjectTypeMapperToGRPC: (subjectType: SubjectType): GRPCSubjectType => { + subjectTypeMapperToGRPC: (subjectType: PropertySubjectType): GRPCSubjectType => { switch (subjectType) { case 'patient': return GRPCSubjectType.SUBJECT_TYPE_PATIENT @@ -54,7 +54,7 @@ export const GRPCConverter = { return GRPCSubjectType.SUBJECT_TYPE_TASK } }, - subjectTypeMapperFromGRPC: (subjectType: GRPCSubjectType): SubjectType => { + subjectTypeMapperFromGRPC: (subjectType: GRPCSubjectType): PropertySubjectType => { switch (subjectType) { case GRPCSubjectType.SUBJECT_TYPE_PATIENT: return 'patient' diff --git a/customer/pages/settings/index.tsx b/customer/pages/settings/index.tsx index a08a66b7f..37a5ff3d8 100644 --- a/customer/pages/settings/index.tsx +++ b/customer/pages/settings/index.tsx @@ -78,7 +78,7 @@ const Settings: NextPage> = ({ overwrit
{translation.settingsDescription} - + {!!data && ( > return (
- + {!!data && (
{translation.teamDescription} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index e07fb0e9e..994c89a07 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -176,8 +176,8 @@ importers: specifier: workspace:* version: link:../api-services '@helpwave/hightide': - specifier: ^0.1.21 - version: 0.1.21(@types/react-dom@18.3.5(@types/react@18.3.17))(@types/react@18.3.17) + specifier: ^0.1.24 + version: 0.1.24(@types/react-dom@18.3.5(@types/react@18.3.17))(@types/react@18.3.17) '@tailwindcss/postcss': specifier: ^4.1.3 version: 4.1.5 @@ -336,8 +336,8 @@ packages: '@helpwave/hightide@0.0.17': resolution: {integrity: sha512-FTktcuyJ9/Vh3odP6r+LLFWHdqV+dHSxzJTOQrY9FZ4ikHSbhmAOfrszi4CZG69/K5KjGkv9WMuvmn+cK+YkQw==} - '@helpwave/hightide@0.1.21': - resolution: {integrity: sha512-09XS2+Eidk2j+xRwezhgowdIbn8Ujk1RSDxuuKF2afK6AYA54EgaScdTg+qnmFnAUtrO/eJuDbJCROc4rQvGlA==} + '@helpwave/hightide@0.1.24': + resolution: {integrity: sha512-DPd8k9vFe+4gV8je50Vikt7lHl35fU6bTgcNxMu0VXNFLMnbHLdhAQ27uytC3eiCZ1x6haZBZBxWd7+0AhMKeQ==} '@helpwave/proto-ts@0.64.0-89e2023.0': resolution: {integrity: sha512-EqHsZDOttnCBqcAZ0WiRO32rGMICwvGUxvhSltU0KahFsbIx8o5DZ3l9N8GdB3c/66qe5tabTvVKbUzxf66Yjw==} @@ -2685,7 +2685,7 @@ snapshots: - babel-plugin-react-compiler - sass - '@helpwave/hightide@0.1.21(@types/react-dom@18.3.5(@types/react@18.3.17))(@types/react@18.3.17)': + '@helpwave/hightide@0.1.24(@types/react-dom@18.3.5(@types/react@18.3.17))(@types/react@18.3.17)': dependencies: '@radix-ui/react-checkbox': 1.1.3(@types/react-dom@18.3.5(@types/react@18.3.17))(@types/react@18.3.17)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@tailwindcss/cli': 4.1.10 diff --git a/tasks/components/KanbanHeader.tsx b/tasks/components/KanbanHeader.tsx index 47e200e14..9db1bcb6c 100644 --- a/tasks/components/KanbanHeader.tsx +++ b/tasks/components/KanbanHeader.tsx @@ -43,7 +43,9 @@ export const KanbanHeader = ({ const translation = useTranslation([defaultKanbanHeaderTranslations], overwriteTranslation) return (
- +
{orgMember.name} {orgMember.email} @@ -210,7 +210,6 @@ export const OrganizationMemberList = ({ hasError={(isError || !data) && !members} isLoading={!members && isLoading} className="min-h-131" - minimumLoadingDuration={200} > {taskTemplates && (
diff --git a/tasks/components/UserInvitationList.tsx b/tasks/components/UserInvitationList.tsx index 43f11d733..9f4e7b5f8 100644 --- a/tasks/components/UserInvitationList.tsx +++ b/tasks/components/UserInvitationList.tsx @@ -71,7 +71,7 @@ export const UserInvitationList = ({ const invite = data[cell.row.index]! return (
- + {invite.organization.longName}
) @@ -127,7 +127,6 @@ export const UserInvitationList = ({ isLoading={isLoading || !data} hasError={isError} className="min-h-157" - minimumLoadingDuration={200} > {data && ( {/* TODO set this color in the css config */}
{user.name}
- + )}> {({ close }) => { - const withClose = (func: () => void) => - { + const withClose = (func: () => void) => { return () => { func() close() diff --git a/tasks/components/cards/OrganizationCard.tsx b/tasks/components/cards/OrganizationCard.tsx index aeab8a6d3..3bfff7a15 100644 --- a/tasks/components/cards/OrganizationCard.tsx +++ b/tasks/components/cards/OrganizationCard.tsx @@ -1,4 +1,3 @@ - import { Mail } from 'lucide-react' import type { Translation } from '@helpwave/hightide' import { useTranslation, type PropsForTranslation } from '@helpwave/hightide' @@ -31,10 +30,10 @@ export type OrganizationCardProps = EditCardProps & { * A Card displaying a Organization */ export const OrganizationCard = ({ - overwriteTranslation, - organization, - ...editCardProps -}: PropsForTranslation) => { + overwriteTranslation, + organization, + ...editCardProps + }: PropsForTranslation) => { const translation = useTranslation([defaultOrganizationCardTranslation], overwriteTranslation) const organizationMemberCount = organization.members.length @@ -55,7 +54,13 @@ export const OrganizationCard = ({
{`${organizationMemberCount} ${organizationMemberCount > 1 ? translation('members') : translation('member')}`}
- ({ avatarUrl: user.avatarURL, alt: user.name }))}/> + ({ + image: { avatarUrl: user.avatarURL, alt: user.name }, + name: user.name, + }))} + fullyRounded={true} + /> diff --git a/tasks/components/cards/TaskCard.tsx b/tasks/components/cards/TaskCard.tsx index ad5c641e6..4f4e92e79 100644 --- a/tasks/components/cards/TaskCard.tsx +++ b/tasks/components/cards/TaskCard.tsx @@ -86,8 +86,8 @@ export const TaskCard = ({ )}
- {(assignee && assignee.avatarUrl) ? - (): + {assignee ? + () : () } 0 ? progress : 1}/> diff --git a/tasks/components/cards/UserCard.tsx b/tasks/components/cards/UserCard.tsx index 1368ccde8..ca863ccbb 100644 --- a/tasks/components/cards/UserCard.tsx +++ b/tasks/components/cards/UserCard.tsx @@ -1,4 +1,3 @@ - import { Avatar } from '@helpwave/hightide' import type { User } from '@helpwave/api-services/authentication/useAuth' @@ -13,7 +12,7 @@ const UserCard = ({ user }: UserCardProps) => { return (
- +
{user.nickname}
diff --git a/tasks/components/layout/DashboardDisplay.tsx b/tasks/components/layout/DashboardDisplay.tsx index 295752ef9..e668601f0 100644 --- a/tasks/components/layout/DashboardDisplay.tsx +++ b/tasks/components/layout/DashboardDisplay.tsx @@ -95,7 +95,7 @@ export const DashboardDisplay = ({
- +
@@ -123,7 +123,7 @@ export const DashboardDisplay = ({
- +
@@ -145,7 +145,7 @@ export const DashboardDisplay = ({
- + {patients && patients.length > 0 && (
diff --git a/tasks/components/layout/PatientDetails.tsx b/tasks/components/layout/PatientDetails.tsx index b06a2413d..09de1960b 100644 --- a/tasks/components/layout/PatientDetails.tsx +++ b/tasks/components/layout/PatientDetails.tsx @@ -149,10 +149,12 @@ export const PatientDetail = ({ setTaskId(undefined)} - wardId={wardId} + createInformation={{ + wardId, + patientId: newPatient.id, + initialStatus: initialTaskStatus ?? 'todo', + }} taskId={taskId} - patientId={newPatient.id} - initialStatus={initialTaskStatus} />
({ ...emptyProperty, }) - const propertyCreateMutation = usePropertyCreateMutation(property => { - updateContext({ ...contextState, propertyId: property.id }) + const propertyCreateMutation = usePropertyCreateMutation({ + onSuccess: property => { + updateContext({ ...contextState, propertyId: property.id }) + } }) useEffect(() => { @@ -107,7 +109,6 @@ export const PropertyDetails = ({ isLoading={!isCreatingNewProperty && isLoading} hasError={!isCreatingNewProperty && isError} className="min-h-128" - minimumLoadingDuration={200} > { - let selectData: SelectData | undefined = fieldDetails.selectData ? { ...fieldDetails.selectData } : undefined + let selectData: PropertySelectData | undefined = fieldDetails.selectData ? { ...fieldDetails.selectData } : undefined if (isSelect) { selectData ??= emptySelectData if (selectUpdate) { diff --git a/tasks/components/layout/property/PropertyDetailsField.tsx b/tasks/components/layout/property/PropertyDetailsField.tsx index b8545330a..61dd1f054 100644 --- a/tasks/components/layout/property/PropertyDetailsField.tsx +++ b/tasks/components/layout/property/PropertyDetailsField.tsx @@ -13,7 +13,7 @@ import { useTranslation } from '@helpwave/hightide' import { Plus, X } from 'lucide-react' -import type { FieldType, Property, SelectData, SelectOption } from '@helpwave/api-services/types/properties/property' +import type { FieldType, Property, PropertySelectData, SelectOption } from '@helpwave/api-services/types/properties/property' import { fieldTypeList } from '@helpwave/api-services/types/properties/property' import { useEffect, useMemo, useState } from 'react' import type { ColumnDef, RowSelectionState } from '@tanstack/react-table' @@ -44,12 +44,12 @@ const defaultPropertySelectOptionsUpdaterPropsTranslation: Translation void, + value: PropertySelectData, + onChange: (data: PropertySelectData, update: SelectDataUpdate) => void, } type PropertySelectOptionsUpdaterState = { - data: SelectData, + data: PropertySelectData, update: SelectDataUpdate, } @@ -281,8 +281,8 @@ export const PropertyDetailsField = ({ )} {isSelectType && ( ) => { + overwriteTranslation, + value, + onChange, + onEditComplete, + expandableOptions + }: PropsForTranslation) => { const translation = useTranslation([defaultPropertyDetailsRulesTranslation], overwriteTranslation) return ( = { en: { @@ -105,8 +105,8 @@ export const PropertyDisplay = ({ const value = propertyList[cell.row.index]! return ( ) }, @@ -187,7 +187,6 @@ export const PropertyDisplay = ({ isLoading={isLoading} hasError={isError} className="min-h-190" - minimumLoadingDuration={200} >
{ + if (!success) { + return + } + PropertyService.get(property.id).then((property) => { + const update = updater(attachedValue => { + const option = property.selectData?.options.find(value => value.name === variables.selectUpdate?.add[0]?.name) + if (!option) { + return + } + if (property.fieldType === 'singleSelect') { + attachedValue.value.singleSelectValue = option + } else if (property.fieldType === 'multiSelect') { + attachedValue.value.multiSelectValue = [...attachedValue.value.multiSelectValue, option] + } + }) + onChange(update) + onEditComplete(update) + }) + } + }) + switch (property.fieldType) { case 'text': return ( @@ -141,6 +168,22 @@ export const PropertyEntryDisplay = ({ label: option.name, searchTags: [option.name] }))} + onAddNew={(name) => { + updatePropertyMutation.mutate({ + property, + selectUpdate: { + add: [ + { + id: '', + name, + isCustom: true, + } + ], + update: [], + remove: [] + }, + }) + }} selectedDisplayOverwrite={attachedProperty.value.singleSelectValue?.name} alignmentVertical="topOutside" /> @@ -170,6 +213,22 @@ export const PropertyEntryDisplay = ({ selected: !!attachedProperty.value.multiSelectValue.find(value => value.id === option.id), searchTags: [option.name] }))} + onAddNew={(name) => { + updatePropertyMutation.mutate({ + property, + selectUpdate: { + add: [ + { + id: '', + name, + isCustom: true, + } + ], + update: [], + remove: [] + }, + }) + }} useChipDisplay={true} alignmentVertical="topOutside" /> @@ -201,7 +260,6 @@ export const PropertyEntry = ({ diff --git a/tasks/components/layout/property/PropertyList.tsx b/tasks/components/layout/property/PropertyList.tsx index 421f4b823..18a24272a 100644 --- a/tasks/components/layout/property/PropertyList.tsx +++ b/tasks/components/layout/property/PropertyList.tsx @@ -1,13 +1,12 @@ import { LoadingAnimation } from '@helpwave/hightide' -import type { PropsForTranslation , Translation } from '@helpwave/hightide' +import type { PropsForTranslation, Translation } from '@helpwave/hightide' import { useTranslation } from '@helpwave/hightide' -import { Tile } from '@helpwave/hightide' import { Plus, Tag } from 'lucide-react' import { LoadingAndErrorComponent } from '@helpwave/hightide' import { Menu, MenuItem } from '@helpwave/hightide' import { useEffect, useState } from 'react' import { SearchableList } from '@helpwave/hightide' -import type { SubjectType } from '@helpwave/api-services/types/properties/property' +import type { PropertySubjectType } from '@helpwave/api-services/types/properties/property' import { useAttachPropertyMutation, usePropertyWithValueListQuery @@ -22,6 +21,7 @@ import { useUpdatePropertyViewRuleRequest } from '@helpwave/api-services/mutations/properties/property_view_src_mutations' import { PropertyEntry } from '@/components/layout/property/PropertyEntry' +import { ColumnTitle } from '@/components/ColumnTitle' type PropertyListTranslation = { properties: string, @@ -41,17 +41,17 @@ const defaultPropertyListTranslation: Translation = { export type PropertyListProps = { subjectId: string, - subjectType: SubjectType, + subjectType: PropertySubjectType, } /** * A component for listing properties for a subject */ export const PropertyList = ({ - overwriteTranslation, - subjectId, - subjectType -}: PropsForTranslation) => { + overwriteTranslation, + subjectId, + subjectType + }: PropsForTranslation) => { const translation = useTranslation([defaultPropertyListTranslation], overwriteTranslation) const { data: propertyList, @@ -77,20 +77,32 @@ export const PropertyList = ({ className="min-h-48" >
- } - className="!gap-x-2" + + + {translation('properties')} +
+ )} /> {properties && properties.map((property, index) => ( - setProperties(prevState => prevState - .map(value1 => value1.propertyId === value.propertyId && value1.subjectId === value.subjectId ? { ...value1, ...value } : value1))} - onEditComplete={value => addOrUpdatePropertyMutation.mutate({ previous: property, update: value, fieldType: property.fieldType })} - onRemove={value => addOrUpdatePropertyMutation.mutate({ previous: property, update: value, fieldType: property.fieldType })} - /> + setProperties(prevState => prevState + .map(value1 => value1.propertyId === value.propertyId && value1.subjectId === value.subjectId ? { ...value1, ...value } : value1))} + onEditComplete={value => addOrUpdatePropertyMutation.mutate({ + previous: property, + update: value, + fieldType: property.fieldType + })} + onRemove={value => addOrUpdatePropertyMutation.mutate({ + previous: property, + update: value, + fieldType: property.fieldType + })} + /> ))} trigger={({ toggleOpen }, ref) => ( @@ -122,8 +134,16 @@ export const PropertyList = ({ { - const attachedProperty : AttachedProperty = { propertyId: property.id, subjectId, value: emptyPropertyValue } - addOrUpdatePropertyMutation.mutate({ previous: attachedProperty, update: attachedProperty, fieldType: property.fieldType }) + const attachedProperty: AttachedProperty = { + propertyId: property.id, + subjectId, + value: emptyPropertyValue + } + addOrUpdatePropertyMutation.mutate({ + previous: attachedProperty, + update: attachedProperty, + fieldType: property.fieldType + }) updateViewRulesMutation.mutate({ subjectId, appendToAlwaysInclude: [property.id] }) close() }} diff --git a/tasks/components/layout/property/PropertySubjectTypeSelect.tsx b/tasks/components/layout/property/PropertySubjectTypeSelect.tsx index 3daab348f..039ed4855 100644 --- a/tasks/components/layout/property/PropertySubjectTypeSelect.tsx +++ b/tasks/components/layout/property/PropertySubjectTypeSelect.tsx @@ -3,10 +3,10 @@ import type { PropsForTranslation } from '@helpwave/hightide' import { useTranslation } from '@helpwave/hightide' import type { SelectProps } from '@helpwave/hightide' import { Select } from '@helpwave/hightide' -import type { SubjectType } from '@helpwave/api-services/types/properties/property' +import type { PropertySubjectType } from '@helpwave/api-services/types/properties/property' import { subjectTypeList } from '@helpwave/api-services/types/properties/property' -type PropertySubjectTypeSelectTranslation = { [key in SubjectType]: string } +type PropertySubjectTypeSelectTranslation = { [key in PropertySubjectType]: string } const defaultPropertySubjectTypeSelectTranslation: Translation = { en: { @@ -25,7 +25,7 @@ const defaultPropertySubjectTypeSelectTranslation: Translation, 'options'>>) => { +}: PropsForTranslation, 'options'>>) => { const translation = useTranslation([defaultPropertySubjectTypeSelectTranslation], overwriteTranslation) return (
{tasksDetails} {buttons} diff --git a/tasks/components/selects/AssigneeSelect.tsx b/tasks/components/selects/AssigneeSelect.tsx index 0d023b949..034a75d19 100644 --- a/tasks/components/selects/AssigneeSelect.tsx +++ b/tasks/components/selects/AssigneeSelect.tsx @@ -28,7 +28,6 @@ export const AssigneeSelect = ({ isLoading={isLoading} hasError={isError} className="min-h-10 w-full" - minimumLoadingDuration={200} > setTouched({ name: true })} - onChangeText={text => { - context.updateContext({ - template: { ...context.state.template, name: text }, - isValid: validateName(text) === undefined, - hasChanges: true, - deletedSubtaskIds: context.state.deletedSubtaskIds - }) - }} - maxLength={maxNameLength} - className={clsx(inputClasses, { [inputErrorClasses]: isDisplayingNameError })} - /> - {isDisplayingNameError && {nameErrorMessage}} - +
+ setTouched({ name: true })} + onChangeText={text => { + updateContext(prevState => ({ + ...prevState, + template: { ...prevState.template, name: text }, + isValid: validateName(text) === undefined, + hasChanges: true, + deletedSubtaskIds: state.deletedSubtaskIds + })) + }} + maxLength={maxNameLength} + className={clsx(inputClasses, { [inputErrorClasses]: isDisplayingNameError })} + /> + {isDisplayingNameError && {nameErrorMessage}}