diff --git a/api-services/authentication/useAuth.tsx b/api-services/authentication/useAuth.tsx index 61a53a481..7a45e8d6f 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 })) @@ -129,12 +129,11 @@ export const ProvideAuth = ({ children }: PropsWithChildren) => { console.debug('Creating a new organization') OrganizationService.create({ id: '', - email: 'test@helpwave.de', + contactEmail: 'test@helpwave.de', longName: 'Test Organization', shortName: 'Test-Org', avatarURL: 'https://helpwave.de/favicon.ico', isPersonal: false, - isVerified: false, }).then(organization => { setUser(() => ({ ...user, 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/query_keys.ts b/api-services/mutations/query_keys.ts index 323bc26f1..d69c67b4c 100644 --- a/api-services/mutations/query_keys.ts +++ b/api-services/mutations/query_keys.ts @@ -8,6 +8,7 @@ export const QueryKeys = { beds: 'beds', patients: 'patients', tasks: 'tasks', + taskTemplates: 'taskTemplates', // Users invitations: 'invitations', organizations: 'organizations', 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..81352b74b 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,58 @@ 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 usePatientDeleteMutation = (options?: UseMutationOptions) => { + const queryClient = useQueryClient() + return useMutation({ + ...options, + mutationFn: async (id) => { + return await PatientService.delete(id) + }, + 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 usePatientAssignToBedMutation = (options?: UseMutationOptions) => { const queryClient = useQueryClient() return useMutation({ ...options, @@ -100,7 +121,7 @@ export const useAssignBedMutation = (options?: UseMutationOptions) => { +export const usePatientUnassignMutation = (options?: UseMutationOptions) => { const queryClient = useQueryClient() return useMutation({ ...options, @@ -134,7 +155,7 @@ export const usePatientDischargeMutation = (options?: UseMutationOptions) => { +export const usePatientReadmitMutation = (options?: UseMutationOptions) => { const queryClient = useQueryClient() return useMutation({ ...options, @@ -150,3 +171,30 @@ export const useReadmitPatientMutation = (options?: UseMutationOptions) => { + const queryClient = useQueryClient() + return useMutation({ + ...options, + mutationFn: async ({ patientId, bedId }) => { + await PatientService.reAdmit(patientId) + return await PatientService.assignToBed({ patientId, bedId }) + }, + onMutate: (variables) => { + if(options?.onMutate){ + options.onMutate(variables) + } + }, + onSuccess: (data, variables, context) => { + if(options?.onSuccess){ + options.onSuccess(data, variables, context) + } + queryClient.invalidateQueries([QueryKeys.rooms, roomOverviewsQueryKey]).catch(console.error) + queryClient.invalidateQueries([QueryKeys.patients]).catch(console.error) + } + }) +} diff --git a/api-services/mutations/tasks/room_mutations.ts b/api-services/mutations/tasks/room_mutations.ts index 9d395b0a8..44391d70b 100644 --- a/api-services/mutations/tasks/room_mutations.ts +++ b/api-services/mutations/tasks/room_mutations.ts @@ -1,38 +1,15 @@ +import type { UseMutationOptions } from '@tanstack/react-query' import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query' -import { - CreateRoomRequest, - DeleteRoomRequest, - GetRoomOverviewsByWardRequest, - GetRoomRequest, - UpdateRoomRequest -} from '@helpwave/proto-ts/services/tasks_svc/v1/room_svc_pb' -import { noop } from '@helpwave/hightide' -import type { RoomDTO, RoomMinimalDTO, RoomOverviewDTO } from '../../types/tasks/room' -import { APIServices } from '../../services' -import { getAuthenticatedGrpcMetadata } from '../../authentication/grpc_metadata' +import type { RoomMinimalDTO } from '../../types/tasks/room' import { QueryKeys } from '../query_keys' +import { RoomService } from '../../service/tasks/RoomService' export const useRoomQuery = (roomId?: string) => { return useQuery({ queryKey: [QueryKeys.rooms, roomId], enabled: !!roomId, queryFn: async () => { - const req = new GetRoomRequest() - if (roomId) { - req.setId(roomId) - } - const res = await APIServices.room.getRoom(req, getAuthenticatedGrpcMetadata()) - - const room: RoomDTO = { - id: res.getId(), - name: res.getName(), - beds: res.getBedsList().map(bed => ({ - id: bed.getId(), - name: bed.getName() - })) - } - - return room + return await RoomService.get(roomId!) }, }) } @@ -43,98 +20,55 @@ export const useRoomOverviewsQuery = (wardId: string | undefined) => { queryKey: [QueryKeys.rooms, roomOverviewsQueryKey], enabled: !!wardId, queryFn: async () => { - const req = new GetRoomOverviewsByWardRequest() - if (wardId) { - req.setId(wardId) - } - const res = await APIServices.room.getRoomOverviewsByWard(req, getAuthenticatedGrpcMetadata()) - - const rooms: RoomOverviewDTO[] = res.getRoomsList().map((room) => ({ - id: room.getId(), - name: room.getName(), - beds: room.getBedsList().map(bed => { - const patient = bed.getPatient() - return { - id: bed.getId(), - name: bed.getName(), - patient: !patient ? undefined : { - id: patient.getId(), - name: patient.getHumanReadableIdentifier(), - tasksUnscheduled: patient.getTasksUnscheduled(), - tasksInProgress: patient.getTasksInProgress(), - tasksDone: patient.getTasksDone() - } - } - }) - })) - - return rooms + return await RoomService.getWardOverview(wardId!) }, }) } -export const useRoomUpdateMutation = (callback: (room: RoomMinimalDTO) => void) => { +export const useRoomCreateMutation = (options?: UseMutationOptions) => { const queryClient = useQueryClient() return useMutation({ + ...options, mutationFn: async (room: RoomMinimalDTO) => { - const req = new UpdateRoomRequest() - req.setId(room.id) - req.setName(room.name) - const res = await APIServices.room.updateRoom(req, getAuthenticatedGrpcMetadata()) - - if (!res.toObject()) { - console.error('error in RoomUpdate') - } - - callback(room) - return room + return await RoomService.create(room) }, - onSuccess: () => { + onSuccess: (data, variables, context) => { + if (options?.onSuccess) { + options.onSuccess(data, variables, context) + } queryClient.invalidateQueries([QueryKeys.rooms]).catch(console.error) - }, + queryClient.invalidateQueries([QueryKeys.wards]).catch(console.error) + } }) } -export const useRoomCreateMutation = (callback: (room: RoomMinimalDTO) => void = noop, wardId: string) => { +export const useRoomUpdateMutation = (options?: UseMutationOptions) => { const queryClient = useQueryClient() return useMutation({ + ...options, mutationFn: async (room: RoomMinimalDTO) => { - const req = new CreateRoomRequest() - req.setWardId(wardId) - req.setName(room.name) - const res = await APIServices.room.createRoom(req, getAuthenticatedGrpcMetadata()) - - if (!res.getId()) { - console.error('RoomCreate failed') - } - - room.id = res.getId() - callback(room) - return room + return await RoomService.update(room) }, - onSuccess: () => { + onSuccess: (data, variables, context) => { + if (options?.onSuccess) { + options.onSuccess(data, variables, context) + } queryClient.invalidateQueries([QueryKeys.rooms]).catch(console.error) - queryClient.invalidateQueries([QueryKeys.wards]).catch(console.error) - } + }, }) } -export const useRoomDeleteMutation = (callback: () => void = noop) => { +export const useRoomDeleteMutation = (options?: UseMutationOptions) => { const queryClient = useQueryClient() return useMutation({ + ...options, mutationFn: async (roomId: string) => { - const req = new DeleteRoomRequest() - req.setId(roomId) - const res = await APIServices.room.deleteRoom(req, getAuthenticatedGrpcMetadata()) - - if (!res.toObject()) { - console.error('RoomDelete failed') - } - - callback() - return req.toObject() + return await RoomService.delete(roomId) }, - onSuccess: () => { + onSuccess: (data, variables, context) => { + if (options?.onSuccess) { + options.onSuccess(data, variables, context) + } queryClient.invalidateQueries([QueryKeys.rooms]).catch(console.error) queryClient.invalidateQueries([QueryKeys.wards]).catch(console.error) } diff --git a/api-services/mutations/tasks/task_mutations.ts b/api-services/mutations/tasks/task_mutations.ts index 088040311..351f25af4 100644 --- a/api-services/mutations/tasks/task_mutations.ts +++ b/api-services/mutations/tasks/task_mutations.ts @@ -1,298 +1,168 @@ +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 type { SubtaskDeleteParameter } from '../../service/tasks/TaskSubtaskService' +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({ + ...options, 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({ + ...options, 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({ + ...options, 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() + ...options, + 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() + ...options, + mutationFn: async (subtask: SubtaskDTO) => { + 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() + ...options, + mutationFn: async (parameter: SubtaskDeleteParameter) => { + return await TaskSubtaskService.delete(parameter) }, - 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() + ...options, + 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() + ...options, + 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..de4c3e0d1 100644 --- a/api-services/mutations/tasks/task_template_mutations.ts +++ b/api-services/mutations/tasks/task_template_mutations.ts @@ -1,278 +1,155 @@ +import type { UseMutationOptions } from '@tanstack/react-query' import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query' -import { noop } from '@helpwave/hightide' +import type { TaskTemplateDTO } from '../../types/tasks/tasks_templates' +import type { SubtaskDTO } from '../../types/tasks/task' +import type { + TaskTemplateGetOptions } from '../../service/tasks/TaskTemplateService' import { - CreateTaskTemplateRequest, - CreateTaskTemplateSubTaskRequest, - DeleteTaskTemplateRequest, - DeleteTaskTemplateSubTaskRequest, - GetAllTaskTemplatesRequest, UpdateTaskTemplateRequest, - UpdateTaskTemplateSubTaskRequest -} from '@helpwave/proto-ts/services/tasks_svc/v1/task_template_svc_pb' -import type { TaskTemplateDTO, TaskTemplateFormType } from '../../types/tasks/tasks_templates' -import { APIServices } from '../../services' -import { getAuthenticatedGrpcMetadata } from '../../authentication/grpc_metadata' -import type { SubTaskDTO } from '../../types/tasks/task' + TaskTemplateService, + TaskTemplateSubtaskService +} from '../../service/tasks/TaskTemplateService' +import { QueryKeys } from '../query_keys' -type QueryKey = 'personalTaskTemplates' | 'wardTaskTemplates' +const TaskTemplateTypeQueryKey = { + personal: 0, + ward: 1 +} as const -export const useWardTaskTemplateQuery = (wardId?: string) => { +export const useTaskTemplateQuery = (options?: TaskTemplateGetOptions) => { return useQuery({ - queryKey: ['wardTaskTemplates', wardId], + queryKey: [QueryKeys.taskTemplates], queryFn: async () => { - let wardTaskTemplates: TaskTemplateDTO[] = [] - if (wardId !== undefined) { - const req = new GetAllTaskTemplatesRequest() - req.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 - })), - isPublicVisible: template.getIsPublic() - })) - return wardTaskTemplates - } - - return wardTaskTemplates + return await TaskTemplateService.getMany(options) }, }) } -type UseAllTaskTemplatesByCreatorProps = { - createdBy?: string, - onSuccess: (data: TaskTemplateDTO[]) => void, - type: QueryKey, -} -export const useAllTaskTemplatesByCreator = ({ - createdBy, - onSuccess = noop, - type = 'wardTaskTemplates' -}: UseAllTaskTemplatesByCreatorProps) => { - const queryKey = type - const onlyPrivate = type === 'personalTaskTemplates' +export const useWardTaskTemplateQuery = (wardId?: string) => { return useQuery({ - queryKey: [queryKey, createdBy], + queryKey: [QueryKeys.taskTemplates, TaskTemplateTypeQueryKey.ward, wardId], + enabled: !!wardId, queryFn: async () => { - let personalTaskTemplates: TaskTemplateDTO[] = [] - if (createdBy !== undefined) { - const req = new GetAllTaskTemplatesRequest() - req.setCreatedBy(createdBy) - req.setPrivateOnly(onlyPrivate) - const res = await APIServices.taskTemplates.getAllTaskTemplates(req, getAuthenticatedGrpcMetadata()) + return await TaskTemplateService.getByWard(wardId!) + }, + }) +} - personalTaskTemplates = res.getTemplatesList().map((template) => ({ - id: template.getId(), - name: template.getName(), - notes: template.getDescription(), - subtasks: template.getSubtasksList().map((subtask) => ({ - id: subtask.getId(), - name: subtask.getName(), - isDone: false - })), - isPublicVisible: template.getIsPublic() - })) - return personalTaskTemplates - } - return personalTaskTemplates +export const useAllTaskTemplatesByCreator = ( + createdBy: string, + onlyPersonal: boolean = false +) => { + return useQuery({ + queryKey: [QueryKeys.taskTemplates, onlyPersonal ? TaskTemplateTypeQueryKey.personal : TaskTemplateTypeQueryKey.ward, createdBy], + queryFn: async () => { + return await TaskTemplateService.getByCreator(createdBy) }, - onSuccess }) } -export const usePersonalTaskTemplateQuery = (createdBy?: string, onSuccess: (data: TaskTemplateDTO[]) => void = noop) => { - return useAllTaskTemplatesByCreator({ createdBy, onSuccess, type: 'personalTaskTemplates' }) +export const usePersonalTaskTemplateQuery = (createdBy?: string) => { + return useQuery({ + queryKey: [QueryKeys.taskTemplates, TaskTemplateTypeQueryKey.personal, createdBy], + enabled: !!createdBy, + queryFn: async () => { + return await TaskTemplateService.getByCreator(createdBy!, false) + }, + }) } -export const useUpdateMutation = (queryKey: QueryKey, setTemplate: (taskTemplate?: TaskTemplateDTO) => void) => { +export const useTaskTemplateCreateMutation = (options?: UseMutationOptions) => { const queryClient = useQueryClient() return useMutation({ - mutationFn: async (templateForm: TaskTemplateFormType) => { - const updateTaskTemplate = new UpdateTaskTemplateRequest() - - const taskTemplate = templateForm.template - - updateTaskTemplate.setName(taskTemplate.name) - updateTaskTemplate.setDescription(taskTemplate.notes) - updateTaskTemplate.setId(taskTemplate.id) - - const updateSubtaskTemplate = new UpdateTaskTemplateSubTaskRequest() - const createSubTaskTemplate = new CreateTaskTemplateSubTaskRequest() - const deleteSubtaskTaskTemplate = new DeleteTaskTemplateSubTaskRequest() - - if (templateForm.deletedSubtaskIds) { - for (const id of templateForm.deletedSubtaskIds) { - if (!id) { - continue - } - deleteSubtaskTaskTemplate.setId(id) - await APIServices.taskTemplates.deleteTaskTemplateSubTask(deleteSubtaskTaskTemplate, getAuthenticatedGrpcMetadata()) - } - } - - for (const subtask of taskTemplate.subtasks) { - // create new subtasks - if (!subtask.id) { - createSubTaskTemplate.setName(subtask.name) - createSubTaskTemplate.setTaskTemplateId(taskTemplate.id) - - const res = await APIServices.taskTemplates.createTaskTemplateSubTask(createSubTaskTemplate, getAuthenticatedGrpcMetadata()) - subtask.id = res.getId() - - continue - } - - // update subtask - updateSubtaskTemplate.setName(subtask.name) - updateSubtaskTemplate.setSubtaskId(subtask.id) - - await APIServices.taskTemplates.updateTaskTemplateSubTask(updateSubtaskTemplate, getAuthenticatedGrpcMetadata()) - } - - // update task template - const res = await APIServices.taskTemplates.updateTaskTemplate(updateTaskTemplate, getAuthenticatedGrpcMetadata()) - const newTaskTemplate: TaskTemplateDTO = { ...taskTemplate, ...res } - - templateForm.deletedSubtaskIds = [] - setTemplate(newTaskTemplate) - }, - onMutate: async () => { - await queryClient.cancelQueries({ queryKey: [queryKey] }) - const previousTaskTemplates = queryClient.getQueryData([queryKey]) - queryClient.setQueryData( - [queryKey], - (old) => old -) - return { previousTaskTemplates } + ...options, + mutationFn: async (taskTemplate: TaskTemplateDTO) => { + return await TaskTemplateService.create(taskTemplate) }, - onError: (_, newTodo, context) => { - queryClient.setQueryData([queryKey], context === undefined ? [] : context.previousTaskTemplates) + onSuccess: (data, variables, context) => { + if (options?.onSuccess) { + options.onSuccess(data, variables, context) + } + const key = data.wardId ? TaskTemplateTypeQueryKey.ward : TaskTemplateTypeQueryKey.personal + queryClient.invalidateQueries({ queryKey: [QueryKeys.taskTemplates, key] }).catch(console.error) }, - onSettled: () => { - queryClient.invalidateQueries({ queryKey: [queryKey] }).catch(console.error) - } }) } -export const useCreateMutation = (wardId: string, queryKey: QueryKey, setTemplate: (taskTemplate: TaskTemplateDTO | undefined) => void) => { +export const useTaskTemplateUpdateMutation = (options?: UseMutationOptions) => { const queryClient = useQueryClient() - return useMutation({ - mutationFn: async (taskTemplate: TaskTemplateDTO) => { - const createTaskTemplate = new CreateTaskTemplateRequest() - - createTaskTemplate.setName(taskTemplate.name) - createTaskTemplate.setDescription(taskTemplate.notes) - createTaskTemplate.setSubtasksList(taskTemplate.subtasks.map((cSubtask) => { - const subTask = new CreateTaskTemplateRequest.SubTask() - subTask.setName(cSubtask.name) - return subTask - })) - - if (wardId) { - createTaskTemplate.setWardId(wardId) - } - - const res = await APIServices.taskTemplates.createTaskTemplate(createTaskTemplate, getAuthenticatedGrpcMetadata()) - const newTaskTemplate: TaskTemplateDTO = { ...taskTemplate, ...res } - - setTemplate(newTaskTemplate) - }, - onError: (_, newTodo, context) => { - queryClient.setQueryData([queryKey], context === undefined ? [] : context.previousTaskTemplate) - }, - onMutate: async () => { - await queryClient.cancelQueries({ queryKey: [queryKey] }) - const previousTaskTemplate = queryClient.getQueryData([queryKey]) - queryClient.setQueryData([queryKey], (old) => old) - return { previousTaskTemplate } + ...options, + mutationFn: async (template: TaskTemplateDTO) => { + return await TaskTemplateService.update(template) }, - onSettled: () => { - queryClient.invalidateQueries({ queryKey: [queryKey] }).catch(console.error) + onSuccess: (data, variables, context) => { + if (options?.onSuccess) { + options.onSuccess(data, variables, context) + } + const key = variables.wardId ? TaskTemplateTypeQueryKey.ward : TaskTemplateTypeQueryKey.personal + queryClient.invalidateQueries({ queryKey: [QueryKeys.taskTemplates, key] }).catch(console.error) }, }) } -export const useDeleteMutation = (queryKey: QueryKey, setTemplate: (task?: TaskTemplateDTO) => void) => { +export const useTaskTemplateDeleteMutation = (options?: UseMutationOptions) => { const queryClient = useQueryClient() return useMutation({ - mutationFn: async (taskTemplate: TaskTemplateDTO) => { - const deleteTaskTemplate = new DeleteTaskTemplateRequest() - deleteTaskTemplate.setId(taskTemplate.id) - await APIServices.taskTemplates.deleteTaskTemplate(deleteTaskTemplate, getAuthenticatedGrpcMetadata()) - - setTemplate(undefined) - }, - onMutate: async () => { - await queryClient.cancelQueries({ queryKey: [queryKey] }) - const previousTaskTemplate = queryClient.getQueryData([queryKey]) - queryClient.setQueryData( - [queryKey], - (old) => old -) - return { previousTaskTemplate } + ...options, + mutationFn: async (taskTemplateId: string) => { + return await TaskTemplateService.delete(taskTemplateId) }, - onError: (_, newTodo, context) => { - queryClient.setQueryData([queryKey], context === undefined ? [] : context.previousTaskTemplate) - }, - onSettled: () => { - queryClient.invalidateQueries({ queryKey: [queryKey] }).catch(console.error) + onSuccess: (data, variables, context) => { + if (options?.onSuccess) { + options.onSuccess(data, variables, context) + } + queryClient.invalidateQueries({ queryKey: [QueryKeys.taskTemplates] }).catch(console.error) }, }) } -export const useSubTaskTemplateDeleteMutation = (callback: () => void = noop) => { +export const useTaskTemplateSubtaskCreateMutation = (options?: UseMutationOptions) => { const queryClient = useQueryClient() return useMutation({ - mutationFn: async (subtaskId: string) => { - const deleteSubtaskTaskTemplate = new DeleteTaskTemplateSubTaskRequest() - deleteSubtaskTaskTemplate.setId(subtaskId) - await APIServices.taskTemplates.deleteTaskTemplateSubTask(deleteSubtaskTaskTemplate, getAuthenticatedGrpcMetadata()) - queryClient.invalidateQueries(['personalTaskTemplates']).catch(console.error) - callback() - return deleteSubtaskTaskTemplate.toObject() + ...options, + mutationFn: async (subtask: SubtaskDTO) => { + return await TaskTemplateSubtaskService.create(subtask) }, - onSettled: () => { - queryClient.invalidateQueries({ queryKey: ['personalTaskTemplates'] }).catch(console.error) + onSuccess: (data, variables, context) => { + if (options?.onSuccess) { + options.onSuccess(data, variables, context) + } + queryClient.invalidateQueries({ queryKey: [QueryKeys.taskTemplates] }).catch(console.error) }, }) } -export const useSubTaskTemplateUpdateMutation = (callback: (subtask: SubTaskDTO) => void = noop) => { +export const useTaskTemplateSubtaskUpdateMutation = (options?: UseMutationOptions) => { const queryClient = useQueryClient() return useMutation({ - mutationFn: async (subtask: SubTaskDTO) => { - const updateSubtaskTemplate = new UpdateTaskTemplateSubTaskRequest() - updateSubtaskTemplate.setName(subtask.name) - updateSubtaskTemplate.setSubtaskId(subtask.id) - await APIServices.taskTemplates.updateTaskTemplateSubTask(updateSubtaskTemplate, getAuthenticatedGrpcMetadata()) - const newSubtask: SubTaskDTO = { ...subtask } - queryClient.invalidateQueries(['wardTaskTemplates']).catch(console.error) - callback(newSubtask) - return updateSubtaskTemplate.toObject() + ...options, + mutationFn: async (subtask: SubtaskDTO) => { + return await TaskTemplateSubtaskService.update(subtask) }, - onSettled: () => { - queryClient.invalidateQueries({ queryKey: ['personalTaskTemplates'] }).catch(console.error) + onSuccess: (data, variables, context) => { + if (options?.onSuccess) { + options.onSuccess(data, variables, context) + } + queryClient.invalidateQueries({ queryKey: [QueryKeys.taskTemplates] }).catch(console.error) }, }) } -export const useSubTaskTemplateAddMutation = (taskTemplateId: string) => { +export const useTaskTemplateSubtaskDeleteMutation = (options?: UseMutationOptions) => { const queryClient = useQueryClient() return useMutation({ - mutationFn: async (subtask: SubTaskDTO) => { - const createSubTaskTemplate = new CreateTaskTemplateSubTaskRequest() - createSubTaskTemplate.setName(subtask.name) - createSubTaskTemplate.setTaskTemplateId(taskTemplateId) - await APIServices.taskTemplates.createTaskTemplateSubTask(createSubTaskTemplate, getAuthenticatedGrpcMetadata()) - - queryClient.invalidateQueries(['wardTaskTemplates']).catch(console.error) - return createSubTaskTemplate.toObject() + ...options, + mutationFn: async (subtaskId: string) => { + return await TaskTemplateSubtaskService.delete(subtaskId) + }, + onSuccess: (data, variables, context) => { + if (options?.onSuccess) { + options.onSuccess(data, variables, context) + } + queryClient.invalidateQueries({ queryKey: [QueryKeys.taskTemplates] }).catch(console.error) }, }) } diff --git a/api-services/mutations/tasks/util.ts b/api-services/mutations/tasks/util.ts index 4b2eded2b..a7f12816a 100644 --- a/api-services/mutations/tasks/util.ts +++ b/api-services/mutations/tasks/util.ts @@ -1,4 +1,4 @@ -import type { SubTaskDTO } from '../../types/tasks/task' +import type { SubtaskDTO } from '../../types/tasks/task' interface GRPCSubTask { getId: () => string, @@ -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..a684659e5 100644 --- a/api-services/mutations/tasks/ward_mutations.ts +++ b/api-services/mutations/tasks/ward_mutations.ts @@ -1,35 +1,22 @@ +import type { UseMutationOptions } from '@tanstack/react-query' import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query' -import { - CreateWardRequest, - DeleteWardRequest, - GetWardDetailsRequest, - GetWardOverviewsRequest, - GetWardRequest, - UpdateWardRequest -} from '@helpwave/proto-ts/services/tasks_svc/v1/ward_svc_pb' -import { noop } from '@helpwave/hightide' -import { APIServices } from '../../services' -import { getAuthenticatedGrpcMetadata } from '../../authentication/grpc_metadata' -import type { WardDetailDTO, WardMinimalDTO, WardOverviewDTO } from '../../types/tasks/wards' +import type { WardDetailDTO, WardMinimalDTO } from '../../types/tasks/wards' import { QueryKeys } from '../query_keys' +import { WardService } from '../../service/tasks/WardService' -export const useWardOverviewsQuery = (organisationId?: string) => { +export const useWardQuery = (id: string) => useQuery({ + queryKey: [QueryKeys.wards, id], + enabled: !!id, + queryFn: async (): Promise => { + return await WardService.get(id) + } +}) + +export const useWardOverviewsQuery = () => { return useQuery({ - queryKey: [QueryKeys.wards, organisationId], + queryKey: [QueryKeys.wards], queryFn: async () => { - const req = new GetWardOverviewsRequest() - const res = await APIServices.ward.getWardOverviews(req, getAuthenticatedGrpcMetadata()) - - const wards: WardOverviewDTO[] = res.getWardsList().map((ward) => ({ - id: ward.getId(), - name: ward.getName(), - bedCount: ward.getBedCount(), - unscheduled: ward.getTasksTodo(), - inProgress: ward.getTasksInProgress(), - done: ward.getTasksDone(), - })) - - return wards + return await WardService.getWardOverviews() } }) } @@ -38,105 +25,55 @@ export const useWardDetailsQuery = (wardId?: string) => { return useQuery({ queryKey: [QueryKeys.wards, wardId], enabled: !!wardId, - queryFn: async (): Promise => { - if (!wardId) { - return null - } - - const req = new GetWardDetailsRequest() - req.setId(wardId) - const res = await APIServices.ward.getWardDetails(req, getAuthenticatedGrpcMetadata()) - - return { - id: res.getId(), - name: res.getName(), - rooms: res.getRoomsList().map((room) => ({ - id: room.getId(), - name: room.getName(), - beds: room.getBedsList().map((bed) => ({ - id: bed.getId() - })) - })), - task_templates: res.getTaskTemplatesList().map((taskTemplate) => ({ - id: taskTemplate.getId(), - name: taskTemplate.getName(), - subtasks: taskTemplate.getSubtasksList().map((subTask) => ({ - id: subTask.getId(), - name: subTask.getName() - })) - })) - } + queryFn: async (): Promise => { + return await WardService.getDetails(wardId!) } }) } -export const useWardQuery = (id: string) => useQuery({ - queryKey: [QueryKeys.wards, id], - enabled: !!id, - queryFn: async (): Promise => { - const req = new GetWardRequest() - req.setId(id) - const res = await APIServices.ward.getWard(req, getAuthenticatedGrpcMetadata()) - - if (!res.toObject()) { - console.error('error in Ward query') - } - - return { - id: res.getId(), - name: res.getName(), - } - } -}) - -export const useWardUpdateMutation = (callback: (ward: WardMinimalDTO) => void = noop) => { +export const useWardCreateMutation = (options?: UseMutationOptions) => { const queryClient = useQueryClient() return useMutation({ + ...options, mutationFn: async (ward: WardMinimalDTO) => { - const req = new UpdateWardRequest() - req.setId(ward.id) - req.setName(ward.name) - await APIServices.ward.updateWard(req, getAuthenticatedGrpcMetadata()) - - callback(ward) + return await WardService.create(ward) }, - onSuccess: () => { + onSuccess: (data, variables, context) => { + if (options?.onSuccess) { + return options.onSuccess(data, variables, context) + } queryClient.invalidateQueries([QueryKeys.wards]).catch(console.error) } }) } -export const useWardCreateMutation = (callback: (ward: WardMinimalDTO) => void = noop) => { +export const useWardUpdateMutation = (options?: UseMutationOptions) => { const queryClient = useQueryClient() return useMutation({ + ...options, mutationFn: async (ward: WardMinimalDTO) => { - const createWardRequest = new CreateWardRequest() - createWardRequest.setName(ward.name) - const res = await APIServices.ward.createWard(createWardRequest, getAuthenticatedGrpcMetadata()) - const newWard: WardMinimalDTO = { - ...ward, - id: res.getId() - } - - callback(newWard) + return await WardService.update(ward) }, - onSuccess: () => { + onSuccess: (data, variables, context) => { + if (options?.onSuccess) { + return options.onSuccess(data, variables, context) + } queryClient.invalidateQueries([QueryKeys.wards]).catch(console.error) } }) } -export const useWardDeleteMutation = (callback: () => void = noop) => { +export const useWardDeleteMutation = (options?: UseMutationOptions) => { const queryClient = useQueryClient() return useMutation({ + ...options, mutationFn: async (wardId: string) => { - const req = new DeleteWardRequest() - req.setId(wardId) - await APIServices.ward.deleteWard(req, getAuthenticatedGrpcMetadata()) - - callback() + return await WardService.delete(wardId) }, - onSuccess: () => { + onSuccess: (data, variables, context) => { + if (options?.onSuccess) { + return options.onSuccess(data, variables, context) + } queryClient.invalidateQueries([QueryKeys.wards]).catch(console.error) } }) diff --git a/api-services/mutations/users/organization_member_mutations.ts b/api-services/mutations/users/organization_member_mutations.ts index 1adc559f6..e392a28e7 100644 --- a/api-services/mutations/users/organization_member_mutations.ts +++ b/api-services/mutations/users/organization_member_mutations.ts @@ -1,35 +1,16 @@ import { useQuery } from '@tanstack/react-query' -import { GetMembersByOrganizationRequest } from '@helpwave/proto-ts/services/user_svc/v1/organization_svc_pb' -import { APIServices } from '../../services' -import { getAuthenticatedGrpcMetadata } from '../../authentication/grpc_metadata' import { QueryKeys } from '../query_keys' -import type { OrganizationMember } from '../../types/users/organization_member' +import { useAuth } from '../../authentication/useAuth' +import { OrganizationMemberService } from '../../service/users/OrganizationMemberService' -export const useMembersByOrganizationQuery = (organizationId?: string) => { +export const useMembersByOrganizationQuery = () => { + const { organization } = useAuth() + const organizationId = organization?.id return useQuery({ queryKey: [QueryKeys.organizations, organizationId, 'members'], enabled: !!organizationId, queryFn: async () => { - if (!organizationId) { - return [] - } - - const req = new GetMembersByOrganizationRequest() - req.setId(organizationId) - - const res = await APIServices.organization.getMembersByOrganization(req, getAuthenticatedGrpcMetadata()) - - if (!res.toObject()) { - console.error('error in MembersByOrganization') - } - - const members: OrganizationMember[] = res.getMembersList().map(member => ({ - id: member.getUserId(), - name: member.getNickname(), - email: member.getEmail(), - avatarURL: member.getAvatarUrl(), - })) - return members + return await OrganizationMemberService.getByOrganization(organizationId!) }, }) } diff --git a/api-services/mutations/users/user_mutations.ts b/api-services/mutations/users/user_mutations.ts index 9155ded11..0dff78b26 100644 --- a/api-services/mutations/users/user_mutations.ts +++ b/api-services/mutations/users/user_mutations.ts @@ -1,21 +1,14 @@ import { useQuery } from '@tanstack/react-query' -import { ReadPublicProfileRequest } from '@helpwave/proto-ts/services/user_svc/v1/user_svc_pb' import { QueryKeys } from '../query_keys' -import { APIServices } from '../../services' -import { getAuthenticatedGrpcMetadata } from '../../authentication/grpc_metadata' +import { UserService } from '../../service/users/UserService' -export const useUserQuery = (userId: string | undefined) => { - const enabled = !!userId && userId !== '00000000-0000-0000-0000-000000000000' +export const useUserQuery = (userId?: string) => { + const enabled = !!userId return useQuery({ queryKey: [QueryKeys.users, userId], enabled, queryFn: async () => { - if (!enabled || !userId) return - const req = new ReadPublicProfileRequest() - req.setId(userId) - - const res = await APIServices.user.readPublicProfile(req, getAuthenticatedGrpcMetadata()) - return res.toObject() + return await UserService.get(userId!) }, }) } 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..818d95222 --- /dev/null +++ b/api-services/service/tasks/PatientService.ts @@ -0,0 +1,193 @@ +import type { + PatientDetailsDTO, + PatientDTO, + PatientListDTO, + 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 { BedWithMinimalPatientDTO, BedWithPatientId } from '../../types/tasks/bed' + +export const PatientService = { + getDetails: async function (patientId: string): Promise { + const req = new GetPatientDetailsRequest() + .setId(patientId) + + const res = await APIServices.patient.getPatientDetails(req, getAuthenticatedGrpcMetadata()) + + return { + id: res.getId(), + notes: res.getNotes(), + humanReadableIdentifier: res.getHumanReadableIdentifier(), + discharged: res.getIsDischarged(), + bed: res.getBed()?.toObject(), + room: res.getRoom()?.toObject(), + wardId: res.getRoom()?.getWardId(), + 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() + .setWardId(wardId) + const res = await APIServices.patient.getPatientsByWard(req, getAuthenticatedGrpcMetadata()) + + return res.getPatientsList().map((patient) => patient.toObject()) + }, + 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) => ({ + ...room.toObject(), + wardId, + beds: room.getBedsList().map(bed => ({ + ...bed.toObject(), + patient: bed.hasPatient() ? { + ...bed.getPatient()!.toObject(), + bedId: bed.getId(), + humanReadableIdentifier: bed.getPatient()!.getName(), + } : undefined, + })) + } + )) + }, + 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) { + throw new Error('No room for active patient in PatientList') + } + if (!bed) { + throw new Error('No bed for active patient in PatientList') + } + return ({ + id: value.getId(), + humanReadableIdentifier: value.getHumanReadableIdentifier(), + bed: bed!.toObject(), + room: room!.toObject(), + }) + }), + discharged: res.getDischargedPatientsList().map(value => value.toObject()), + unassigned: res.getUnassignedPatientsList().map(value => value.toObject()) + } + }, + 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()) { + patients.push({ + id: patient.getId(), + humanReadableIdentifier: patient.getHumanReadableIdentifier(), + bed: patient.getBed()?.toObject(), + room: patient.getRoom()?.toObject(), + wardId: patient.getRoom()?.getWardId(), + }) + } + return patients + }, + create: async function (patient: PatientDTO): Promise { + const req = new CreatePatientRequest() + .setNotes(patient.notes) + .setHumanReadableIdentifier(patient.humanReadableIdentifier) + const res = await APIServices.patient.createPatient(req, getAuthenticatedGrpcMetadata()) + + try { + if (patient.bedId) { + await PatientService.assignToBed({ patientId: res.getId(), bedId: patient.bedId }) + } + } catch (err) { + console.log(err) + } + + return { ...patient, id: res.getId() } + }, + update: async function (patient: PatientDTO): Promise { + const req = new UpdatePatientRequest() + .setId(patient.id) + .setNotes(patient.notes) + .setHumanReadableIdentifier(patient.humanReadableIdentifier) + + await APIServices.patient.updatePatient(req, getAuthenticatedGrpcMetadata()) + return true + }, + delete: async function (patientId: string): Promise { + const req = new DeletePatientRequest() + .setId(patientId) + + await APIServices.patient.deletePatient(req, getAuthenticatedGrpcMetadata()) + return true + }, + assignToBed: async function (bedWithPatientId: BedWithPatientId): Promise { + const req = new AssignBedRequest() + .setId(bedWithPatientId.patientId) + .setBedId(bedWithPatientId.bedId) + + await APIServices.patient.assignBed(req, getAuthenticatedGrpcMetadata()) + return true + }, + unassignFromBed: async function (patientId: string): Promise { + const req = new UnassignBedRequest() + .setId(patientId) + + await APIServices.patient.unassignBed(req, getAuthenticatedGrpcMetadata()) + return true + }, + discharge: async function (patientId: string): Promise { + const req = new DischargePatientRequest() + .setId(patientId) + + await APIServices.patient.dischargePatient(req, getAuthenticatedGrpcMetadata()) + return true + }, + reAdmit: async function (patientId: string): Promise { + const req = new ReadmitPatientRequest() + .setPatientId(patientId) + + await APIServices.patient.readmitPatient(req, getAuthenticatedGrpcMetadata()) + return true + } +} diff --git a/api-services/service/tasks/RoomService.ts b/api-services/service/tasks/RoomService.ts new file mode 100644 index 000000000..7b99845ea --- /dev/null +++ b/api-services/service/tasks/RoomService.ts @@ -0,0 +1,74 @@ +import { APIServices } from '../../services' +import { getAuthenticatedGrpcMetadata } from '../../authentication/grpc_metadata' +import { + CreateRoomRequest, DeleteRoomRequest, + GetRoomOverviewsByWardRequest, + GetRoomRequest, UpdateRoomRequest +} from '@helpwave/proto-ts/services/tasks_svc/v1/room_svc_pb' +import type { RoomDTO, RoomMinimalDTO, RoomOverviewDTO } from '../../types/tasks/room' +import type { BedWithPatientWithTasksNumberDTO } from '../../types/tasks/bed' + +export const RoomService = { + get: async (id: string): Promise => { + const req = new GetRoomRequest() + .setId(id) + const res = await APIServices.room.getRoom(req, getAuthenticatedGrpcMetadata()) + + return { + id: res.getId(), + name: res.getName(), + wardId: res.getWardId(), + beds: res.getBedsList().map(bed => ({ + id: bed.getId(), + name: bed.getName() + })) + } + }, + getWardOverview: async (wardId: string): Promise => { + const req = new GetRoomOverviewsByWardRequest() + .setId(wardId) + const res = await APIServices.room.getRoomOverviewsByWard(req, getAuthenticatedGrpcMetadata()) + + return res.getRoomsList().map((room) => ({ + id: room.getId(), + name: room.getName(), + wardId, + beds: room.getBedsList().map(bed => { + const patient = bed.getPatient() + return { + id: bed.getId(), + name: bed.getName(), + patient: !patient ? undefined : { + id: patient.getId(), + humanReadableIdentifier: patient.getHumanReadableIdentifier(), + bedId: bed.getId(), + wardId: wardId, + tasksTodo: patient.getTasksUnscheduled(), + tasksInProgress: patient.getTasksInProgress(), + tasksDone: patient.getTasksDone() + } + } + }) + })) + }, + create: async (room: RoomMinimalDTO): Promise => { + const req = new CreateRoomRequest() + .setWardId(room.wardId) + .setName(room.name) + const res = await APIServices.room.createRoom(req, getAuthenticatedGrpcMetadata()) + return { ...room, id: res.getId() } + }, + update: async (room: RoomMinimalDTO): Promise => { + const req = new UpdateRoomRequest() + .setId(room.id) + .setName(room.name) + await APIServices.room.updateRoom(req, getAuthenticatedGrpcMetadata()) + return true + }, + delete: async (id: string): Promise => { + const req = new DeleteRoomRequest() + .setId(id) + await APIServices.room.deleteRoom(req, getAuthenticatedGrpcMetadata()) + return true + } +} 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..c95985ff1 --- /dev/null +++ b/api-services/service/tasks/TaskSubtaskService.ts @@ -0,0 +1,46 @@ +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 type SubtaskDeleteParameter = { + id: string, + taskId: string, +} + +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 ({ id, taskId }: SubtaskDeleteParameter): Promise { + const req = new DeleteSubtaskRequest() + .setSubtaskId(id) + .setTaskId(taskId) + const res = await APIServices.task.deleteSubtask(req, getAuthenticatedGrpcMetadata()) + return !!res.toObject() + }, +} diff --git a/api-services/service/tasks/TaskTemplateService.ts b/api-services/service/tasks/TaskTemplateService.ts new file mode 100644 index 000000000..0a59c389a --- /dev/null +++ b/api-services/service/tasks/TaskTemplateService.ts @@ -0,0 +1,128 @@ +import type { TaskTemplateDTO } from '../../types/tasks/tasks_templates' +import { + CreateTaskTemplateRequest, + CreateTaskTemplateSubTaskRequest, + DeleteTaskTemplateRequest, + DeleteTaskTemplateSubTaskRequest, + GetAllTaskTemplatesRequest, + UpdateTaskTemplateRequest, + UpdateTaskTemplateSubTaskRequest +} from '@helpwave/proto-ts/services/tasks_svc/v1/task_template_svc_pb' +import { APIServices } from '../../services' +import { getAuthenticatedGrpcMetadata } from '../../authentication/grpc_metadata' +import type { SubtaskDTO } from '../../types/tasks/task' + +export type TaskTemplateGetOptions = { + wardId?: string, + onlyPersonal?: boolean, + creatorId?: string, +} + +export const TaskTemplateService = { + /* TODO use when we can get the wardId here + get: async (id: string): Promise => { + const req = new GetTaskTemplateRequest() + .setId(id) + const res = await APIServices.taskTemplates.getTaskTemplate(req, getAuthenticatedGrpcMetadata()) + return { + id: res.getId(), + name: res.getName(), + notes: res.getDescription(), + subtasks: res.getSubtasksList().map((subtask) => ({ + id: subtask.getId(), + name: subtask.getName(), + isDone: false, + taskId: res.getId(), + })), + isPublicVisible: res.getIsPublic(), + creatorId: res.getCreatedBy(), + } + }, + */ + getMany: async (options?: TaskTemplateGetOptions): Promise => { + const req = new GetAllTaskTemplatesRequest() + if (options?.wardId) { + req.setWardId(options.wardId) + } + if (options?.creatorId) { + req.setCreatedBy(options.creatorId) + } + if (options?.onlyPersonal) { + req.setPrivateOnly(options.onlyPersonal) + } + const res = await APIServices.taskTemplates.getAllTaskTemplates(req, getAuthenticatedGrpcMetadata()) + return res.getTemplatesList().map((template) => ({ + id: template.getId(), + wardId: options?.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(), + creatorId: template.getCreatedBy(), + })) + }, + getByWard: async (wardId: string): Promise => { + return TaskTemplateService.getMany({ wardId }) + }, + getByCreator: async (creatorId: string, onlyPersonal: boolean = false): Promise => { + return TaskTemplateService.getMany({ creatorId, onlyPersonal }) + }, + create: async (taskTemplate: TaskTemplateDTO): Promise => { + const req = new CreateTaskTemplateRequest() + .setName(taskTemplate.name) + .setDescription(taskTemplate.notes) + .setSubtasksList(taskTemplate.subtasks.map((cSubtask) => { + const subTask = new CreateTaskTemplateRequest.SubTask() + subTask.setName(cSubtask.name) + return subTask + })) + if (taskTemplate?.wardId) { + req.setWardId(taskTemplate.wardId) + } + + const res = await APIServices.taskTemplates.createTaskTemplate(req, getAuthenticatedGrpcMetadata()) + return { ...taskTemplate, id: res.getId() } + }, + update: async (taskTemplate: TaskTemplateDTO): Promise => { + const updateTaskTemplate = new UpdateTaskTemplateRequest() + .setId(taskTemplate.id) + .setName(taskTemplate.name) + .setDescription(taskTemplate.notes) + await APIServices.taskTemplates.updateTaskTemplate(updateTaskTemplate, getAuthenticatedGrpcMetadata()) + return true + }, + delete: async (taskTemplateId: string): Promise => { + const req = new DeleteTaskTemplateRequest() + .setId(taskTemplateId) + await APIServices.taskTemplates.deleteTaskTemplate(req, getAuthenticatedGrpcMetadata()) + return true + } +} + +export const TaskTemplateSubtaskService = { + create: async (subtask: SubtaskDTO): Promise => { + const req = new CreateTaskTemplateSubTaskRequest() + .setName(subtask.name) + .setTaskTemplateId(subtask.taskId) + const res = await APIServices.taskTemplates.createTaskTemplateSubTask(req, getAuthenticatedGrpcMetadata()) + return { ...subtask, id: res.getId() } + }, + update: async (subtask: SubtaskDTO): Promise => { + const req = new UpdateTaskTemplateSubTaskRequest() + .setName(subtask.name) + .setSubtaskId(subtask.id) + await APIServices.taskTemplates.updateTaskTemplateSubTask(req, getAuthenticatedGrpcMetadata()) + return true + }, + delete: async (subtaskId: string): Promise => { + const req = new DeleteTaskTemplateSubTaskRequest() + .setId(subtaskId) + await APIServices.taskTemplates.deleteTaskTemplateSubTask(req, getAuthenticatedGrpcMetadata()) + return true + } +} diff --git a/api-services/service/tasks/WardService.ts b/api-services/service/tasks/WardService.ts new file mode 100644 index 000000000..c8eebccfb --- /dev/null +++ b/api-services/service/tasks/WardService.ts @@ -0,0 +1,83 @@ +import { APIServices } from '../../services' +import { getAuthenticatedGrpcMetadata } from '../../authentication/grpc_metadata' +import { + CreateWardRequest, + DeleteWardRequest, + GetWardDetailsRequest, + GetWardOverviewsRequest, + GetWardRequest, + UpdateWardRequest +} from '@helpwave/proto-ts/services/tasks_svc/v1/ward_svc_pb' +import type { WardDetailDTO, WardMinimalDTO, WardOverviewDTO } from '../../types/tasks/wards' + +export const WardService = { + get: async (id: string): Promise => { + const req = new GetWardRequest() + .setId(id) + const res = await APIServices.ward.getWard(req, getAuthenticatedGrpcMetadata()) + return { + id: res.getId(), + name: res.getName(), + } + }, + getDetails: async (wardId: string): Promise => { + const req = new GetWardDetailsRequest() + .setId(wardId) + const res = await APIServices.ward.getWardDetails(req, getAuthenticatedGrpcMetadata()) + + return { + id: res.getId(), + name: res.getName(), + rooms: res.getRoomsList().map((room) => ({ + id: room.getId(), + name: room.getName(), + beds: room.getBedsList().map((bed) => ({ + id: bed.getId() + })) + })), + task_templates: res.getTaskTemplatesList().map((taskTemplate) => ({ + id: taskTemplate.getId(), + name: taskTemplate.getName(), + subtasks: taskTemplate.getSubtasksList().map((subTask) => ({ + id: subTask.getId(), + name: subTask.getName() + })) + })) + } + }, + getWardOverviews: async (): Promise => { + const req = new GetWardOverviewsRequest() + const res = await APIServices.ward.getWardOverviews(req, getAuthenticatedGrpcMetadata()) + + return res.getWardsList().map((ward) => ({ + id: ward.getId(), + name: ward.getName(), + bedCount: ward.getBedCount(), + unscheduled: ward.getTasksTodo(), + inProgress: ward.getTasksInProgress(), + done: ward.getTasksDone(), + })) + }, + create: async (ward: WardMinimalDTO): Promise => { + const createWardRequest = new CreateWardRequest() + createWardRequest.setName(ward.name) + const res = await APIServices.ward.createWard(createWardRequest, getAuthenticatedGrpcMetadata()) + return { + ...ward, + id: res.getId() + } + }, + update: async (ward: WardMinimalDTO): Promise => { + const req = new UpdateWardRequest() + .setId(ward.id) + .setName(ward.name) + await APIServices.ward.updateWard(req, getAuthenticatedGrpcMetadata()) + return true + }, + delete: async (id: string): Promise => { + const req = new DeleteWardRequest() + .setId(id) + await APIServices.ward.deleteWard(req, getAuthenticatedGrpcMetadata()) + return true + } +} diff --git a/api-services/service/users/OrganizationMemberService.ts b/api-services/service/users/OrganizationMemberService.ts new file mode 100644 index 000000000..14422ef36 --- /dev/null +++ b/api-services/service/users/OrganizationMemberService.ts @@ -0,0 +1,18 @@ +import { APIServices } from '../../services' +import { getAuthenticatedGrpcMetadata } from '../../authentication/grpc_metadata' +import type { OrganizationMember } from '../../types/users/organization_member' +import { GetMembersByOrganizationRequest } from '@helpwave/proto-ts/services/user_svc/v1/organization_svc_pb' + +export const OrganizationMemberService = { + getByOrganization: async (id: string): Promise => { + const req = new GetMembersByOrganizationRequest() + .setId(id) + + const res = await APIServices.organization.getMembersByOrganization(req, getAuthenticatedGrpcMetadata()) + + return res.getMembersList().map(member => ({ + ...member.toObject(), + avatarURL: 'https://cdn.helpwave.de/boringavatar.svg', // TODO remove later + })) + }, +} diff --git a/api-services/service/users/OrganizationService.ts b/api-services/service/users/OrganizationService.ts index f1a470ae4..0fe2a36b7 100644 --- a/api-services/service/users/OrganizationService.ts +++ b/api-services/service/users/OrganizationService.ts @@ -4,20 +4,49 @@ import type { OrganizationWithMinimalMemberDTO } from '../../types/users/organizations' import { - CreateOrganizationRequest, CreatePersonalOrganizationRequest, DeleteOrganizationRequest, + CreateOrganizationRequest, + CreatePersonalOrganizationRequest, + DeleteOrganizationRequest, GetOrganizationRequest, - GetOrganizationsForUserRequest, UpdateOrganizationRequest + GetOrganizationsForUserRequest, + UpdateOrganizationRequest } from '@helpwave/proto-ts/services/user_svc/v1/organization_svc_pb' import { APIServices } from '../../services' import { getAuthenticatedGrpcMetadata } from '../../authentication/grpc_metadata' export const OrganizationService = { + get: async (id: string): Promise => { + const req = new GetOrganizationRequest() + .setId(id) + + const res = await APIServices.organization.getOrganization(req, getAuthenticatedGrpcMetadata()) + + return { + ...res.toObject(), + members: res.getMembersList().map(member => ({ + ...member.toObject(), + })) + } + }, + getForUser: async (): Promise => { + const req = new GetOrganizationsForUserRequest() + const res = await APIServices.organization.getOrganizationsForUser(req, getAuthenticatedGrpcMetadata()) + + return res.getOrganizationsList().map(organization => ({ + ...organization.toObject(), + avatarURL: 'https://cdn.helpwave.de/boringavatar.svg', // TODO remove later + members: organization.getMembersList().map(member => ({ + ...member.toObject(), + avatarURL: 'https://cdn.helpwave.de/boringavatar.svg', // TODO remove later + })) + })) + }, create: async (organization: OrganizationMinimalDTO): Promise => { const req = new CreateOrganizationRequest() - req.setLongName(organization.longName) - req.setShortName(organization.shortName) - req.setIsPersonal(organization.isPersonal) - req.setContactEmail(organization.email) + .setLongName(organization.longName) + .setShortName(organization.shortName) + .setIsPersonal(organization.isPersonal) + .setContactEmail(organization.contactEmail) const res = await APIServices.organization.createOrganization(req, getAuthenticatedGrpcMetadata()) return { ...organization, id: res.getId() } @@ -27,61 +56,25 @@ export const OrganizationService = { const res = await APIServices.organization.createPersonalOrganization(req, getAuthenticatedGrpcMetadata()) return res.getId() }, - delete: async (id: string): Promise => { - const req = new DeleteOrganizationRequest() - req.setId(id) - await APIServices.organization.deleteOrganization(req, getAuthenticatedGrpcMetadata()) - return true - }, - get: async (id: string): Promise => { - const req = new GetOrganizationRequest() - req.setId(id) - - const res = await APIServices.organization.getOrganization(req, getAuthenticatedGrpcMetadata()) - - return { - id: res.getId(), - email: res.getContactEmail(), - isVerified: true, // TODO update later - longName: res.getLongName(), - shortName: res.getShortName(), - isPersonal: res.getIsPersonal(), - avatarURL: res.getAvatarUrl(), - members: res.getMembersList().map(member => ({ id: member.getUserId() })) - } - }, - getForUser: - async (): Promise => { - const req = new GetOrganizationsForUserRequest() - const res = await APIServices.organization.getOrganizationsForUser(req, getAuthenticatedGrpcMetadata()) - - return res.getOrganizationsList().map(organization => ({ - id: organization.getId(), - email: organization.getContactEmail(), - isPersonal: organization.getIsPersonal(), - avatarUrl: organization.getAvatarUrl(), - longName: organization.getLongName(), - shortName: organization.getShortName(), - avatarURL: organization.getAvatarUrl(), - isVerified: true, // TODO change Later - members: organization.getMembersList().map(member => ({ - id: member.getUserId(), - email: member.getEmail(), - name: member.getNickname(), - avatarURL: member.getAvatarUrl(), - })) - })) - }, update: async (organization: OrganizationMinimalDTO): Promise => { const req = new UpdateOrganizationRequest() - req.setId(organization.id) - req.setLongName(organization.longName) - req.setShortName(organization.shortName) - req.setIsPersonal(organization.isPersonal) - req.setAvatarUrl(organization.avatarURL) - req.setContactEmail(organization.email) + .setId(organization.id) + .setLongName(organization.longName) + .setShortName(organization.shortName) + .setIsPersonal(organization.isPersonal) + .setContactEmail(organization.contactEmail) + + if(organization.avatarURL) { + req.setAvatarUrl(organization.avatarURL) + } await APIServices.organization.updateOrganization(req, getAuthenticatedGrpcMetadata()) return organization + }, + delete: async (id: string): Promise => { + const req = new DeleteOrganizationRequest() + .setId(id) + await APIServices.organization.deleteOrganization(req, getAuthenticatedGrpcMetadata()) + return true } } diff --git a/api-services/service/users/PatientService.ts b/api-services/service/users/PatientService.ts deleted file mode 100644 index 4c16413f4..000000000 --- a/api-services/service/users/PatientService.ts +++ /dev/null @@ -1,225 +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() - })) - })) - } - }, - 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/service/users/UserService.ts b/api-services/service/users/UserService.ts new file mode 100644 index 000000000..cbc7842a2 --- /dev/null +++ b/api-services/service/users/UserService.ts @@ -0,0 +1,17 @@ +import { APIServices } from '../../services' +import { getAuthenticatedGrpcMetadata } from '../../authentication/grpc_metadata' +import { ReadPublicProfileRequest } from '@helpwave/proto-ts/services/user_svc/v1/user_svc_pb' +import type { User } from '../../types/users/user' + +export const UserService = { + get: async (id: string): Promise => { + const req = new ReadPublicProfileRequest() + .setId(id) + + const res = await APIServices.user.readPublicProfile(req, getAuthenticatedGrpcMetadata()) + return { + ...res.toObject(), + avatarUrl: `https://cdn.helpwave.de/boringavatar.svg` + } + }, +} 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/types/tasks/bed.ts b/api-services/types/tasks/bed.ts index 7b55867c3..4ad6671f4 100644 --- a/api-services/types/tasks/bed.ts +++ b/api-services/types/tasks/bed.ts @@ -34,6 +34,6 @@ export type BedWithMinimalPatientDTO = BedMinimalDTO & { } export type BedWithPatientId = { - id: string, + bedId: string, patientId: string, } diff --git a/api-services/types/tasks/patient.ts b/api-services/types/tasks/patient.ts index 83d81d158..7c40272ed 100644 --- a/api-services/types/tasks/patient.ts +++ b/api-services/types/tasks/patient.ts @@ -1,47 +1,38 @@ import type { TaskDTO, TaskMinimalDTO } from './task' +import type { RoomMinimalDTO } from './room' +import type { BedMinimalDTO } from './bed' export type PatientMinimalDTO = { id: string, - name: string, + humanReadableIdentifier: string, + bedId?: string, } -export type PatientDTO = PatientMinimalDTO & { - note: string, - tasks: TaskMinimalDTO[], +export type PatientWithBedIdDTO = PatientMinimalDTO & { + notes: string, + bedId?: string, } -export const emptyPatient: PatientDTO = { - id: '', - note: '', - name: '', - tasks: [] +export type PatientDTO = PatientMinimalDTO & { + notes: string, + tasks: TaskMinimalDTO[], } export type PatientWithTasksNumberDTO = PatientMinimalDTO & { - tasksUnscheduled: number, + tasksTodo: number, tasksInProgress: number, tasksDone: number, } -export type PatientCompleteDTO = PatientMinimalDTO & { - note: string, - tasks: TaskDTO[], -} - -export const emptyPatientMinimal: PatientMinimalDTO = { - id: '', - name: '' -} - export type PatientWithBedAndRoomDTO = PatientMinimalDTO & { - room: { id: string, name: string, wardId: string }, - bed: { id: string, name: string }, + room: RoomMinimalDTO, + bed: BedMinimalDTO, } export type RecentPatientDTO = PatientMinimalDTO & { wardId?: string, - room?: { id: string, name: string }, - bed?: { id: string, name: string }, + room?: RoomMinimalDTO, + bed?: BedMinimalDTO, } export type PatientListDTO = { @@ -50,21 +41,19 @@ export type PatientListDTO = { discharged: PatientMinimalDTO[], } -export type PatientWithBedIdDTO = PatientMinimalDTO & { - note: string, - bedId: string, -} - export type PatientDetailsDTO = PatientMinimalDTO & { - note: string, + notes: string, tasks: TaskDTO[], discharged: boolean, + room?: RoomMinimalDTO, + bed?: BedMinimalDTO, + wardId?: string, } export const emptyPatientDetails: PatientDetailsDTO = { id: '', - name: '', - note: '', + humanReadableIdentifier: '', + notes: '', tasks: [], discharged: false } diff --git a/api-services/types/tasks/room.ts b/api-services/types/tasks/room.ts index 47436abf7..f6cc4f522 100644 --- a/api-services/types/tasks/room.ts +++ b/api-services/types/tasks/room.ts @@ -3,9 +3,6 @@ import type { BedDTO, BedWithMinimalPatientDTO, BedWithPatientWithTasksNumberDTO export type RoomMinimalDTO = { id: string, name: string, -} - -export type RoomWithWardId = RoomMinimalDTO & { wardId: string, } @@ -20,6 +17,7 @@ export type RoomOverviewDTO = RoomMinimalDTO & { export const emptyRoomOverview: RoomOverviewDTO = { id: '', name: '', + wardId: '', beds: [] } diff --git a/api-services/types/tasks/task.ts b/api-services/types/tasks/task.ts index 8bffa8af5..685e93206 100644 --- a/api-services/types/tasks/task.ts +++ b/api-services/types/tasks/task.ts @@ -1,15 +1,13 @@ import type { Translation } from '@helpwave/hightide' -export type SubTaskDTO = { +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 = { @@ -39,10 +66,11 @@ export type TaskDTO = { assignee?: string, notes: string, status: TaskStatus, - subtasks: SubTaskDTO[], + subtasks: SubtaskDTO[], 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..d69be2bac 100644 --- a/api-services/types/tasks/tasks_templates.ts +++ b/api-services/types/tasks/tasks_templates.ts @@ -1,14 +1,13 @@ +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, + creatorId: string, } export const emptyTaskTemplate: TaskTemplateDTO = { @@ -16,7 +15,8 @@ export const emptyTaskTemplate: TaskTemplateDTO = { isPublicVisible: false, name: '', notes: '', - subtasks: [] + subtasks: [], + creatorId: '' } export type TaskTemplateFormType = { diff --git a/api-services/types/users/organization_member.ts b/api-services/types/users/organization_member.ts index b0d642ee5..78cdfe0a0 100644 --- a/api-services/types/users/organization_member.ts +++ b/api-services/types/users/organization_member.ts @@ -1,9 +1,9 @@ export type OrganizationMemberMinimalDTO = { - id: string, + userId: string, } export type OrganizationMember = OrganizationMemberMinimalDTO & { email: string, - name: string, - avatarURL: string, + nickname: string, + avatarURL?: string, } diff --git a/api-services/types/users/organizations.ts b/api-services/types/users/organizations.ts index 88e05877a..012b524c1 100644 --- a/api-services/types/users/organizations.ts +++ b/api-services/types/users/organizations.ts @@ -4,10 +4,9 @@ export type OrganizationMinimalDTO = { id: string, shortName: string, longName: string, - email: string, - isVerified: boolean, + contactEmail: string, isPersonal: boolean, - avatarURL: string, + avatarURL?: string, } export type OrganizationDTO = OrganizationMinimalDTO & { @@ -18,10 +17,8 @@ export const emptyOrganization: OrganizationDTO = { id: '', shortName: '', longName: '', - email: '', - avatarURL: '', + contactEmail: '', isPersonal: false, - isVerified: false, members: [] } diff --git a/api-services/types/users/user.ts b/api-services/types/users/user.ts new file mode 100644 index 000000000..ded089b9f --- /dev/null +++ b/api-services/types/users/user.ts @@ -0,0 +1,6 @@ +export type User = { + id: string, + name: string, + nickname: string, + avatarUrl?: string, +} 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..2cd67083b 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -169,6 +169,9 @@ importers: '@dnd-kit/core': specifier: 6.3.1 version: 6.3.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@dnd-kit/modifiers': + specifier: 9.0.0 + version: 9.0.0(@dnd-kit/core@6.3.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react@18.3.1) '@dnd-kit/sortable': specifier: 7.0.2 version: 7.0.2(@dnd-kit/core@6.3.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react@18.3.1) @@ -176,8 +179,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.25 + version: 0.1.25(@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 @@ -265,6 +268,12 @@ packages: react: '>=16.8.0' react-dom: '>=16.8.0' + '@dnd-kit/modifiers@9.0.0': + resolution: {integrity: sha512-ybiLc66qRGuZoC20wdSSG6pDXFikui/dCNGthxv4Ndy8ylErY0N3KVxY2bgo7AWwIbxDmXDg3ylAFmnrjcbVvw==} + peerDependencies: + '@dnd-kit/core': ^6.3.0 + react: '>=16.8.0' + '@dnd-kit/sortable@7.0.2': resolution: {integrity: sha512-wDkBHHf9iCi1veM834Gbk1429bd4lHX4RpAwT0y2cHLf246GAvU2sVw/oxWNpPKQNQRQaeGXhAVgrOl1IT+iyA==} peerDependencies: @@ -336,8 +345,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.25': + resolution: {integrity: sha512-3UoSnhCl0yi0K2lst3lOPl0b38R4Mxwdaj3bMmptrJGp3DLauDt94iCWEcoulbcV8O8LsJ1qdtrIOhG1i3400Q==} '@helpwave/proto-ts@0.64.0-89e2023.0': resolution: {integrity: sha512-EqHsZDOttnCBqcAZ0WiRO32rGMICwvGUxvhSltU0KahFsbIx8o5DZ3l9N8GdB3c/66qe5tabTvVKbUzxf66Yjw==} @@ -2568,6 +2577,13 @@ snapshots: react-dom: 18.3.1(react@18.3.1) tslib: 2.8.1 + '@dnd-kit/modifiers@9.0.0(@dnd-kit/core@6.3.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react@18.3.1)': + dependencies: + '@dnd-kit/core': 6.3.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@dnd-kit/utilities': 3.2.2(react@18.3.1) + react: 18.3.1 + tslib: 2.8.1 + '@dnd-kit/sortable@7.0.2(@dnd-kit/core@6.3.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react@18.3.1)': dependencies: '@dnd-kit/core': 6.3.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1) @@ -2685,7 +2701,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.25(@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 (
triggerOnChange({ ...organizationForm.organization }, false, { @@ -223,20 +222,16 @@ export const OrganizationForm = ({ })} onChangeText={text => triggerOnChange({ ...organizationForm.organization, - email: text + contactEmail: text }, false, { ...organizationForm.touched })} onEditCompleted={text => triggerOnChange({ ...organizationForm.organization, - email: text + contactEmail: text }, true, { ...organizationForm.touched, email: true })} maxLength={maxMailLength} className={clsx(inputClasses, { [inputErrorClasses]: isDisplayingEmailNameError })} />
- { - !organizationForm.organization.isVerified && - {translation('notVerified')} - }
{isDisplayingEmailNameError && {emailErrorMessage}} {translation('contactEmailDescription')} diff --git a/tasks/components/OrganizationInvitationList.tsx b/tasks/components/OrganizationInvitationList.tsx index 8d3b33a06..4fe80a235 100644 --- a/tasks/components/OrganizationInvitationList.tsx +++ b/tasks/components/OrganizationInvitationList.tsx @@ -123,7 +123,6 @@ export const OrganizationInvitationList = ({ isLoading={isLoading && !!context.state.organizationId} hasError={isError && !!context.state.organizationId} className="min-h-72" - minimumLoadingDuration={200} > data ?? [], [data]) const usedMembers: OrganizationMember[] = useMemo( () => members ?? membersByOrganization ?? [], [members, membersByOrganization] @@ -112,7 +111,7 @@ export const OrganizationMemberList = ({ const removeMemberMutation = useRemoveMemberMutation(organizationId) const [deleteDialogState, setDeleteDialogState] = useState(defaultDeleteDialogState) const [selectionState, setSelectionState] = useState({}) - const columns = useMemo[]>(() => [ + const columns = useMemo[]>(() => [ { id: 'member', header: translation('member'), @@ -126,9 +125,14 @@ export const OrganizationMemberList = ({ navigator.clipboard.writeText(orgMember.email).catch(console.error) }} > - +
- {orgMember.name} + {orgMember.nickname} {orgMember.email}
@@ -187,17 +191,17 @@ export const OrganizationMemberList = ({ onCancel={() => setDeleteDialogState(defaultDeleteDialogState)} onConfirm={() => { if (deleteDialogState.member) { - if (selectionState[deleteDialogState.member.id]) { + if (selectionState[deleteDialogState.member.userId]) { const newSelection = { ...selectionState } - delete newSelection[deleteDialogState.member.id] + delete newSelection[deleteDialogState.member.userId] setSelectionState(newSelection) } - removeMemberMutation.mutate(deleteDialogState.member.id) + removeMemberMutation.mutate(deleteDialogState.member.userId) } else { Object.keys(selectionState).forEach(value => { - const member = usedMembers.find(member => member.id === value) + const member = usedMembers.find(member => member.userId === value) if (member) { - removeMemberMutation.mutate(member.id) + removeMemberMutation.mutate(member.userId) } }) setSelectionState({}) @@ -210,7 +214,6 @@ export const OrganizationMemberList = ({ hasError={(isError || !data) && !members} isLoading={!members && isLoading} className="min-h-131" - minimumLoadingDuration={200} > = 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/RoomList.tsx b/tasks/components/RoomList.tsx index 494a9b757..3620f0967 100644 --- a/tasks/components/RoomList.tsx +++ b/tasks/components/RoomList.tsx @@ -10,8 +10,8 @@ import { TextButton, useTranslation } from '@helpwave/hightide' -import { useContext, useEffect, useMemo, useState } from 'react' -import type { RoomMinimalDTO } from '@helpwave/api-services/types/tasks/room' +import { useContext, useMemo, useState } from 'react' +import type { RoomOverviewDTO } from '@helpwave/api-services/types/tasks/room' import { useRoomCreateMutation, useRoomDeleteMutation, @@ -91,17 +91,13 @@ const defaultRoomListTranslations: Translation = { } } -export type RoomListRoomRepresentation = RoomMinimalDTO & { - bedCount: number, -} - type DeleteDialogState = { isShowing: boolean, /** If not set, the entire selection will be deleted */ - value?: RoomListRoomRepresentation, + value?: RoomOverviewDTO, } + export type RoomListProps = { - rooms?: RoomListRoomRepresentation[], // TODO replace with more optimized RoomDTO roomsPerPage?: number, } @@ -110,45 +106,29 @@ export type RoomListProps = { */ export const RoomList = ({ overwriteTranslation, - rooms + roomsPerPage = 5 }: PropsForTranslation) => { const translation = useTranslation([defaultRoomListTranslations], overwriteTranslation) const context = useContext(OrganizationOverviewContext) const [selectionState, setSelectionState] = useState({}) - const [usedRooms, setUsedRooms] = useState(rooms ?? []) const [deleteRoomDialogState, setDeleteRoomDialogState] = useState({ isShowing: false }) const [managedRoom, setManagedRoom] = useState() - const creatRoomMutation = useRoomCreateMutation((room) => { - context.updateContext({ ...context.state }) - setUsedRooms(prevState => [...prevState, { ...room, bedCount: 0 }]) - }, context.state.wardId ?? '') // Not good but should be safe most of the time - const deleteRoomMutation = useRoomDeleteMutation(() => { - context.updateContext({ ...context.state }) - }) - const updateRoomMutation = useRoomUpdateMutation(() => context.updateContext({ ...context.state })) + const creatRoomMutation = useRoomCreateMutation() + const deleteRoomMutation = useRoomDeleteMutation() + const updateRoomMutation = useRoomUpdateMutation() - const { data, isError, isLoading } = useRoomOverviewsQuery(context.state.wardId) // TODO use a more light weight query - - useEffect(() => { - if (data) { - setUsedRooms(data.map(room => ({ - id: room.id, - name: room.name, - bedCount: room.beds.length - }))) - } - }, [data]) + const { data: rooms, isError, isLoading } = useRoomOverviewsQuery(context.state.wardId) // TODO use a more light weight query const minRoomNameLength = 1 const maxRoomNameLength = 32 - const columns = useMemo[]>(() => [ + const columns = useMemo[]>(() => [ { id: 'room', header: translation('roomName'), cell: ({ cell }) => { - const room = usedRooms[cell.row.index]! + const room = (rooms ?? [])[cell.row.index]! return ( room.beds.length, sortingFn: 'text', minSize: 190, meta: { @@ -187,7 +167,7 @@ export const RoomList = ({ id: 'manage', header: translation('manageBeds'), cell: ({ cell }) => { - const room = usedRooms[cell.row.index]! + const room = (rooms ?? [])[cell.row.index]! return ( setManagedRoom(room.id)} color="primary"> {translation('manage')} @@ -201,7 +181,7 @@ export const RoomList = ({ id: 'actions', header: '', cell: ({ cell }) => { - const room = usedRooms[cell.row.index]! + const room = (rooms ?? [])[cell.row.index]! return ( setDeleteRoomDialogState({ isShowing: true, value: room })} @@ -215,15 +195,7 @@ export const RoomList = ({ maxSize: 140, enableResizing: false, } - ], [translation, updateRoomMutation, usedRooms]) - - const addRoom = () => { - const newRoom = { - id: '', - name: translation('room') + (usedRooms.length + 1), - } - creatRoomMutation.mutate(newRoom) - } + ], [translation, updateRoomMutation, rooms]) const selectedElementCount = Object.keys(selectionState).length @@ -235,7 +207,7 @@ export const RoomList = ({ descriptionText: translation('dangerZoneText', { count: selectedElementCount }), }} isOpen={deleteRoomDialogState.isShowing} - onCancel={() => setDeleteRoomDialogState({ ...deleteRoomDialogState, isShowing: false })} + onCancel={() => setDeleteRoomDialogState({ isShowing: false })} onConfirm={() => { if (deleteRoomDialogState.value) { if (selectionState[deleteRoomDialogState.value.id]) { @@ -246,13 +218,14 @@ export const RoomList = ({ deleteRoomMutation.mutate(deleteRoomDialogState.value.id) } else { Object.keys(selectionState).forEach(value => { - const room = usedRooms.find(room => room.id === value) + const room = (rooms ?? []).find(room => room.id === value) if (room) { deleteRoomMutation.mutate(room.id) } }) setSelectionState({}) } + setDeleteRoomDialogState({ isShowing: false }) }} confirmType="negative" /> @@ -263,7 +236,7 @@ export const RoomList = ({ onClose={() => setManagedRoom(undefined)} /> {(selectedElementCount > 0) && ( @@ -276,7 +249,18 @@ export const RoomList = ({ {translation('removeSelection')} )} - }> + { + creatRoomMutation.mutate({ + id: '', + name: translation('room') + ' ' + (rooms.length + 1), + wardId: context.state.wardId + }) + }} + color="positive" + size="small" + startIcon={} + > {translation('addRoom')}
@@ -287,19 +271,20 @@ export const RoomList = ({ isLoading={isLoading} hasError={isError} className="min-h-101" - minimumLoadingDuration={200} > - ()} - initialState={{ - pagination: { pageSize: 5 } - }} - /> + {rooms && ( + ()} + initialState={{ + pagination: { pageSize: roomsPerPage } + }} + /> + )}
) diff --git a/tasks/components/RoomOverview.tsx b/tasks/components/RoomOverview.tsx index e8ac5a4fe..503f83e93 100644 --- a/tasks/components/RoomOverview.tsx +++ b/tasks/components/RoomOverview.tsx @@ -1,34 +1,25 @@ import { useContext, useRef } from 'react' import type { RoomOverviewDTO } from '@helpwave/api-services/types/tasks/room' -import type { BedMinimalDTO } from '@helpwave/api-services/types/tasks/bed' -import type { PatientDTO } from '@helpwave/api-services/types/tasks/patient' -import { emptyPatient } from '@helpwave/api-services/types/tasks/patient' +import type { BedWithPatientWithTasksNumberDTO } from '@helpwave/api-services/types/tasks/bed' import { BedCard } from './cards/BedCard' import { PatientCard } from './cards/PatientCard' -import { Droppable, Draggable } from './dnd-kit-instances/patients' +import { WardOverviewDraggable, WardOverviewDroppable } from './dnd-kit-instances/patients' import { DragCard } from './cards/DragCard' import { WardOverviewContext } from '@/pages/ward/[wardId]' +import { noop } from '@helpwave/hightide' export type RoomOverviewProps = { room: RoomOverviewDTO, + onBedClick?: (bed: BedWithPatientWithTasksNumberDTO) => void, } /** * A component to show all beds and patients within a room in a ward */ -export const RoomOverview = ({ room }: RoomOverviewProps) => { +export const RoomOverview = ({ room, onBedClick = noop }: RoomOverviewProps) => { const context = useContext(WardOverviewContext) const ref = useRef(null) - const setSelectedBed = (room: RoomOverviewDTO, bed: BedMinimalDTO, patientId?: string, patient?: PatientDTO) => - context.updateContext({ - ...context.state, - roomId: room.id, - bedId: bed.id, - patientId, - patient - }) - const selectedBedId = context.state.bedId return ( @@ -38,67 +29,79 @@ export const RoomOverview = ({ room }: RoomOverviewProps) => { {room.name}
- {room.beds.map((bed, index) => bed.patient && bed.patient?.id ? - ( - - {({ isOver }) => bed.patient && bed.patient?.id && ( - - - {() => bed.patient && bed.patient?.id && ( - { - event.stopPropagation() - if (bed.patient) { - setSelectedBed(room, bed, bed.patient.id) - } - }} - isSelected={selectedBedId === bed.id} - cardDragProperties={{ isOver, isDangerous: true }} - /> - )} - - - )} - - ) : ( - // Maybe also wrap inside the drag and drop later - - {({ isOver, active }) => { - const source = active?.data.current - - if (isOver) { - return ( - - ) - } else { - return ( - { - event.stopPropagation() - setSelectedBed(room, bed, undefined, { - ...emptyPatient, - id: '', - name: `Patient ${room?.beds.findIndex(bedOfRoom => bedOfRoom.id === bed.id) ?? 1}` - }) + {room.beds.map((bed, index) => { + const patient = bed.patient + if (patient) { + return ( + + {({ active, isOver }) => { + const activeData = active?.data?.current + const isOriginalBed = activeData?.patient?.id === patient.id + return ( + + + {() => { + if(isOriginalBed && !isOver) { + return ( + + ) + } + return ( + { + onBedClick(bed) + }} + isSelected={selectedBedId === bed.id} + cardDragProperties={{ isOver, isDangerous: !isOriginalBed }} + /> + ) }} - isSelected={selectedBedId === bed.id} - /> - ) - } + + + ) }} - - ))} + + ) + } + return ( + + {({ isOver, active }) => { + const activeData = active?.data.current + + if (isOver && activeData) { + return ( + + ) + } else { + return ( + onBedClick(bed)} + isSelected={selectedBedId === bed.id} + /> + ) + } + }} + + ) + })}
) diff --git a/tasks/components/SubtaskTile.tsx b/tasks/components/SubtaskTile.tsx index edc51d8e5..bff5f101c 100644 --- a/tasks/components/SubtaskTile.tsx +++ b/tasks/components/SubtaskTile.tsx @@ -1,6 +1,6 @@ import { useEffect, useState } from 'react' import { Checkbox, type PropsForTranslation, TextButton, ToggleableInput, useTranslation } from '@helpwave/hightide' -import type { SubTaskDTO } from '@helpwave/api-services/types/tasks/task' +import type { SubtaskDTO } from '@helpwave/api-services/types/tasks/task' type SubtaskTileTranslation = { subtasks: string, @@ -19,9 +19,9 @@ const defaultSubtaskTileTranslation = { } type SubtaskTileProps = { - subtask: SubTaskDTO, + subtask: SubtaskDTO, onRemoveClick: () => void, - onChange: (subtask: SubTaskDTO) => void, + onChange: (subtask: SubtaskDTO) => void, } /** @@ -37,7 +37,7 @@ export const SubtaskTile = ({ const minTaskNameLength = 2 const maxTaskNameLength = 64 - const [localSubtask, setLocalSubtask] = useState({ ...subtask }) + const [localSubtask, setLocalSubtask] = useState({ ...subtask }) useEffect(() => { setLocalSubtask(subtask) @@ -47,7 +47,7 @@ export const SubtaskTile = ({
{ - const newSubtask: SubTaskDTO = { + const newSubtask: SubtaskDTO = { ...localSubtask, isDone } @@ -64,7 +64,7 @@ export const SubtaskTile = ({ name })} onEditCompleted={name => { - const newSubtask: SubTaskDTO = { + const newSubtask: SubtaskDTO = { ...localSubtask, name } @@ -80,6 +80,7 @@ export const SubtaskTile = ({ onClick={onRemoveClick} aria-label={translation('remove')} color="negative" + size="small" > {translation('remove')} diff --git a/tasks/components/SubtaskView.tsx b/tasks/components/SubtaskView.tsx index 3e46d5a71..3c513bd56 100644 --- a/tasks/components/SubtaskView.tsx +++ b/tasks/components/SubtaskView.tsx @@ -4,12 +4,7 @@ import { Plus } from 'lucide-react' import type { Translation } from '@helpwave/hightide' import { useTranslation, type PropsForTranslation } from '@helpwave/hightide' import { SolidButton } from '@helpwave/hightide' -import type { SubTaskDTO } from '@helpwave/api-services/types/tasks/task' -import { - useSubTaskAddMutation, - useSubTaskDeleteMutation, - useSubTaskUpdateMutation -} from '@helpwave/api-services/mutations/tasks/task_mutations' +import type { SubtaskDTO } from '@helpwave/api-services/types/tasks/task' import { SubtaskTile } from './SubtaskTile' import { TaskTemplateContext } from '@/pages/templates' @@ -36,12 +31,27 @@ const defaultSubtaskViewTranslation: Translation = { } type SubtaskViewProps = { - // TODO: This component should not decide between two mutate functions. Pass mutate function instead. - subtasks: SubTaskDTO[], - taskId?: string, + subtasks: SubtaskDTO[], + taskOrTemplateId?: string, createdBy?: string, - taskTemplateId?: string, - onChange: (subtasks: SubTaskDTO[]) => void, + /** + * Gives the full list with updates + * + * New items are only created locally + */ + onChange: (subtasks: SubtaskDTO[]) => void, + /** + * Callback when an item is added and not creating + */ + onAdd: (subtask: SubtaskDTO) => void, + /** + * Callback when an item is updated and not creating + */ + onUpdate: (subtask: SubtaskDTO) => void, + /** + * Callback when an item is removed and not creating + */ + onRemove: (subtask: SubtaskDTO) => void, } /** @@ -50,16 +60,16 @@ type SubtaskViewProps = { export const SubtaskView = ({ overwriteTranslation, subtasks, - taskId, + taskOrTemplateId, onChange, + onAdd, + onUpdate, + onRemove, }: PropsForTranslation) => { const context = useContext(TaskTemplateContext) const translation = useTranslation([defaultSubtaskViewTranslation], overwriteTranslation) - const isCreatingTask = taskId === '' - const addSubtaskMutation = useSubTaskAddMutation(taskId) - const deleteSubtaskMutation = useSubTaskDeleteMutation() - const updateSubtaskMutation = useSubTaskUpdateMutation(taskId) + const isCreating = taskOrTemplateId === '' const scrollableRef = useRef(null) const [scrollToBottomFlag, setScrollToBottom] = useState(false) @@ -72,10 +82,9 @@ export const SubtaskView = ({ setScrollToBottom(false) }, [scrollToBottomFlag, subtasks]) - const removeSubtask = (subtask: SubTaskDTO, index: number) => { + const removeSubtask = (subtask: SubtaskDTO, index: number) => { const filteredSubtasks = subtasks.filter((_, subtaskIndex) => subtaskIndex !== index) - // undefined because taskId === "" would mean task creation // TODO: this seems like a terrible way of differentiating things.. - if (taskId === undefined) { + if (isCreating) { context.updateContext({ ...context.state, template: { ...context.state.template, subtasks: filteredSubtasks }, @@ -84,7 +93,7 @@ export const SubtaskView = ({ }) } else { onChange(filteredSubtasks) - deleteSubtaskMutation.mutate(subtask.id) + onRemove(subtask) } } @@ -94,13 +103,14 @@ 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: taskOrTemplateId ?? '' } onChange([...subtasks, newSubtask]) - if (!isCreatingTask) { - addSubtaskMutation.mutate(newSubtask) + if (!isCreating) { + onAdd(newSubtask) } setScrollToBottom(true) }} + size="small" >
@@ -119,7 +129,7 @@ export const SubtaskView = ({ const newSubtasks = [...subtasks] newSubtasks[index] = newSubtask if (subtask.id) { - updateSubtaskMutation.mutate(newSubtask) + onUpdate(newSubtask) } onChange(newSubtasks) }} diff --git a/tasks/components/TaskTemplateListColumn.tsx b/tasks/components/TaskTemplateListColumn.tsx index 1b3924874..d4b391af3 100644 --- a/tasks/components/TaskTemplateListColumn.tsx +++ b/tasks/components/TaskTemplateListColumn.tsx @@ -49,9 +49,9 @@ export const TaskTemplateListColumn = ({ }, [ref.current?.clientHeight]) return ( -
-
- +
+
+ {translation('template')} {onColumnEditClick && } @@ -59,14 +59,14 @@ export const TaskTemplateListColumn = ({
-
+
{templates.map((taskTemplateExtension) => ( onTileClick(taskTemplateExtension.taskTemplate)} - className="min-h-auto" + className="min-h-auto px-2 py-1" isSelected={activeId === taskTemplateExtension.taskTemplate.id} typeForLabel={taskTemplateExtension.type} /> diff --git a/tasks/components/TaskTemplateWardPreview.tsx b/tasks/components/TaskTemplateWardPreview.tsx index 09316685f..95612b91c 100644 --- a/tasks/components/TaskTemplateWardPreview.tsx +++ b/tasks/components/TaskTemplateWardPreview.tsx @@ -53,7 +53,6 @@ export const TaskTemplateWardPreview = ({ isLoading={isLoading || !context.state.wardId} hasError={isError} className="min-h-27" - 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/WardForm.tsx b/tasks/components/WardForm.tsx index c324fc0d7..fe3cec94e 100644 --- a/tasks/components/WardForm.tsx +++ b/tasks/components/WardForm.tsx @@ -60,7 +60,7 @@ export const WardForm = ({ const maxWardNameLength = 32 const inputErrorClasses = 'border-negative focus:border-negative focus:ring-negative border-2' - const inputClasses = 'mt-1 block rounded-md w-full border-gray-300 shadow-sm focus:outline-none focus:border-primary focus:ring-primary' + const inputClasses = 'mt-1 block rounded-md w-full' function validateName(ward: WardFormInfoDTO) { const wardName = ward.name.trim() diff --git a/tasks/components/cards/AddCard.tsx b/tasks/components/cards/AddCard.tsx index df3f1049a..63e8fc341 100644 --- a/tasks/components/cards/AddCard.tsx +++ b/tasks/components/cards/AddCard.tsx @@ -21,7 +21,7 @@ export const AddCard = ({
- {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 (
) => { + overwriteTranslation, + organization, + ...editCardProps + }: PropsForTranslation) => { const translation = useTranslation([defaultOrganizationCardTranslation], overwriteTranslation) const organizationMemberCount = organization.members.length @@ -49,13 +48,19 @@ export const OrganizationCard = ({
- {organization.email} + {organization.contactEmail}
{`${organizationMemberCount} ${organizationMemberCount > 1 ? translation('members') : translation('member')}`}
- ({ avatarUrl: user.avatarURL, alt: user.name }))}/> + ({ + image: user.avatarURL ? { avatarUrl: user.avatarURL, alt: user.nickname } : undefined, + name: user.nickname, + }))} + fullyRounded={true} + />
diff --git a/tasks/components/cards/PatientCard.tsx b/tasks/components/cards/PatientCard.tsx index 8676c3520..bd46b078b 100644 --- a/tasks/components/cards/PatientCard.tsx +++ b/tasks/components/cards/PatientCard.tsx @@ -1,6 +1,5 @@ - import type { Translation } from '@helpwave/hightide' -import { useTranslation, type PropsForTranslation } from '@helpwave/hightide' +import { type PropsForTranslation, useTranslation } from '@helpwave/hightide' import { PillLabelsColumn } from '../PillLabel' import { DragCard, type DragCardProps } from './DragCard' import clsx from 'clsx' @@ -18,43 +17,47 @@ const defaultPatientCardTranslations: Translation = { } } +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..4f4e92e79 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 ? + () : + () + } + 0 ? progress : 1}/> +
) } diff --git a/tasks/components/cards/TaskTemplateCard.tsx b/tasks/components/cards/TaskTemplateCard.tsx index 5562554a5..9110ddd8b 100644 --- a/tasks/components/cards/TaskTemplateCard.tsx +++ b/tasks/components/cards/TaskTemplateCard.tsx @@ -49,18 +49,19 @@ export const TaskTemplateCard = ({ {...editCardProps} >
-
-

{name}

- {typeForLabel && ( - - {typeForLabel === 'ward' ? translation('ward') : translation('personal')} - - )} -
- {subtaskCount + ' ' + translation('subtask')} +
+

{name}

+ {typeForLabel && ( + + {typeForLabel === 'ward' ? translation('ward') : translation('personal')} + + )} +
+ {subtaskCount + ' ' + translation('subtask')}
) 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/cards/WardCard.tsx b/tasks/components/cards/WardCard.tsx index 7d9ee2ed2..07b80c92c 100644 --- a/tasks/components/cards/WardCard.tsx +++ b/tasks/components/cards/WardCard.tsx @@ -18,9 +18,9 @@ export const WardCard = ({ }: WardCardProps) => { return ( -
+
- {ward.name} + {ward.name}
diff --git a/tasks/components/dnd-kit-instances/patients.ts b/tasks/components/dnd-kit-instances/patients.ts index c41c62b6c..ad17320a7 100644 --- a/tasks/components/dnd-kit-instances/patients.ts +++ b/tasks/components/dnd-kit-instances/patients.ts @@ -1,4 +1,4 @@ -import type { PatientMinimalDTO } from '@helpwave/api-services/types/tasks/patient' +import type { PatientMinimalDTO, PatientWithTasksNumberDTO } from '@helpwave/api-services/types/tasks/patient' import type { BedWithPatientWithTasksNumberDTO } from '@helpwave/api-services/types/tasks/bed' import type { RoomOverviewDTO } from '@helpwave/api-services/types/tasks/room' import type { @@ -12,30 +12,33 @@ import { DndContext as DndContextFree } from '@/components/dnd-kit/typesafety' import { Droppable as DroppableFree } from '@/components/dnd-kit/Droppable' import { Draggable as DraggableFree } from '@/components/dnd-kit/Draggable' -type DraggableData = { - // patient from 'patient list' +export type WardOverviewDraggableData = { + type: 'patientListItem', patient: PatientMinimalDTO, discharged: boolean, + assigned: boolean, } | { - // patient from 'room overview' + type: 'assignedPatient', bed: BedWithPatientWithTasksNumberDTO, room: RoomOverviewDTO, - patient: PatientMinimalDTO, + patient: PatientWithTasksNumberDTO, } -type DroppableData = { +export type WardOverViewDroppableData = { + type: 'bed', bed: BedWithPatientWithTasksNumberDTO, room: RoomOverviewDTO, - patient: PatientMinimalDTO | undefined, + patient?: PatientMinimalDTO, } | { - patientListSection: 'unassigned' | 'discharged', + type: 'section', + section: 'unassigned' | 'discharged', } -export const Droppable = DroppableFree -export const Draggable = DraggableFree -export const DndContext = DndContextFree +export const WardOverviewDroppable = DroppableFree +export const WardOverviewDraggable = DraggableFree +export const WardOverviewDndContext = DndContextFree -export type DragStartEvent = DragStartEventFree -export type DragMoveEvent = DragMoveEventFree -export type DragOverEvent = DragOverEventFree -export type DragEndEvent = DragEndEventFree -export type DragCancelEvent = DragCancelEventFree +export type WardOverviewDragStartEvent = DragStartEventFree +export type WardOverviewDragMoveEvent = DragMoveEventFree +export type WardOverviewDragOverEvent = DragOverEventFree +export type WardOverviewDragEndEvent = DragEndEventFree +export type WardOverviewDragCancelEvent = DragCancelEventFree diff --git a/tasks/components/layout/DashboardDisplay.tsx b/tasks/components/layout/DashboardDisplay.tsx index 8da9a2718..d4557ebe9 100644 --- a/tasks/components/layout/DashboardDisplay.tsx +++ b/tasks/components/layout/DashboardDisplay.tsx @@ -1,6 +1,11 @@ import clsx from 'clsx' import type { Translation } from '@helpwave/hightide' -import { LoadingAndErrorComponent, type PropsForTranslation, useTranslation } from '@helpwave/hightide' +import { + LoadingAndErrorComponent, + type PropsForTranslation, + TranslationPluralCount, + useTranslation +} from '@helpwave/hightide' import { useRouter } from 'next/router' import { useWardOverviewsQuery } from '@helpwave/api-services/mutations/tasks/ward_mutations' import { useRecentPatientsQuery } from '@helpwave/api-services/mutations/tasks/patient_mutations' @@ -10,32 +15,39 @@ 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 { TaskDetailModal } from '@/components/modals/TaskDetailModal' +import type { MedicalTranslationType } from '@/translation/medical' +import { medicalTranslation } from '@/translation/medical' type DashboardDisplayTranslation = { - patients: string, - organizations: string, - wards: string, recent: string, showAllOrganizations: string, addWard: string, + myTasks: string, + noTasks: string, } +type TranslationType = MedicalTranslationType & DashboardDisplayTranslation + const defaultDashboardDisplayTranslations: Translation = { en: { - patients: 'Patients', - organizations: 'Organizations', - wards: 'Wards', recent: 'Recently used', showAllOrganizations: 'Show all organizations', addWard: 'Add Ward', + myTasks: 'My Tasks', + noTasks: 'No tasks' }, de: { - patients: 'Patienten', - organizations: 'Organisationen', - wards: 'Stationen', recent: 'Kürzlich Benutzt', showAllOrganizations: 'Alle Organisationen anzeigen', addWard: 'Station hinzufügen', + myTasks: 'Meine Aufgaben', + noTasks: 'Keine Aufgaben', } } @@ -46,10 +58,11 @@ export type DashboardDisplayProps = Record */ export const DashboardDisplay = ({ overwriteTranslation, - }: PropsForTranslation) => { - const translation = useTranslation([defaultDashboardDisplayTranslations], overwriteTranslation) + }: PropsForTranslation) => { + const translation = useTranslation([medicalTranslation, defaultDashboardDisplayTranslations], overwriteTranslation) const { organization } = useAuth() const router = useRouter() + const [taskDetailModalId, setTaskDetailModalId] = useState() // TODO replace with recent wards later const { @@ -60,49 +73,163 @@ export const DashboardDisplay = ({ data: patients, isLoading: isLoadingPatients } = useRecentPatientsQuery() + const { + data: tasks, + isLoading: isTasksLoading + } = useMyTasksQuery() + + const todo = useMemo(() => { + return (tasks ?? []).filter(value => value.status === 'todo') + }, [tasks]) + const inProgress = useMemo(() => { + return (tasks ?? []).filter(value => value.status === 'inProgress') + }, [tasks]) + const done = useMemo(() => { + return (tasks ?? []).filter(value => value.status === 'done') + }, [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} - /> - ))} +
+ +
+ +
+ {todo.length > 0 && ( + +
+ {todo.map(task => ( + { + setTaskDetailModalId(task.id) + }} + isSelected={false} + showStatus={true} + className={cardWidth} + /> + ))} +
+
+ )} + {inProgress.length > 0 && ( + +
+ {inProgress.map(task => ( + { + setTaskDetailModalId(task.id) + }} + isSelected={false} + showStatus={true} + className={cardWidth} + /> + ))} +
+
+ )} + {done.length > 0 && ( + +
+ {done.map(task => ( + { + setTaskDetailModalId(task.id) + }} + isSelected={false} + showStatus={true} + className={cardWidth} + /> + ))} +
+
+ )}
- - )} - - -
- -
- {wards && wards.length > 0 && wards?.map(ward => ( - router.push(`/ward/${ward.id}`)} - /> - ))} - router.push(`/organizations/${organization?.id}`)} - />
-
-
+ + +
+ + +
+ {wards && wards.length > 0 && wards?.map(ward => ( + router.push(`/ward/${ward.id}`)} + className={cardWidth} + /> + ))} + router.push(`/organizations/${organization?.id}`)} + className={cardWidth} + /> +
+
+
+
+ + {patients && patients.length > 0 && ( +
+ + +
+
+ {patients?.filter((_, index) => index % 2 == 0).map(patient => ( + { + if (patient.wardId) { + router.push(`/ward/${patient.wardId}?patientId=${patient.id}`).catch(console.error) + } else if (wards && wards.length > 0) { + router.push(`/ward/${wards[0]!.id}?patientId=${patient.id}`).catch(console.error) + } + }} + /> + ))} +
+
+ {patients?.filter((_, index) => index % 2 == 1).map(patient => ( + { + if (patient.wardId) { + router.push(`/ward/${patient.wardId}?patientId=${patient.id}`).catch(console.error) + } else if (wards && wards.length > 0) { + router.push(`/ward/${wards[0]!.id}?patientId=${patient.id}`).catch(console.error) + } + }} + /> + ))} +
+
+
+
+ )} +
+
) } diff --git a/tasks/components/layout/PatientDetails.tsx b/tasks/components/layout/PatientDetails.tsx index b06a2413d..3b48dad4c 100644 --- a/tasks/components/layout/PatientDetails.tsx +++ b/tasks/components/layout/PatientDetails.tsx @@ -12,12 +12,12 @@ import { } from '@helpwave/hightide' import type { TaskStatus } from '@helpwave/api-services/types/tasks/task' import { - useAssignBedMutation, + usePatientAssignToBedMutation, usePatientDetailsQuery, usePatientDischargeMutation, usePatientUpdateMutation, - useReadmitPatientMutation, - useUnassignMutation + usePatientReadmitMutation, + usePatientUnassignMutation } from '@helpwave/api-services/mutations/tasks/patient_mutations' import type { PatientDetailsDTO } from '@helpwave/api-services/types/tasks/patient' import { emptyPatientDetails } from '@helpwave/api-services/types/tasks/patient' @@ -65,7 +65,6 @@ const defaultPatientDetailTranslations: Translation = export type PatientDetailProps = { wardId: string, - patient?: PatientDetailsDTO, width?: number, } @@ -75,7 +74,6 @@ export type PatientDetailProps = { export const PatientDetail = ({ overwriteTranslation, wardId, - patient = emptyPatientDetails }: PropsForTranslation) => { const [isShowingDischargeDialog, setIsShowingDischargeDialog] = useState(false) const translation = useTranslation([defaultPatientDetailTranslations], overwriteTranslation) @@ -83,12 +81,12 @@ export const PatientDetail = ({ const context = useContext(WardOverviewContext) const updateMutation = usePatientUpdateMutation() - const readmitMutation = useReadmitPatientMutation() + const readmitMutation = usePatientReadmitMutation() const dischargeMutation = usePatientDischargeMutation({ onSuccess: () => context.updateContext({ wardId: context.state.wardId }) }) - const unassignMutation = useUnassignMutation({ + const unassignMutation = usePatientUnassignMutation({ onSuccess: () => context.updateContext({ wardId: context.state.wardId }) }) @@ -98,7 +96,7 @@ export const PatientDetail = ({ isLoading } = usePatientDetailsQuery(context.state.patientId) - const [newPatient, setNewPatient] = useState(patient) + const [newPatient, setNewPatient] = useState(emptyPatientDetails) const [taskId, setTaskId] = useState() const [isShowingSavedNotification, setIsShowingSavedNotification] = useState(false) const [initialTaskStatus, setInitialTaskStatus] = useState() @@ -112,7 +110,7 @@ export const PatientDetail = ({ }, [data]) const [isSubmitting, setIsSubmitting] = useState(false) - const assignBedMutation = useAssignBedMutation({ + const assignBedMutation = usePatientAssignToBedMutation({ onSuccess: () => { setIsSubmitting(false) } @@ -149,10 +147,12 @@ export const PatientDetail = ({ setTaskId(undefined)} - wardId={wardId} + createInformation={{ + wardId, + patientId: newPatient.id, + initialStatus: initialTaskStatus ?? 'todo', + }} taskId={taskId} - patientId={newPatient.id} - initialStatus={initialTaskStatus} /> changeSavedValue({ ...newPatient, - name + humanReadableIdentifier: name })} />
{ if (roomBedDropdownIds.bedId && context.state.patientId) { context.updateContext({ ...context.state, ...roomBedDropdownIds }) assignBedMutation.mutate({ - id: roomBedDropdownIds.bedId, + bedId: roomBedDropdownIds.bedId, patientId: context.state.patientId }) } @@ -203,10 +203,10 @@ export const PatientDetail = ({