From 747cb40e7e241a129154b9f7c0aa49ed788421d9 Mon Sep 17 00:00:00 2001 From: DasProffi <67233923+DasProffi@users.noreply.github.com> Date: Sat, 14 Mar 2026 13:24:36 +0100 Subject: [PATCH 01/20] feat: update to hightide 0.9.0 --- web/components/patients/PatientDataEditor.tsx | 4 +- .../properties/PropertyDetailView.tsx | 8 +- web/components/properties/PropertyEntry.tsx | 14 +- web/components/tables/TaskList.tsx | 6 +- web/components/tasks/TaskDataEditor.tsx | 8 +- web/hooks/useTableState.ts | 67 +- web/package-lock.json | 1426 ++--------------- web/package.json | 4 +- web/pages/settings/index.tsx | 20 +- web/utils/propertyFilterMapping.ts | 4 +- web/utils/tableStateToApi.ts | 188 ++- 11 files changed, 319 insertions(+), 1430 deletions(-) diff --git a/web/components/patients/PatientDataEditor.tsx b/web/components/patients/PatientDataEditor.tsx index 3d93665..f91d059 100644 --- a/web/components/patients/PatientDataEditor.tsx +++ b/web/components/patients/PatientDataEditor.tsx @@ -344,9 +344,7 @@ export const PatientDataEditor = ({ {...interactionStates} > {sexOptions.map(option => ( - - {option.label} - + ))} )} diff --git a/web/components/properties/PropertyDetailView.tsx b/web/components/properties/PropertyDetailView.tsx index 54b3417..6977ffe 100644 --- a/web/components/properties/PropertyDetailView.tsx +++ b/web/components/properties/PropertyDetailView.tsx @@ -249,9 +249,7 @@ export const PropertyDetailView = ({ }} > {propertySubjectTypeList.map(v => ( - - {translation('sPropertySubjectType', { subject: v })} - + ))} )} @@ -276,9 +274,7 @@ export const PropertyDetailView = ({ }} > {propertyFieldTypeList.map(v => ( - - {translation('sPropertyType', { type: v })} - + ))} )} diff --git a/web/components/properties/PropertyEntry.tsx b/web/components/properties/PropertyEntry.tsx index 5c2f605..537d338 100644 --- a/web/components/properties/PropertyEntry.tsx +++ b/web/components/properties/PropertyEntry.tsx @@ -108,11 +108,8 @@ export const PropertyEntry = ({ onEditComplete={singleSelectValue => onEditComplete({ ...value, singleSelectValue })} > {selectData?.options.map(option => ( - - {option.name} - - )) - } + + ))} ) case 'multiSelect': @@ -124,11 +121,8 @@ export const PropertyEntry = ({ onEditComplete={multiSelectValue => onEditComplete({ ...value, multiSelectValue })} > {selectData?.options.map(option => ( - - {option.name} - - )) - } + + ))} ) case 'user': diff --git a/web/components/tables/TaskList.tsx b/web/components/tables/TaskList.tsx index 72945de..f5ba624 100644 --- a/web/components/tables/TaskList.tsx +++ b/web/components/tables/TaskList.tsx @@ -586,9 +586,9 @@ export const TaskList = forwardRef(({ tasks: initial buttonProps={{ className: 'min-w-32' }} contentPanelProps={{ className: 'min-w-32' }} > - {translation('filterAll') || 'All'} - {translation('filterUndone') || 'Undone'} - {translation('done')} + + + {headerActions} {canHandover && ( diff --git a/web/components/tasks/TaskDataEditor.tsx b/web/components/tasks/TaskDataEditor.tsx index a657905..ae96d02 100644 --- a/web/components/tasks/TaskDataEditor.tsx +++ b/web/components/tasks/TaskDataEditor.tsx @@ -273,9 +273,7 @@ export const TaskDataEditor = ({ > {patients.map(patient => { return ( - - {patient.name} - + ) })} @@ -362,9 +360,9 @@ export const TaskDataEditor = ({ dataProps.onEditComplete?.(priority) }} > - {translation('priorityNone')} + {priorities.map(({ value, label }) => ( - +
{label} diff --git a/web/hooks/useTableState.ts b/web/hooks/useTableState.ts index 9acb60d..907561b 100644 --- a/web/hooks/useTableState.ts +++ b/web/hooks/useTableState.ts @@ -1,11 +1,12 @@ import type { + ColumnFilter, ColumnFiltersState, PaginationState, SortingState, VisibilityState } from '@tanstack/react-table' import type { Dispatch, SetStateAction } from 'react' -import type { DateFilterParameter, DatetimeFilterParameter, TableFilterValue } from '@helpwave/hightide' +import type { DataType, FilterOperator, FilterParameter, FilterValue } from '@helpwave/hightide' import { useStorage } from '@/hooks/useStorage' const defaultPagination: PaginationState = { @@ -59,24 +60,13 @@ export function useStorageSyncedTableState( defaultValue: initialFilters, serialize: (value) => { const mappedColumnFilter = value.map((filter) => { - const tableFilterValue = filter.value as TableFilterValue - let parameter: Record = tableFilterValue.parameter - if(tableFilterValue.operator.startsWith('dateTime')) { - const dateTimeParameter: DatetimeFilterParameter = parameter as DatetimeFilterParameter - parameter = { - ...parameter, - compareDatetime: dateTimeParameter.compareDatetime ? dateTimeParameter.compareDatetime.toISOString() : undefined, - min: dateTimeParameter.min ? dateTimeParameter.min.toISOString() : undefined, - max: dateTimeParameter.max ? dateTimeParameter.max.toISOString() : undefined, - } - } else if(tableFilterValue.operator.startsWith('date')) { - const dateParameter: DateFilterParameter = parameter as DateFilterParameter - parameter = { - ...parameter, - compareDate: dateParameter.compareDate ? dateParameter.compareDate.toISOString() : undefined, - min: dateParameter.min ? dateParameter.min.toISOString() : undefined, - max: dateParameter.max ? dateParameter.max.toISOString() : undefined, - } + const tableFilterValue = filter.value as FilterValue + const filterParameter = tableFilterValue.parameter + const parameter: Record = { + ...filterParameter, + compareDate: filterParameter.compareDate ? filterParameter.compareDate.toISOString() : undefined, + minDate: filterParameter.minDate ? filterParameter.minDate.toISOString() : undefined, + maxDate: filterParameter.maxDate ? filterParameter.maxDate.toISOString() : undefined, } return { ...filter, @@ -91,34 +81,25 @@ export function useStorageSyncedTableState( }, deserialize: (value) => { const mappedColumnFilter = JSON.parse(value) as Record[] - return mappedColumnFilter.map((filter) => { - const filterValue = filter['value'] as Record - const operator: string = filterValue['operator'] as string - let parameter: Record = filterValue['parameter'] as Record - if(operator.startsWith('dateTime')) { - parameter = { - ...parameter, - compareDatetime: parameter['compareDatetime'] ? new Date(parameter['compareDatetime'] as string) : undefined, - min: parameter['min'] ? new Date(parameter['min'] as string) : undefined, - max: parameter['max'] ? new Date(parameter['max'] as string) : undefined, - } + return mappedColumnFilter.map((filter): ColumnFilter => { + const value = filter['value'] as Record + const parameter: Record = value['parameter'] as Record + const filterParameter: FilterParameter = { + ...parameter, + compareDate: parameter['compareDate'] ? new Date(parameter['compareDate'] as string) : undefined, + minDate: parameter['minDate'] ? new Date(parameter['minDate'] as string) : undefined, + maxDate: parameter['maxDate'] ? new Date(parameter['maxDate'] as string) : undefined, } - else if(operator.startsWith('date')) { - parameter = { - ...parameter, - compareDate: parameter['compareDate'] ? new Date(parameter['compareDate'] as string) : undefined, - min: parameter['min'] ? new Date(parameter['min'] as string) : undefined, - max: parameter['max'] ? new Date(parameter['max'] as string) : undefined, - } + const mappedValue: FilterValue = { + operator: value['operator'] as FilterOperator, + dataType: value['dataType'] as DataType, + parameter: filterParameter, } return { ...filter, - value: { - ...filterValue, - parameter, - }, - } - }) as unknown as ColumnFiltersState + value: mappedValue, + } as ColumnFilter + }) }, }) const { value: columnVisibility, setValue: setColumnVisibility } = useStorage({ diff --git a/web/package-lock.json b/web/package-lock.json index d87cd0e..e578d61 100644 --- a/web/package-lock.json +++ b/web/package-lock.json @@ -12,7 +12,7 @@ "@dnd-kit/core": "6.3.1", "@dnd-kit/modifiers": "9.0.0", "@dnd-kit/sortable": "7.0.2", - "@helpwave/hightide": "0.8.12", + "@helpwave/hightide": "0.9.0", "@helpwave/internationalization": "0.4.0", "@tailwindcss/postcss": "4.1.3", "@tanstack/react-query": "5.90.16", @@ -40,7 +40,7 @@ "@graphql-codegen/client-preset": "5.2.1", "@graphql-codegen/typescript": "5.0.6", "@graphql-codegen/typescript-operations": "5.0.6", - "@graphql-codegen/typescript-react-query": "6.1.1", + "@graphql-codegen/typescript-react-query": "7.0.0", "@helpwave/eslint-config": "0.0.11", "@playwright/test": "1.57.0", "@types/node": "20.17.10", @@ -108,25 +108,15 @@ } }, "node_modules/@ardatan/relay-compiler": { - "version": "12.0.3", - "resolved": "https://registry.npmjs.org/@ardatan/relay-compiler/-/relay-compiler-12.0.3.tgz", - "integrity": "sha512-mBDFOGvAoVlWaWqs3hm1AciGHSQE1rqFc/liZTyYz/Oek9yZdT5H26pH2zAFuEiTiBVPPyMuqf5VjOFPI2DGsQ==", + "version": "13.0.0", + "resolved": "https://registry.npmjs.org/@ardatan/relay-compiler/-/relay-compiler-13.0.0.tgz", + "integrity": "sha512-ite4+xng5McO8MflWCi0un0YmnorTujsDnfPfhzYzAgoJ+jkI1pZj6jtmTl8Jptyi1H+Pa0zlatJIsxDD++ETA==", "dev": true, "license": "MIT", "dependencies": { - "@babel/generator": "^7.26.10", - "@babel/parser": "^7.26.10", "@babel/runtime": "^7.26.10", - "chalk": "^4.0.0", - "fb-watchman": "^2.0.0", - "immutable": "~3.7.6", - "invariant": "^2.2.4", - "nullthrows": "^1.1.1", - "relay-runtime": "12.0.0", - "signedsource": "^1.0.0" - }, - "bin": { - "relay-compiler": "bin/relay-compiler" + "immutable": "^5.1.5", + "invariant": "^2.2.4" }, "peerDependencies": { "graphql": "*" @@ -205,19 +195,6 @@ "node": ">=6.9.0" } }, - "node_modules/@babel/helper-annotate-as-pure": { - "version": "7.27.3", - "resolved": "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.27.3.tgz", - "integrity": "sha512-fXSwMQqitTGeHLBC08Eq5yXz2m37E4pJX1qAU1+2cNedz/ifv/bVXft90VeSav5nFO61EcNgwr0aJxbyPaWBPg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/types": "^7.27.3" - }, - "engines": { - "node": ">=6.9.0" - } - }, "node_modules/@babel/helper-compilation-targets": { "version": "7.28.6", "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.28.6.tgz", @@ -235,28 +212,6 @@ "node": ">=6.9.0" } }, - "node_modules/@babel/helper-create-class-features-plugin": { - "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.28.6.tgz", - "integrity": "sha512-dTOdvsjnG3xNT9Y0AUg1wAl38y+4Rl4sf9caSQZOXdNqVn+H+HbbJ4IyyHaIqNR6SW9oJpA/RuRjsjCw2IdIow==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-annotate-as-pure": "^7.27.3", - "@babel/helper-member-expression-to-functions": "^7.28.5", - "@babel/helper-optimise-call-expression": "^7.27.1", - "@babel/helper-replace-supers": "^7.28.6", - "@babel/helper-skip-transparent-expression-wrappers": "^7.27.1", - "@babel/traverse": "^7.28.6", - "semver": "^6.3.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0" - } - }, "node_modules/@babel/helper-globals": { "version": "7.28.0", "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.28.0.tgz", @@ -267,20 +222,6 @@ "node": ">=6.9.0" } }, - "node_modules/@babel/helper-member-expression-to-functions": { - "version": "7.28.5", - "resolved": "https://registry.npmjs.org/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.28.5.tgz", - "integrity": "sha512-cwM7SBRZcPCLgl8a7cY0soT1SptSzAlMH39vwiRpOQkJlh53r5hdHwLSCZpQdVLT39sZt+CRpNwYG4Y2v77atg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/traverse": "^7.28.5", - "@babel/types": "^7.28.5" - }, - "engines": { - "node": ">=6.9.0" - } - }, "node_modules/@babel/helper-module-imports": { "version": "7.28.6", "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.28.6.tgz", @@ -313,564 +254,84 @@ "@babel/core": "^7.0.0" } }, - "node_modules/@babel/helper-optimise-call-expression": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.27.1.tgz", - "integrity": "sha512-URMGH08NzYFhubNSGJrpUEphGKQwMQYBySzat5cAByY1/YgIRkULnIy3tAMeszlL/so2HbeilYloUmSpd7GdVw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/types": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" - } - }, "node_modules/@babel/helper-plugin-utils": { - "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.28.6.tgz", - "integrity": "sha512-S9gzZ/bz83GRysI7gAD4wPT/AI3uCnY+9xn+Mx/KPs2JwHJIz1W8PZkg2cqyt3RNOBM8ejcXhV6y8Og7ly/Dug==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-replace-supers": { - "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/helper-replace-supers/-/helper-replace-supers-7.28.6.tgz", - "integrity": "sha512-mq8e+laIk94/yFec3DxSjCRD2Z0TAjhVbEJY3UQrlwVo15Lmt7C2wAUbK4bjnTs4APkwsYLTahXRraQXhb1WCg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-member-expression-to-functions": "^7.28.5", - "@babel/helper-optimise-call-expression": "^7.27.1", - "@babel/traverse": "^7.28.6" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0" - } - }, - "node_modules/@babel/helper-skip-transparent-expression-wrappers": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/helper-skip-transparent-expression-wrappers/-/helper-skip-transparent-expression-wrappers-7.27.1.tgz", - "integrity": "sha512-Tub4ZKEXqbPjXgWLl2+3JpQAYBJ8+ikpQ2Ocj/q/r0LwE3UhENh7EUabyHjz2kCEsrRY83ew2DQdHluuiDQFzg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/traverse": "^7.27.1", - "@babel/types": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-string-parser": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", - "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-validator-identifier": { - "version": "7.28.5", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz", - "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-validator-option": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.27.1.tgz", - "integrity": "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helpers": { - "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.28.6.tgz", - "integrity": "sha512-xOBvwq86HHdB7WUDTfKfT/Vuxh7gElQ+Sfti2Cy6yIWNW05P8iUslOVcZ4/sKbE+/jQaukQAdz/gf3724kYdqw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/template": "^7.28.6", - "@babel/types": "^7.28.6" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/parser": { - "version": "7.29.0", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.0.tgz", - "integrity": "sha512-IyDgFV5GeDUVX4YdF/3CPULtVGSXXMLh1xVIgdCgxApktqnQV0r7/8Nqthg+8YLGaAtdyIlo2qIdZrbCv4+7ww==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/types": "^7.29.0" - }, - "bin": { - "parser": "bin/babel-parser.js" - }, - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/@babel/plugin-proposal-class-properties": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-class-properties/-/plugin-proposal-class-properties-7.18.6.tgz", - "integrity": "sha512-cumfXOF0+nzZrrN8Rf0t7M+tF6sZc7vhQwYQck9q1/5w2OExlD+b4v4RpMJFaV1Z7WcDRgO6FqvxqxGlwo+RHQ==", - "deprecated": "This proposal has been merged to the ECMAScript standard and thus this plugin is no longer maintained. Please use @babel/plugin-transform-class-properties instead.", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-create-class-features-plugin": "^7.18.6", - "@babel/helper-plugin-utils": "^7.18.6" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-proposal-object-rest-spread": { - "version": "7.20.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-object-rest-spread/-/plugin-proposal-object-rest-spread-7.20.7.tgz", - "integrity": "sha512-d2S98yCiLxDVmBmE8UjGcfPvNEUbA1U5q5WxaWFUGRzJSVAZqm5W6MbPct0jxnegUZ0niLeNX+IOzEs7wYg9Dg==", - "deprecated": "This proposal has been merged to the ECMAScript standard and thus this plugin is no longer maintained. Please use @babel/plugin-transform-object-rest-spread instead.", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/compat-data": "^7.20.5", - "@babel/helper-compilation-targets": "^7.20.7", - "@babel/helper-plugin-utils": "^7.20.2", - "@babel/plugin-syntax-object-rest-spread": "^7.8.3", - "@babel/plugin-transform-parameters": "^7.20.7" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-class-properties": { - "version": "7.12.13", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-class-properties/-/plugin-syntax-class-properties-7.12.13.tgz", - "integrity": "sha512-fm4idjKla0YahUNgFNLCB0qySdsoPiZP3iQE3rky0mBUtMZ23yDJ9SJdg6dXTSDnulOVqiF3Hgr9nbXvXTQZYA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.12.13" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-flow": { - "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-flow/-/plugin-syntax-flow-7.28.6.tgz", - "integrity": "sha512-D+OrJumc9McXNEBI/JmFnc/0uCM2/Y3PEBG3gfV3QIYkKv5pvnpzFrl1kYCrcHJP8nOeFB/SHi1IHz29pNGuew==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.28.6" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-import-assertions": { - "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-assertions/-/plugin-syntax-import-assertions-7.28.6.tgz", - "integrity": "sha512-pSJUpFHdx9z5nqTSirOCMtYVP2wFgoWhP0p3g8ONK/4IHhLIBd0B9NYqAvIUAhq+OkhO4VM1tENCt0cjlsNShw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.28.6" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-jsx": { - "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.28.6.tgz", - "integrity": "sha512-wgEmr06G6sIpqr8YDwA2dSRTE3bJ+V0IfpzfSY3Lfgd7YWOaAdlykvJi13ZKBt8cZHfgH1IXN+CL656W3uUa4w==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.28.6" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-object-rest-spread": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-object-rest-spread/-/plugin-syntax-object-rest-spread-7.8.3.tgz", - "integrity": "sha512-XoqMijGZb9y3y2XskN+P1wUGiVwWZ5JmoDRwx5+3GmEplNyVM2s2Dg8ILFQm8rWM48orGy5YpI5Bl8U1y7ydlA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.8.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-arrow-functions": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-arrow-functions/-/plugin-transform-arrow-functions-7.27.1.tgz", - "integrity": "sha512-8Z4TGic6xW70FKThA5HYEKKyBpOOsucTOD1DjU3fZxDg+K3zBJcXMFnt/4yQiZnf5+MiOMSXQ9PaEK/Ilh1DeA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-block-scoped-functions": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoped-functions/-/plugin-transform-block-scoped-functions-7.27.1.tgz", - "integrity": "sha512-cnqkuOtZLapWYZUYM5rVIdv1nXYuFVIltZ6ZJ7nIj585QsjKM5dhL2Fu/lICXZ1OyIAFc7Qy+bvDAtTXqGrlhg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-block-scoping": { - "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoping/-/plugin-transform-block-scoping-7.28.6.tgz", - "integrity": "sha512-tt/7wOtBmwHPNMPu7ax4pdPz6shjFrmHDghvNC+FG9Qvj7D6mJcoRQIF5dy4njmxR941l6rgtvfSB2zX3VlUIw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.28.6" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-classes": { - "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-classes/-/plugin-transform-classes-7.28.6.tgz", - "integrity": "sha512-EF5KONAqC5zAqT783iMGuM2ZtmEBy+mJMOKl2BCvPZ2lVrwvXnB6o+OBWCS+CoeCCpVRF2sA2RBKUxvT8tQT5Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-annotate-as-pure": "^7.27.3", - "@babel/helper-compilation-targets": "^7.28.6", - "@babel/helper-globals": "^7.28.0", - "@babel/helper-plugin-utils": "^7.28.6", - "@babel/helper-replace-supers": "^7.28.6", - "@babel/traverse": "^7.28.6" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-computed-properties": { - "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-computed-properties/-/plugin-transform-computed-properties-7.28.6.tgz", - "integrity": "sha512-bcc3k0ijhHbc2lEfpFHgx7eYw9KNXqOerKWfzbxEHUGKnS3sz9C4CNL9OiFN1297bDNfUiSO7DaLzbvHQQQ1BQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.28.6", - "@babel/template": "^7.28.6" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-destructuring": { - "version": "7.28.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-destructuring/-/plugin-transform-destructuring-7.28.5.tgz", - "integrity": "sha512-Kl9Bc6D0zTUcFUvkNuQh4eGXPKKNDOJQXVyyM4ZAQPMveniJdxi8XMJwLo+xSoW3MIq81bD33lcUe9kZpl0MCw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1", - "@babel/traverse": "^7.28.5" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-flow-strip-types": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-flow-strip-types/-/plugin-transform-flow-strip-types-7.27.1.tgz", - "integrity": "sha512-G5eDKsu50udECw7DL2AcsysXiQyB7Nfg521t2OAJ4tbfTJ27doHLeF/vlI1NZGlLdbb/v+ibvtL1YBQqYOwJGg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1", - "@babel/plugin-syntax-flow": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-for-of": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-for-of/-/plugin-transform-for-of-7.27.1.tgz", - "integrity": "sha512-BfbWFFEJFQzLCQ5N8VocnCtA8J1CLkNTe2Ms2wocj75dd6VpiqS5Z5quTYcUoo4Yq+DN0rtikODccuv7RU81sw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1", - "@babel/helper-skip-transparent-expression-wrappers": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-function-name": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-function-name/-/plugin-transform-function-name-7.27.1.tgz", - "integrity": "sha512-1bQeydJF9Nr1eBCMMbC+hdwmRlsv5XYOMu03YSWFwNs0HsAmtSxxF1fyuYPqemVldVyFmlCU7w8UE14LupUSZQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-compilation-targets": "^7.27.1", - "@babel/helper-plugin-utils": "^7.27.1", - "@babel/traverse": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-literals": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-literals/-/plugin-transform-literals-7.27.1.tgz", - "integrity": "sha512-0HCFSepIpLTkLcsi86GG3mTUzxV5jpmbv97hTETW3yzrAij8aqlD36toB1D0daVFJM8NK6GvKO0gslVQmm+zZA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-member-expression-literals": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-member-expression-literals/-/plugin-transform-member-expression-literals-7.27.1.tgz", - "integrity": "sha512-hqoBX4dcZ1I33jCSWcXrP+1Ku7kdqXf1oeah7ooKOIiAdKQ+uqftgCFNOSzA5AMS2XIHEYeGFg4cKRCdpxzVOQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-modules-commonjs": { - "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-commonjs/-/plugin-transform-modules-commonjs-7.28.6.tgz", - "integrity": "sha512-jppVbf8IV9iWWwWTQIxJMAJCWBuuKx71475wHwYytrRGQ2CWiDvYlADQno3tcYpS/T2UUWFQp3nVtYfK/YBQrA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-module-transforms": "^7.28.6", - "@babel/helper-plugin-utils": "^7.28.6" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-object-super": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-object-super/-/plugin-transform-object-super-7.27.1.tgz", - "integrity": "sha512-SFy8S9plRPbIcxlJ8A6mT/CxFdJx/c04JEctz4jf8YZaVS2px34j7NXRrlGlHkN/M2gnpL37ZpGRGVFLd3l8Ng==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1", - "@babel/helper-replace-supers": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-parameters": { - "version": "7.27.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-parameters/-/plugin-transform-parameters-7.27.7.tgz", - "integrity": "sha512-qBkYTYCb76RRxUM6CcZA5KRu8K4SM8ajzVeUgVdMVO9NN9uI/GaVmBg/WKJJGnNokV9SY8FxNOVWGXzqzUidBg==", + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.28.6.tgz", + "integrity": "sha512-S9gzZ/bz83GRysI7gAD4wPT/AI3uCnY+9xn+Mx/KPs2JwHJIz1W8PZkg2cqyt3RNOBM8ejcXhV6y8Og7ly/Dug==", "dev": true, "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1" - }, "engines": { "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" } }, - "node_modules/@babel/plugin-transform-property-literals": { + "node_modules/@babel/helper-string-parser": { "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-property-literals/-/plugin-transform-property-literals-7.27.1.tgz", - "integrity": "sha512-oThy3BCuCha8kDZ8ZkgOg2exvPYUlprMukKQXI1r1pJ47NCvxfkEy8vK+r/hT9nF0Aa4H1WUPZZjHTFtAhGfmQ==", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", + "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", "dev": true, "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1" - }, "engines": { "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" } }, - "node_modules/@babel/plugin-transform-react-display-name": { - "version": "7.28.0", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-display-name/-/plugin-transform-react-display-name-7.28.0.tgz", - "integrity": "sha512-D6Eujc2zMxKjfa4Zxl4GHMsmhKKZ9VpcqIchJLvwTxad9zWIYulwYItBovpDOoNLISpcZSXoDJ5gaGbQUDqViA==", + "node_modules/@babel/helper-validator-identifier": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz", + "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==", "dev": true, "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1" - }, "engines": { "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" } }, - "node_modules/@babel/plugin-transform-react-jsx": { - "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx/-/plugin-transform-react-jsx-7.28.6.tgz", - "integrity": "sha512-61bxqhiRfAACulXSLd/GxqmAedUSrRZIu/cbaT18T1CetkTmtDN15it7i80ru4DVqRK1WMxQhXs+Lf9kajm5Ow==", + "node_modules/@babel/helper-validator-option": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.27.1.tgz", + "integrity": "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==", "dev": true, "license": "MIT", - "dependencies": { - "@babel/helper-annotate-as-pure": "^7.27.3", - "@babel/helper-module-imports": "^7.28.6", - "@babel/helper-plugin-utils": "^7.28.6", - "@babel/plugin-syntax-jsx": "^7.28.6", - "@babel/types": "^7.28.6" - }, "engines": { "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" } }, - "node_modules/@babel/plugin-transform-shorthand-properties": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-shorthand-properties/-/plugin-transform-shorthand-properties-7.27.1.tgz", - "integrity": "sha512-N/wH1vcn4oYawbJ13Y/FxcQrWk63jhfNa7jef0ih7PHSIHX2LB7GWE1rkPrOnka9kwMxb6hMl19p7lidA+EHmQ==", + "node_modules/@babel/helpers": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.28.6.tgz", + "integrity": "sha512-xOBvwq86HHdB7WUDTfKfT/Vuxh7gElQ+Sfti2Cy6yIWNW05P8iUslOVcZ4/sKbE+/jQaukQAdz/gf3724kYdqw==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1" + "@babel/template": "^7.28.6", + "@babel/types": "^7.28.6" }, "engines": { "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" } }, - "node_modules/@babel/plugin-transform-spread": { - "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-spread/-/plugin-transform-spread-7.28.6.tgz", - "integrity": "sha512-9U4QObUC0FtJl05AsUcodau/RWDytrU6uKgkxu09mLR9HLDAtUMoPuuskm5huQsoktmsYpI+bGmq+iapDcriKA==", + "node_modules/@babel/parser": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.0.tgz", + "integrity": "sha512-IyDgFV5GeDUVX4YdF/3CPULtVGSXXMLh1xVIgdCgxApktqnQV0r7/8Nqthg+8YLGaAtdyIlo2qIdZrbCv4+7ww==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.28.6", - "@babel/helper-skip-transparent-expression-wrappers": "^7.27.1" + "@babel/types": "^7.29.0" }, - "engines": { - "node": ">=6.9.0" + "bin": { + "parser": "bin/babel-parser.js" }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" + "engines": { + "node": ">=6.0.0" } }, - "node_modules/@babel/plugin-transform-template-literals": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-template-literals/-/plugin-transform-template-literals-7.27.1.tgz", - "integrity": "sha512-fBJKiV7F2DxZUkg5EtHKXQdbsbURW3DZKQUWphDum0uRP6eHGGa/He9mc0mypL680pb+e/lDIthRohlv8NCHkg==", + "node_modules/@babel/plugin-syntax-import-assertions": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-assertions/-/plugin-syntax-import-assertions-7.28.6.tgz", + "integrity": "sha512-pSJUpFHdx9z5nqTSirOCMtYVP2wFgoWhP0p3g8ONK/4IHhLIBd0B9NYqAvIUAhq+OkhO4VM1tENCt0cjlsNShw==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1" + "@babel/helper-plugin-utils": "^7.28.6" }, "engines": { "node": ">=6.9.0" @@ -1127,9 +588,9 @@ } }, "node_modules/@eslint/config-array/node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", "dev": true, "license": "ISC", "dependencies": { @@ -1201,9 +662,9 @@ } }, "node_modules/@eslint/eslintrc/node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", "dev": true, "license": "ISC", "dependencies": { @@ -1462,13 +923,13 @@ "license": "0BSD" }, "node_modules/@graphql-codegen/plugin-helpers": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/@graphql-codegen/plugin-helpers/-/plugin-helpers-6.1.0.tgz", - "integrity": "sha512-JJypehWTcty9kxKiqH7TQOetkGdOYjY78RHlI+23qB59cV2wxjFFVf8l7kmuXS4cpGVUNfIjFhVr7A1W7JMtdA==", + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/@graphql-codegen/plugin-helpers/-/plugin-helpers-6.2.0.tgz", + "integrity": "sha512-TKm0Q0+wRlg354Qt3PyXc+sy6dCKxmNofBsgmHoFZNVHtzMQSSgNT+rUWdwBwObQ9bFHiUVsDIv8QqxKMiKmpw==", "dev": true, "license": "MIT", "dependencies": { - "@graphql-tools/utils": "^10.0.0", + "@graphql-tools/utils": "^11.0.0", "change-case-all": "1.0.15", "common-tags": "1.8.2", "import-from": "4.0.0", @@ -1482,6 +943,25 @@ "graphql": "^0.8.0 || ^0.9.0 || ^0.10.0 || ^0.11.0 || ^0.12.0 || ^0.13.0 || ^14.0.0 || ^15.0.0 || ^16.0.0" } }, + "node_modules/@graphql-codegen/plugin-helpers/node_modules/@graphql-tools/utils": { + "version": "11.0.0", + "resolved": "https://registry.npmjs.org/@graphql-tools/utils/-/utils-11.0.0.tgz", + "integrity": "sha512-bM1HeZdXA2C3LSIeLOnH/bcqSgbQgKEDrjxODjqi3y58xai2TkNrtYcQSoWzGbt9VMN1dORGjR7Vem8SPnUFQA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@graphql-typed-document-node/core": "^3.1.1", + "@whatwg-node/promise-helpers": "^1.0.0", + "cross-inspect": "1.0.1", + "tslib": "^2.4.0" + }, + "engines": { + "node": ">=16.0.0" + }, + "peerDependencies": { + "graphql": "^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0" + } + }, "node_modules/@graphql-codegen/plugin-helpers/node_modules/tslib": { "version": "2.6.3", "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.3.tgz", @@ -1620,14 +1100,14 @@ "license": "0BSD" }, "node_modules/@graphql-codegen/typescript-react-query": { - "version": "6.1.1", - "resolved": "https://registry.npmjs.org/@graphql-codegen/typescript-react-query/-/typescript-react-query-6.1.1.tgz", - "integrity": "sha512-knSlUFmq7g7G2DIa5EGjOnwWtNfpU4k+sXWJkxdwJ7lU9nrw6pnDizJcjHCqKelRmk2xwfspVNzu0KoXP7LLsg==", + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/@graphql-codegen/typescript-react-query/-/typescript-react-query-7.0.0.tgz", + "integrity": "sha512-mAgjoMbe0J5s8BhQBlx5txibWNFW2LUVQcV7fc6FY+eYHzRqvqTwBxJWwgMzUAjAZFW32Bzkdyn6F9T8N6TKNg==", "dev": true, "license": "MIT", "dependencies": { - "@graphql-codegen/plugin-helpers": "^3.0.0", - "@graphql-codegen/visitor-plugin-common": "2.13.8", + "@graphql-codegen/plugin-helpers": "^6.1.1", + "@graphql-codegen/visitor-plugin-common": "^6.2.4", "auto-bind": "~4.0.0", "change-case-all": "1.0.15", "tslib": "^2.8.1" @@ -1639,302 +1119,55 @@ "graphql": "^0.8.0 || ^0.9.0 || ^0.10.0 || ^0.11.0 || ^0.12.0 || ^0.13.0 || ^14.0.0 || ^15.0.0 || ^16.0.0" } }, - "node_modules/@graphql-codegen/typescript-react-query/node_modules/@ardatan/relay-compiler": { - "version": "12.0.0", - "resolved": "https://registry.npmjs.org/@ardatan/relay-compiler/-/relay-compiler-12.0.0.tgz", - "integrity": "sha512-9anThAaj1dQr6IGmzBMcfzOQKTa5artjuPmw8NYK/fiGEMjADbSguBY2FMDykt+QhilR3wc9VA/3yVju7JHg7Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/core": "^7.14.0", - "@babel/generator": "^7.14.0", - "@babel/parser": "^7.14.0", - "@babel/runtime": "^7.0.0", - "@babel/traverse": "^7.14.0", - "@babel/types": "^7.0.0", - "babel-preset-fbjs": "^3.4.0", - "chalk": "^4.0.0", - "fb-watchman": "^2.0.0", - "fbjs": "^3.0.0", - "glob": "^7.1.1", - "immutable": "~3.7.6", - "invariant": "^2.2.4", - "nullthrows": "^1.1.1", - "relay-runtime": "12.0.0", - "signedsource": "^1.0.0", - "yargs": "^15.3.1" - }, - "bin": { - "relay-compiler": "bin/relay-compiler" - }, - "peerDependencies": { - "graphql": "*" - } - }, - "node_modules/@graphql-codegen/typescript-react-query/node_modules/@graphql-codegen/plugin-helpers": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/@graphql-codegen/plugin-helpers/-/plugin-helpers-3.1.2.tgz", - "integrity": "sha512-emOQiHyIliVOIjKVKdsI5MXj312zmRDwmHpyUTZMjfpvxq/UVAHUJIVdVf+lnjjrI+LXBTgMlTWTgHQfmICxjg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@graphql-tools/utils": "^9.0.0", - "change-case-all": "1.0.15", - "common-tags": "1.8.2", - "import-from": "4.0.0", - "lodash": "~4.17.0", - "tslib": "~2.4.0" - }, - "peerDependencies": { - "graphql": "^0.8.0 || ^0.9.0 || ^0.10.0 || ^0.11.0 || ^0.12.0 || ^0.13.0 || ^14.0.0 || ^15.0.0 || ^16.0.0" - } - }, - "node_modules/@graphql-codegen/typescript-react-query/node_modules/@graphql-codegen/plugin-helpers/node_modules/tslib": { - "version": "2.4.1", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.4.1.tgz", - "integrity": "sha512-tGyy4dAjRIEwI7BzsB0lynWgOpfqjUdq91XXAlIWD2OwKBH7oCl/GZG/HT4BOHrTlPMOASlMQ7veyTqpmRcrNA==", - "dev": true, - "license": "0BSD" - }, "node_modules/@graphql-codegen/typescript-react-query/node_modules/@graphql-codegen/visitor-plugin-common": { - "version": "2.13.8", - "resolved": "https://registry.npmjs.org/@graphql-codegen/visitor-plugin-common/-/visitor-plugin-common-2.13.8.tgz", - "integrity": "sha512-IQWu99YV4wt8hGxIbBQPtqRuaWZhkQRG2IZKbMoSvh0vGeWb3dB0n0hSgKaOOxDY+tljtOf9MTcUYvJslQucMQ==", + "version": "6.2.4", + "resolved": "https://registry.npmjs.org/@graphql-codegen/visitor-plugin-common/-/visitor-plugin-common-6.2.4.tgz", + "integrity": "sha512-iwiVCc7Mv8/XAa3K35AdFQ9chJSDv/gYEnBeQFF/Sq/W8EyJoHypOGOTTLk7OSrWO4xea65ggv0e7fGt7rPJjQ==", "dev": true, "license": "MIT", "dependencies": { - "@graphql-codegen/plugin-helpers": "^3.1.2", - "@graphql-tools/optimize": "^1.3.0", - "@graphql-tools/relay-operation-optimizer": "^6.5.0", - "@graphql-tools/utils": "^9.0.0", + "@graphql-codegen/plugin-helpers": "^6.1.1", + "@graphql-tools/optimize": "^2.0.0", + "@graphql-tools/relay-operation-optimizer": "^7.1.1", + "@graphql-tools/utils": "^11.0.0", "auto-bind": "~4.0.0", "change-case-all": "1.0.15", - "dependency-graph": "^0.11.0", + "dependency-graph": "^1.0.0", "graphql-tag": "^2.11.0", "parse-filepath": "^1.0.2", - "tslib": "~2.4.0" - }, - "peerDependencies": { - "graphql": "^0.8.0 || ^0.9.0 || ^0.10.0 || ^0.11.0 || ^0.12.0 || ^0.13.0 || ^14.0.0 || ^15.0.0 || ^16.0.0" - } - }, - "node_modules/@graphql-codegen/typescript-react-query/node_modules/@graphql-codegen/visitor-plugin-common/node_modules/tslib": { - "version": "2.4.1", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.4.1.tgz", - "integrity": "sha512-tGyy4dAjRIEwI7BzsB0lynWgOpfqjUdq91XXAlIWD2OwKBH7oCl/GZG/HT4BOHrTlPMOASlMQ7veyTqpmRcrNA==", - "dev": true, - "license": "0BSD" - }, - "node_modules/@graphql-codegen/typescript-react-query/node_modules/@graphql-tools/optimize": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/@graphql-tools/optimize/-/optimize-1.4.0.tgz", - "integrity": "sha512-dJs/2XvZp+wgHH8T5J2TqptT9/6uVzIYvA6uFACha+ufvdMBedkfR4b4GbT8jAKLRARiqRTxy3dctnwkTM2tdw==", - "dev": true, - "license": "MIT", - "dependencies": { - "tslib": "^2.4.0" - }, - "peerDependencies": { - "graphql": "^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0" - } - }, - "node_modules/@graphql-codegen/typescript-react-query/node_modules/@graphql-tools/relay-operation-optimizer": { - "version": "6.5.18", - "resolved": "https://registry.npmjs.org/@graphql-tools/relay-operation-optimizer/-/relay-operation-optimizer-6.5.18.tgz", - "integrity": "sha512-mc5VPyTeV+LwiM+DNvoDQfPqwQYhPV/cl5jOBjTgSniyaq8/86aODfMkrE2OduhQ5E00hqrkuL2Fdrgk0w1QJg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@ardatan/relay-compiler": "12.0.0", - "@graphql-tools/utils": "^9.2.1", - "tslib": "^2.4.0" - }, - "peerDependencies": { - "graphql": "^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0" - } - }, - "node_modules/@graphql-codegen/typescript-react-query/node_modules/@graphql-tools/utils": { - "version": "9.2.1", - "resolved": "https://registry.npmjs.org/@graphql-tools/utils/-/utils-9.2.1.tgz", - "integrity": "sha512-WUw506Ql6xzmOORlriNrD6Ugx+HjVgYxt9KCXD9mHAak+eaXSwuGGPyE60hy9xaDEoXKBsG7SkG69ybitaVl6A==", - "dev": true, - "license": "MIT", - "dependencies": { - "@graphql-typed-document-node/core": "^3.1.1", - "tslib": "^2.4.0" - }, - "peerDependencies": { - "graphql": "^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0" - } - }, - "node_modules/@graphql-codegen/typescript-react-query/node_modules/ansi-regex": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", - "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/@graphql-codegen/typescript-react-query/node_modules/cliui": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/cliui/-/cliui-6.0.0.tgz", - "integrity": "sha512-t6wbgtoCXvAzst7QgXxJYqPt0usEfbgQdftEPbLL/cvv6HPE5VgvqCuAIDR0NgU52ds6rFwqrgakNLrHEjCbrQ==", - "dev": true, - "license": "ISC", - "dependencies": { - "string-width": "^4.2.0", - "strip-ansi": "^6.0.0", - "wrap-ansi": "^6.2.0" - } - }, - "node_modules/@graphql-codegen/typescript-react-query/node_modules/dependency-graph": { - "version": "0.11.0", - "resolved": "https://registry.npmjs.org/dependency-graph/-/dependency-graph-0.11.0.tgz", - "integrity": "sha512-JeMq7fEshyepOWDfcfHK06N3MhyPhz++vtqWhMT5O9A3K42rdsEDpfdVqjaqaAhsw6a+ZqeDvQVtD0hFHQWrzg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.6.0" - } - }, - "node_modules/@graphql-codegen/typescript-react-query/node_modules/find-up": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", - "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", - "dev": true, - "license": "MIT", - "dependencies": { - "locate-path": "^5.0.0", - "path-exists": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/@graphql-codegen/typescript-react-query/node_modules/is-fullwidth-code-point": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", - "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/@graphql-codegen/typescript-react-query/node_modules/locate-path": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", - "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", - "dev": true, - "license": "MIT", - "dependencies": { - "p-locate": "^4.1.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/@graphql-codegen/typescript-react-query/node_modules/p-limit": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", - "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", - "dev": true, - "license": "MIT", - "dependencies": { - "p-try": "^2.0.0" - }, - "engines": { - "node": ">=6" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/@graphql-codegen/typescript-react-query/node_modules/p-locate": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", - "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", - "dev": true, - "license": "MIT", - "dependencies": { - "p-limit": "^2.2.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/@graphql-codegen/typescript-react-query/node_modules/string-width": { - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", - "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", - "dev": true, - "license": "MIT", - "dependencies": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.1" + "tslib": "~2.6.0" }, "engines": { - "node": ">=8" - } - }, - "node_modules/@graphql-codegen/typescript-react-query/node_modules/strip-ansi": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", - "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-regex": "^5.0.1" + "node": ">=16" }, - "engines": { - "node": ">=8" + "peerDependencies": { + "graphql": "^0.8.0 || ^0.9.0 || ^0.10.0 || ^0.11.0 || ^0.12.0 || ^0.13.0 || ^14.0.0 || ^15.0.0 || ^16.0.0" } }, - "node_modules/@graphql-codegen/typescript-react-query/node_modules/y18n": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/y18n/-/y18n-4.0.3.tgz", - "integrity": "sha512-JKhqTOwSrqNA1NY5lSztJ1GrBiUodLMmIZuLiDaMRJ+itFd+ABVE8XBjOvIWL+rSqNDC74LCSFmlb/U4UZ4hJQ==", + "node_modules/@graphql-codegen/typescript-react-query/node_modules/@graphql-codegen/visitor-plugin-common/node_modules/tslib": { + "version": "2.6.3", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.3.tgz", + "integrity": "sha512-xNvxJEOUiWPGhUuUdQgAJPKOOJfGnIyKySOc09XkKsgdUV/3E2zvwZYdejjmRgPCgcym1juLH3226yA7sEFJKQ==", "dev": true, - "license": "ISC" + "license": "0BSD" }, - "node_modules/@graphql-codegen/typescript-react-query/node_modules/yargs": { - "version": "15.4.1", - "resolved": "https://registry.npmjs.org/yargs/-/yargs-15.4.1.tgz", - "integrity": "sha512-aePbxDmcYW++PaqBsJ+HYUFwCdv4LVvdnhBy78E57PIor8/OVvhMrADFFEDh8DHDFRv/O9i3lPhsENjO7QX0+A==", + "node_modules/@graphql-codegen/typescript-react-query/node_modules/@graphql-tools/utils": { + "version": "11.0.0", + "resolved": "https://registry.npmjs.org/@graphql-tools/utils/-/utils-11.0.0.tgz", + "integrity": "sha512-bM1HeZdXA2C3LSIeLOnH/bcqSgbQgKEDrjxODjqi3y58xai2TkNrtYcQSoWzGbt9VMN1dORGjR7Vem8SPnUFQA==", "dev": true, "license": "MIT", "dependencies": { - "cliui": "^6.0.0", - "decamelize": "^1.2.0", - "find-up": "^4.1.0", - "get-caller-file": "^2.0.1", - "require-directory": "^2.1.1", - "require-main-filename": "^2.0.0", - "set-blocking": "^2.0.0", - "string-width": "^4.2.0", - "which-module": "^2.0.0", - "y18n": "^4.0.0", - "yargs-parser": "^18.1.2" + "@graphql-typed-document-node/core": "^3.1.1", + "@whatwg-node/promise-helpers": "^1.0.0", + "cross-inspect": "1.0.1", + "tslib": "^2.4.0" }, "engines": { - "node": ">=8" - } - }, - "node_modules/@graphql-codegen/typescript-react-query/node_modules/yargs-parser": { - "version": "18.1.3", - "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-18.1.3.tgz", - "integrity": "sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ==", - "dev": true, - "license": "ISC", - "dependencies": { - "camelcase": "^5.0.0", - "decamelize": "^1.2.0" + "node": ">=16.0.0" }, - "engines": { - "node": ">=6" + "peerDependencies": { + "graphql": "^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0" } }, "node_modules/@graphql-codegen/typescript/node_modules/@graphql-codegen/visitor-plugin-common": { @@ -2712,13 +1945,13 @@ } }, "node_modules/@graphql-tools/relay-operation-optimizer": { - "version": "7.0.27", - "resolved": "https://registry.npmjs.org/@graphql-tools/relay-operation-optimizer/-/relay-operation-optimizer-7.0.27.tgz", - "integrity": "sha512-rdkL1iDMFaGDiHWd7Bwv7hbhrhnljkJaD0MXeqdwQlZVgVdUDlMot2WuF7CEKVgijpH6eSC6AxXMDeqVgSBS2g==", + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/@graphql-tools/relay-operation-optimizer/-/relay-operation-optimizer-7.1.1.tgz", + "integrity": "sha512-va+ZieMlz6Fj18xUbwyQkZ34PsnzIdPT6Ccy1BNOQw1iclQwk52HejLMZeE/4fH+4cu80Q2HXi5+FjCKpmnJCg==", "dev": true, "license": "MIT", "dependencies": { - "@ardatan/relay-compiler": "^12.0.3", + "@ardatan/relay-compiler": "^13.0.0", "@graphql-tools/utils": "^11.0.0", "tslib": "^2.4.0" }, @@ -2916,12 +2149,13 @@ } }, "node_modules/@helpwave/hightide": { - "version": "0.8.12", - "resolved": "https://registry.npmjs.org/@helpwave/hightide/-/hightide-0.8.12.tgz", - "integrity": "sha512-jZZ48RGIZa1UnWNrWMr9Tr2nSVBKf5g92ZKBVi0iHL95U5et7VKUa1cTw5DA2Nu2qsEDjGWXMRkL44P2n3eleQ==", + "version": "0.9.0", + "resolved": "https://registry.npmjs.org/@helpwave/hightide/-/hightide-0.9.0.tgz", + "integrity": "sha512-dMVMuCDlrWesSEXEIKExN7BvxHASId0/0wmzOUpkQoaoy0Qz3+JbnxS7K3jAjbZR2KVbaJYg8MN6wvlt7Y/0hA==", "license": "MPL-2.0", "dependencies": { "@helpwave/internationalization": "0.4.0", + "@radix-ui/react-slot": "1.2.4", "@tailwindcss/cli": "4.1.18", "@tanstack/react-table": "8.21.3", "clsx": "2.1.1", @@ -2929,6 +2163,9 @@ "react": "19.2.3", "react-dom": "19.2.3", "tailwindcss": "4.1.18" + }, + "bin": { + "barrel": "dist/scripts/barrel.js" } }, "node_modules/@helpwave/hightide/node_modules/lucide-react": { @@ -4381,6 +3618,39 @@ "node": ">=18" } }, + "node_modules/@radix-ui/react-compose-refs": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-1.1.2.tgz", + "integrity": "sha512-z4eqJvfiNnFMHIIvXP3CY57y2WJs5g2v3X0zm9mEJkrkNv4rDxu+sg9Jh8EkXyeqBkB7SOcboo9dMVqhyrACIg==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-slot": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.4.tgz", + "integrity": "sha512-Jl+bCv8HxKnlTLVrcDE8zTMJ09R9/ukw4qBs/oZClOfoQk/cOTbDn+NceXfV7j09YPVQUryJPHurafcSg6EVKA==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.2" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, "node_modules/@repeaterjs/repeater": { "version": "3.0.6", "resolved": "https://registry.npmjs.org/@repeaterjs/repeater/-/repeater-3.0.6.tgz", @@ -5288,7 +4558,7 @@ "version": "19.2.7", "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.7.tgz", "integrity": "sha512-MWtvHrGZLFttgeEj28VXHxpmwYbor/ATPYbBfSFZEIRK0ecCFLl2Qo55z52Hss+UV9CRN7trSeq1zbgx7YDWWg==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "csstype": "^3.2.2" @@ -5686,9 +4956,9 @@ } }, "node_modules/ajv": { - "version": "6.12.6", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", - "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "version": "6.14.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.14.0.tgz", + "integrity": "sha512-IWrosm/yrn43eiKqkfkHis7QioDleaXQHdDVPKg0FSwwd/DuvyX79TZnFOnYpB7dcsFAMmtFztZuXPDvSePkFw==", "dev": true, "license": "MIT", "dependencies": { @@ -5984,52 +5254,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/babel-plugin-syntax-trailing-function-commas": { - "version": "7.0.0-beta.0", - "resolved": "https://registry.npmjs.org/babel-plugin-syntax-trailing-function-commas/-/babel-plugin-syntax-trailing-function-commas-7.0.0-beta.0.tgz", - "integrity": "sha512-Xj9XuRuz3nTSbaTXWv3itLOcxyF4oPD8douBBmj7U9BBC6nEBYfyOJYQMf/8PJAFotC62UY5dFfIGEPr7WswzQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/babel-preset-fbjs": { - "version": "3.4.0", - "resolved": "https://registry.npmjs.org/babel-preset-fbjs/-/babel-preset-fbjs-3.4.0.tgz", - "integrity": "sha512-9ywCsCvo1ojrw0b+XYk7aFvTH6D9064t0RIL1rtMf3nsa02Xw41MS7sZw216Im35xj/UY0PDBQsa1brUDDF1Ow==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/plugin-proposal-class-properties": "^7.0.0", - "@babel/plugin-proposal-object-rest-spread": "^7.0.0", - "@babel/plugin-syntax-class-properties": "^7.0.0", - "@babel/plugin-syntax-flow": "^7.0.0", - "@babel/plugin-syntax-jsx": "^7.0.0", - "@babel/plugin-syntax-object-rest-spread": "^7.0.0", - "@babel/plugin-transform-arrow-functions": "^7.0.0", - "@babel/plugin-transform-block-scoped-functions": "^7.0.0", - "@babel/plugin-transform-block-scoping": "^7.0.0", - "@babel/plugin-transform-classes": "^7.0.0", - "@babel/plugin-transform-computed-properties": "^7.0.0", - "@babel/plugin-transform-destructuring": "^7.0.0", - "@babel/plugin-transform-flow-strip-types": "^7.0.0", - "@babel/plugin-transform-for-of": "^7.0.0", - "@babel/plugin-transform-function-name": "^7.0.0", - "@babel/plugin-transform-literals": "^7.0.0", - "@babel/plugin-transform-member-expression-literals": "^7.0.0", - "@babel/plugin-transform-modules-commonjs": "^7.0.0", - "@babel/plugin-transform-object-super": "^7.0.0", - "@babel/plugin-transform-parameters": "^7.0.0", - "@babel/plugin-transform-property-literals": "^7.0.0", - "@babel/plugin-transform-react-display-name": "^7.0.0", - "@babel/plugin-transform-react-jsx": "^7.0.0", - "@babel/plugin-transform-shorthand-properties": "^7.0.0", - "@babel/plugin-transform-spread": "^7.0.0", - "@babel/plugin-transform-template-literals": "^7.0.0", - "babel-plugin-syntax-trailing-function-commas": "^7.0.0-beta.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0" - } - }, "node_modules/balanced-match": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", @@ -6103,16 +5327,6 @@ "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" } }, - "node_modules/bser": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/bser/-/bser-2.1.1.tgz", - "integrity": "sha512-gQxTNE/GAfIIrmHLUE3oJyp5FO6HRBfhjnw4/wMmA63ZGDJnWBmgY/lyQBpnDUkGmAhbSe39tx2d/iTOAfglwQ==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "node-int64": "^0.4.0" - } - }, "node_modules/call-bind": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.8.tgz", @@ -6184,16 +5398,6 @@ "tslib": "^2.0.3" } }, - "node_modules/camelcase": { - "version": "5.3.1", - "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", - "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, "node_modules/caniuse-lite": { "version": "1.0.30001769", "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001769.tgz", @@ -6519,16 +5723,6 @@ } } }, - "node_modules/cross-fetch": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/cross-fetch/-/cross-fetch-3.2.0.tgz", - "integrity": "sha512-Q+xVJLoGOeIMXZmbUK4HYk+69cQH6LudR0Vu/pRm2YlU/hDV9CiS0gKUMaWY5f2NeUH9C1nV3bsTlCo0FsTV1Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "node-fetch": "^2.7.0" - } - }, "node_modules/cross-inspect": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/cross-inspect/-/cross-inspect-1.0.1.tgz", @@ -6561,7 +5755,7 @@ "version": "3.2.3", "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", - "dev": true, + "devOptional": true, "license": "MIT" }, "node_modules/data-uri-to-buffer": { @@ -6666,16 +5860,6 @@ } } }, - "node_modules/decamelize": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz", - "integrity": "sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/deep-is": { "version": "0.1.4", "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", @@ -7210,9 +6394,9 @@ } }, "node_modules/eslint-plugin-react/node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", "dev": true, "license": "ISC", "dependencies": { @@ -7277,9 +6461,9 @@ } }, "node_modules/eslint/node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", "dev": true, "license": "ISC", "dependencies": { @@ -7421,39 +6605,6 @@ "reusify": "^1.0.4" } }, - "node_modules/fb-watchman": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/fb-watchman/-/fb-watchman-2.0.2.tgz", - "integrity": "sha512-p5161BqbuCaSnB8jIbzQHOlpgsPmK5rJVDfDKO91Axs5NC1uu3HRQm6wt9cd9/+GtQQIO53JdGXXoyDpTAsgYA==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "bser": "2.1.1" - } - }, - "node_modules/fbjs": { - "version": "3.0.5", - "resolved": "https://registry.npmjs.org/fbjs/-/fbjs-3.0.5.tgz", - "integrity": "sha512-ztsSx77JBtkuMrEypfhgc3cI0+0h+svqeie7xHbh1k/IKdcydnvadp/mUaGgjAOXQmQSxsqgaRhS3q9fy+1kxg==", - "dev": true, - "license": "MIT", - "dependencies": { - "cross-fetch": "^3.1.5", - "fbjs-css-vars": "^1.0.0", - "loose-envify": "^1.0.0", - "object-assign": "^4.1.0", - "promise": "^7.1.1", - "setimmediate": "^1.0.5", - "ua-parser-js": "^1.0.35" - } - }, - "node_modules/fbjs-css-vars": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/fbjs-css-vars/-/fbjs-css-vars-1.0.2.tgz", - "integrity": "sha512-b2XGFAFdWZWg0phtAWLHCk836A1Xann+I+Dgd3Gk64MHKZO44FfoD1KxyvbSh0qZsIoXQGGlVztIY+oitJPpRQ==", - "dev": true, - "license": "MIT" - }, "node_modules/fdir": { "version": "6.5.0", "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", @@ -7554,9 +6705,9 @@ } }, "node_modules/flatted": { - "version": "3.3.3", - "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.3.tgz", - "integrity": "sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==", + "version": "3.4.1", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.4.1.tgz", + "integrity": "sha512-IxfVbRFVlV8V/yRaGzk0UVIcsKKHMSfYw66T/u4nTwlWteQePsxe//LjudR1AMX4tZW3WFCh3Zqa/sjlqpbURQ==", "dev": true, "license": "ISC" }, @@ -7620,13 +6771,6 @@ "url": "https://github.com/sponsors/rawify" } }, - "node_modules/fs.realpath": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", - "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", - "dev": true, - "license": "ISC" - }, "node_modules/fsevents": { "version": "2.3.2", "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", @@ -7783,28 +6927,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/glob": { - "version": "7.2.3", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", - "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", - "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", - "dev": true, - "license": "ISC", - "dependencies": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^3.1.1", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" - }, - "engines": { - "node": "*" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, "node_modules/glob-parent": { "version": "6.0.2", "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", @@ -7818,30 +6940,6 @@ "node": ">=10.13.0" } }, - "node_modules/glob/node_modules/brace-expansion": { - "version": "1.1.12", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", - "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", - "dev": true, - "license": "MIT", - "dependencies": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, - "node_modules/glob/node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", - "dev": true, - "license": "ISC", - "dependencies": { - "brace-expansion": "^1.1.7" - }, - "engines": { - "node": "*" - } - }, "node_modules/globals": { "version": "14.0.0", "resolved": "https://registry.npmjs.org/globals/-/globals-14.0.0.tgz", @@ -8385,14 +7483,11 @@ } }, "node_modules/immutable": { - "version": "3.7.6", - "resolved": "https://registry.npmjs.org/immutable/-/immutable-3.7.6.tgz", - "integrity": "sha512-AizQPcaofEtO11RZhPPHBOJRdo/20MKQF9mBLnVkBoyHi1/zXK8fzVdnEpSV9gxqtnh6Qomfp3F0xT5qP/vThw==", + "version": "5.1.5", + "resolved": "https://registry.npmjs.org/immutable/-/immutable-5.1.5.tgz", + "integrity": "sha512-t7xcm2siw+hlUM68I+UEOK+z84RzmN59as9DZ7P1l0994DKUWV7UXBMQZVxaoMSRQ+PBZbHCOoBt7a2wxOMt+A==", "dev": true, - "license": "BSD-3-Clause", - "engines": { - "node": ">=0.8.0" - } + "license": "MIT" }, "node_modules/import-fresh": { "version": "3.3.1", @@ -8444,25 +7539,6 @@ "node": ">=0.8.19" } }, - "node_modules/inflight": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", - "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", - "deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.", - "dev": true, - "license": "ISC", - "dependencies": { - "once": "^1.3.0", - "wrappy": "1" - } - }, - "node_modules/inherits": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", - "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", - "dev": true, - "license": "ISC" - }, "node_modules/internal-slot": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.1.0.tgz", @@ -9796,13 +8872,13 @@ } }, "node_modules/minimatch": { - "version": "9.0.5", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", - "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "version": "9.0.9", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.9.tgz", + "integrity": "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==", "dev": true, "license": "ISC", "dependencies": { - "brace-expansion": "^2.0.1" + "brace-expansion": "^2.0.2" }, "engines": { "node": ">=16 || 14 >=14.17" @@ -10005,34 +9081,6 @@ "node": ">=10.5.0" } }, - "node_modules/node-fetch": { - "version": "2.7.0", - "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", - "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==", - "dev": true, - "license": "MIT", - "dependencies": { - "whatwg-url": "^5.0.0" - }, - "engines": { - "node": "4.x || >=6.0.0" - }, - "peerDependencies": { - "encoding": "^0.1.0" - }, - "peerDependenciesMeta": { - "encoding": { - "optional": true - } - } - }, - "node_modules/node-int64": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/node-int64/-/node-int64-0.4.0.tgz", - "integrity": "sha512-O5lz91xSOeoXP6DulyHfllpq+Eg00MWitZIbtPfoSEvqIHdl5gfcY6hYzDWnj0qD5tz52PI08u9qUvSVeUBeHw==", - "dev": true, - "license": "MIT" - }, "node_modules/node-releases": { "version": "2.0.27", "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.27.tgz", @@ -10053,13 +9101,6 @@ "node": ">=0.10.0" } }, - "node_modules/nullthrows": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/nullthrows/-/nullthrows-1.1.1.tgz", - "integrity": "sha512-2vPPEi+Z7WqML2jZYddDIfy5Dqb0r2fze2zTxNNknZaFpVHU3mFB3R+DWeJWGVx0ecvttSGlJTI+WG+8Z4cDWw==", - "dev": true, - "license": "MIT" - }, "node_modules/object-assign": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", @@ -10285,16 +9326,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/p-try": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", - "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, "node_modules/param-case": { "version": "3.0.4", "resolved": "https://registry.npmjs.org/param-case/-/param-case-3.0.4.tgz", @@ -10385,16 +9416,6 @@ "node": ">=8" } }, - "node_modules/path-is-absolute": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", - "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/path-key": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", @@ -10550,16 +9571,6 @@ "node": ">= 0.8.0" } }, - "node_modules/promise": { - "version": "7.3.1", - "resolved": "https://registry.npmjs.org/promise/-/promise-7.3.1.tgz", - "integrity": "sha512-nolQXZ/4L+bP/UGlkfaIujX9BKxGwmQ9OT4mOt5yvy8iK1h3wqTEJCijzGANTCCl9nWjY41juyAn2K3Q1hLLTg==", - "dev": true, - "license": "MIT", - "dependencies": { - "asap": "~2.0.3" - } - }, "node_modules/prop-types": { "version": "15.8.1", "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz", @@ -10675,18 +9686,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/relay-runtime": { - "version": "12.0.0", - "resolved": "https://registry.npmjs.org/relay-runtime/-/relay-runtime-12.0.0.tgz", - "integrity": "sha512-QU6JKr1tMsry22DXNy9Whsq5rmvwr3LSZiiWV/9+DFpuTWvp+WFhobWMc8TC4OjKFfNhEZy7mOiqUAn5atQtug==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/runtime": "^7.0.0", - "fbjs": "^3.0.0", - "invariant": "^2.2.4" - } - }, "node_modules/remedial": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/remedial/-/remedial-1.0.8.tgz", @@ -10721,13 +9720,6 @@ "node": ">=0.10.0" } }, - "node_modules/require-main-filename": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/require-main-filename/-/require-main-filename-2.0.0.tgz", - "integrity": "sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==", - "dev": true, - "license": "ISC" - }, "node_modules/resolve": { "version": "2.0.0-next.5", "resolved": "https://registry.npmjs.org/resolve/-/resolve-2.0.0-next.5.tgz", @@ -10915,13 +9907,6 @@ "upper-case-first": "^2.0.2" } }, - "node_modules/set-blocking": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", - "integrity": "sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==", - "dev": true, - "license": "ISC" - }, "node_modules/set-function-length": { "version": "1.2.2", "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz", @@ -10971,13 +9956,6 @@ "node": ">= 0.4" } }, - "node_modules/setimmediate": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/setimmediate/-/setimmediate-1.0.5.tgz", - "integrity": "sha512-MATJdZp8sLqDl/68LfQmbP8zKPLQNV6BIZoIgrscFDQ+RsvK/BxeDQOgyxKKoh0y/8h3BqVFnCqQ/gd+reiIXA==", - "dev": true, - "license": "MIT" - }, "node_modules/sharp": { "version": "0.34.5", "resolved": "https://registry.npmjs.org/sharp/-/sharp-0.34.5.tgz", @@ -11159,13 +10137,6 @@ "url": "https://github.com/sponsors/isaacs" } }, - "node_modules/signedsource": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/signedsource/-/signedsource-1.0.0.tgz", - "integrity": "sha512-6+eerH9fEnNmi/hyM1DXcRK3pWdoMQtlkQ+ns0ntzunjKqp5i3sKCc80ym8Fib3iaYhdJUOPdhlJWj1tvge2Ww==", - "dev": true, - "license": "BSD-3-Clause" - }, "node_modules/slash": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", @@ -11563,13 +10534,6 @@ "node": ">=8.0" } }, - "node_modules/tr46": { - "version": "0.0.3", - "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", - "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==", - "dev": true, - "license": "MIT" - }, "node_modules/ts-api-utils": { "version": "2.4.0", "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.4.0.tgz", @@ -11724,33 +10688,6 @@ "typescript": ">=4.8.4 <6.0.0" } }, - "node_modules/ua-parser-js": { - "version": "1.0.41", - "resolved": "https://registry.npmjs.org/ua-parser-js/-/ua-parser-js-1.0.41.tgz", - "integrity": "sha512-LbBDqdIC5s8iROCUjMbW1f5dJQTEFB1+KO9ogbvlb3nm9n4YHa5p4KTvFPWvh2Hs8gZMBuiB1/8+pdfe/tDPug==", - "dev": true, - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/ua-parser-js" - }, - { - "type": "paypal", - "url": "https://paypal.me/faisalman" - }, - { - "type": "github", - "url": "https://github.com/sponsors/faisalman" - } - ], - "license": "MIT", - "bin": { - "ua-parser-js": "script/cli.js" - }, - "engines": { - "node": "*" - } - }, "node_modules/unbox-primitive": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.1.0.tgz", @@ -11877,13 +10814,6 @@ "node": ">= 8" } }, - "node_modules/webidl-conversions": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", - "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==", - "dev": true, - "license": "BSD-2-Clause" - }, "node_modules/whatwg-mimetype": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-4.0.0.tgz", @@ -11894,17 +10824,6 @@ "node": ">=18" } }, - "node_modules/whatwg-url": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", - "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", - "dev": true, - "license": "MIT", - "dependencies": { - "tr46": "~0.0.3", - "webidl-conversions": "^3.0.0" - } - }, "node_modules/which": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", @@ -11988,13 +10907,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/which-module": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/which-module/-/which-module-2.0.1.tgz", - "integrity": "sha512-iBdZ57RDvnOR9AGBhML2vFZf7h8vmBjhoaZqODJBFWHVtKkDmKuHai3cx5PgVMrX5YDNp27AofYbAwctSS+vhQ==", - "dev": true, - "license": "ISC" - }, "node_modules/which-typed-array": { "version": "1.1.20", "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.20.tgz", diff --git a/web/package.json b/web/package.json index 86e73cd..298e7f0 100644 --- a/web/package.json +++ b/web/package.json @@ -17,7 +17,7 @@ "@dnd-kit/core": "6.3.1", "@dnd-kit/modifiers": "9.0.0", "@dnd-kit/sortable": "7.0.2", - "@helpwave/hightide": "0.8.12", + "@helpwave/hightide": "0.9.0", "@helpwave/internationalization": "0.4.0", "@tailwindcss/postcss": "4.1.3", "@tanstack/react-query": "5.90.16", @@ -45,7 +45,7 @@ "@graphql-codegen/client-preset": "5.2.1", "@graphql-codegen/typescript": "5.0.6", "@graphql-codegen/typescript-operations": "5.0.6", - "@graphql-codegen/typescript-react-query": "6.1.1", + "@graphql-codegen/typescript-react-query": "7.0.0", "@helpwave/eslint-config": "0.0.11", "@playwright/test": "1.57.0", "@types/node": "20.17.10", diff --git a/web/pages/settings/index.tsx b/web/pages/settings/index.tsx index 44572ac..8d495e7 100644 --- a/web/pages/settings/index.tsx +++ b/web/pages/settings/index.tsx @@ -274,40 +274,38 @@ const SettingsPage: NextPage = () => {
{translation('language')} -
{translation('pThemes', { count: 1 })} - setFastAccessName(e.target.value)} + /> +
+
+ + +
+
+ ) diff --git a/web/i18n/translations.ts b/web/i18n/translations.ts index 6c98567..4a2e85e 100644 --- a/web/i18n/translations.ts +++ b/web/i18n/translations.ts @@ -13,6 +13,8 @@ export type TasksTranslationEntries = { 'account': string, 'active': string, 'add': string, + 'addFastAccess': string, + 'addFastAccessDescription': string, 'addPatient': string, 'addProperty': string, 'addTask': string, @@ -221,6 +223,8 @@ export const tasksTranslation: Translation Date: Sun, 22 Mar 2026 20:28:47 +0100 Subject: [PATCH 08/20] chore: possible --- backend/api/inputs.py | 31 ++ backend/api/resolvers/__init__.py | 3 + backend/api/resolvers/saved_view.py | 178 +++++++++++ backend/api/types/saved_view.py | 41 +++ .../versions/add_saved_views_table.py | 38 +++ backend/database/models/__init__.py | 1 + backend/database/models/saved_view.py | 48 +++ backend/database/models/user.py | 4 + docs/VIEWS_ARCHITECTURE.md | 79 +++++ web/api/gql/generated.ts | 129 +++++++- web/api/graphql/SavedView.graphql | 83 +++++ web/components/layout/Page.tsx | 33 +- web/components/tables/PatientList.tsx | 110 +++---- .../views/PatientViewTasksPanel.tsx | 109 +++++++ web/components/views/SaveViewDialog.tsx | 102 ++++++ .../views/TaskViewPatientsPanel.tsx | 91 ++++++ web/data/hooks/index.ts | 1 + web/data/hooks/useSavedViews.ts | 25 ++ web/data/index.ts | 2 + web/i18n/translations.ts | 105 ++++++ web/locales/de-DE.arb | 15 + web/locales/en-US.arb | 15 + web/locales/es-ES.arb | 15 + web/locales/fr-FR.arb | 15 + web/locales/nl-NL.arb | 15 + web/locales/pt-BR.arb | 15 + web/pages/settings/index.tsx | 16 +- web/pages/settings/views.tsx | 271 ++++++++++++++++ web/pages/tasks/index.tsx | 42 ++- web/pages/view/[uid].tsx | 301 ++++++++++++++++++ web/utils/viewDefinition.ts | 93 ++++++ 31 files changed, 1962 insertions(+), 64 deletions(-) create mode 100644 backend/api/resolvers/saved_view.py create mode 100644 backend/api/types/saved_view.py create mode 100644 backend/database/migrations/versions/add_saved_views_table.py create mode 100644 backend/database/models/saved_view.py create mode 100644 docs/VIEWS_ARCHITECTURE.md create mode 100644 web/api/graphql/SavedView.graphql create mode 100644 web/components/views/PatientViewTasksPanel.tsx create mode 100644 web/components/views/SaveViewDialog.tsx create mode 100644 web/components/views/TaskViewPatientsPanel.tsx create mode 100644 web/data/hooks/useSavedViews.ts create mode 100644 web/pages/settings/views.tsx create mode 100644 web/pages/view/[uid].tsx create mode 100644 web/utils/viewDefinition.ts diff --git a/backend/api/inputs.py b/backend/api/inputs.py index 204a80f..4db40a1 100644 --- a/backend/api/inputs.py +++ b/backend/api/inputs.py @@ -286,3 +286,34 @@ class FullTextSearchInput: search_columns: list[str] | None = None include_properties: bool = False property_definition_ids: list[str] | None = None + + +@strawberry.enum +class SavedViewEntityType(Enum): + TASK = "task" + PATIENT = "patient" + + +@strawberry.enum +class SavedViewVisibility(Enum): + PRIVATE = "private" + LINK_SHARED = "link_shared" + + +@strawberry.input +class CreateSavedViewInput: + name: str + base_entity_type: SavedViewEntityType + filter_definition: str + sort_definition: str + parameters: str + visibility: SavedViewVisibility = SavedViewVisibility.PRIVATE + + +@strawberry.input +class UpdateSavedViewInput: + name: str | None = None + filter_definition: str | None = None + sort_definition: str | None = None + parameters: str | None = None + visibility: SavedViewVisibility | None = None diff --git a/backend/api/resolvers/__init__.py b/backend/api/resolvers/__init__.py index b053198..c65a364 100644 --- a/backend/api/resolvers/__init__.py +++ b/backend/api/resolvers/__init__.py @@ -4,6 +4,7 @@ from .location import LocationMutation, LocationQuery, LocationSubscription from .patient import PatientMutation, PatientQuery, PatientSubscription from .property import PropertyDefinitionMutation, PropertyDefinitionQuery +from .saved_view import SavedViewMutation, SavedViewQuery from .task import TaskMutation, TaskQuery, TaskSubscription from .user import UserMutation, UserQuery @@ -16,6 +17,7 @@ class Query( PropertyDefinitionQuery, UserQuery, AuditQuery, + SavedViewQuery, ): pass @@ -27,6 +29,7 @@ class Mutation( PropertyDefinitionMutation, LocationMutation, UserMutation, + SavedViewMutation, ): pass diff --git a/backend/api/resolvers/saved_view.py b/backend/api/resolvers/saved_view.py new file mode 100644 index 0000000..31d0349 --- /dev/null +++ b/backend/api/resolvers/saved_view.py @@ -0,0 +1,178 @@ +import json + +import strawberry +from graphql import GraphQLError +from sqlalchemy import select + +from api.context import Info +from api.services.base import BaseRepository +from api.inputs import ( + CreateSavedViewInput, + SavedViewVisibility, + UpdateSavedViewInput, +) +from api.types.saved_view import SavedViewType +from database import models + + +def _require_user(info: Info) -> models.User: + user = info.context.user + if not user: + raise GraphQLError("Authentication required") + return user + + +@strawberry.type +class SavedViewQuery: + @strawberry.field + async def saved_view(self, info: Info, id: strawberry.ID) -> SavedViewType | None: + db = info.context.db + result = await db.execute( + select(models.SavedView).where(models.SavedView.id == str(id)) + ) + row = result.scalars().first() + if not row: + return None + uid = info.context.user.id if info.context.user else None + if row.owner_user_id != uid and row.visibility != SavedViewVisibility.LINK_SHARED.value: + raise GraphQLError("Not found or access denied") + return SavedViewType.from_model(row, current_user_id=uid) + + @strawberry.field + async def my_saved_views(self, info: Info) -> list[SavedViewType]: + user = _require_user(info) + db = info.context.db + result = await db.execute( + select(models.SavedView) + .where(models.SavedView.owner_user_id == user.id) + .order_by(models.SavedView.updated_at.desc()) + ) + rows = result.scalars().all() + return [SavedViewType.from_model(r, current_user_id=user.id) for r in rows] + + +@strawberry.type +class SavedViewMutation: + @strawberry.mutation + async def create_saved_view( + self, + info: Info, + data: CreateSavedViewInput, + ) -> SavedViewType: + user = _require_user(info) + for blob, label in ( + (data.filter_definition, "filter_definition"), + (data.sort_definition, "sort_definition"), + (data.parameters, "parameters"), + ): + try: + json.loads(blob) + except json.JSONDecodeError as e: + raise GraphQLError(f"Invalid JSON in {label}") from e + + row = models.SavedView( + name=data.name.strip(), + base_entity_type=data.base_entity_type.value, + filter_definition=data.filter_definition, + sort_definition=data.sort_definition, + parameters=data.parameters, + owner_user_id=user.id, + visibility=data.visibility.value, + ) + info.context.db.add(row) + await info.context.db.commit() + await info.context.db.refresh(row) + return SavedViewType.from_model(row, current_user_id=user.id) + + @strawberry.mutation + async def update_saved_view( + self, + info: Info, + id: strawberry.ID, + data: UpdateSavedViewInput, + ) -> SavedViewType: + user = _require_user(info) + db = info.context.db + result = await db.execute( + select(models.SavedView).where(models.SavedView.id == str(id)) + ) + row = result.scalars().first() + if not row: + raise GraphQLError("View not found") + if row.owner_user_id != user.id: + raise GraphQLError("Forbidden") + + if data.name is not None: + row.name = data.name.strip() + if data.filter_definition is not None: + try: + json.loads(data.filter_definition) + except json.JSONDecodeError as e: + raise GraphQLError("Invalid JSON in filter_definition") from e + row.filter_definition = data.filter_definition + if data.sort_definition is not None: + try: + json.loads(data.sort_definition) + except json.JSONDecodeError as e: + raise GraphQLError("Invalid JSON in sort_definition") from e + row.sort_definition = data.sort_definition + if data.parameters is not None: + try: + json.loads(data.parameters) + except json.JSONDecodeError as e: + raise GraphQLError("Invalid JSON in parameters") from e + row.parameters = data.parameters + if data.visibility is not None: + row.visibility = data.visibility.value + + await db.commit() + await db.refresh(row) + return SavedViewType.from_model(row, current_user_id=user.id) + + @strawberry.mutation + async def delete_saved_view(self, info: Info, id: strawberry.ID) -> bool: + user = _require_user(info) + db = info.context.db + result = await db.execute( + select(models.SavedView).where(models.SavedView.id == str(id)) + ) + row = result.scalars().first() + if not row: + return False + if row.owner_user_id != user.id: + raise GraphQLError("Forbidden") + repo = BaseRepository(db, models.SavedView) + await repo.delete(row) + return True + + @strawberry.mutation + async def duplicate_saved_view( + self, + info: Info, + id: strawberry.ID, + name: str, + ) -> SavedViewType: + user = _require_user(info) + db = info.context.db + result = await db.execute( + select(models.SavedView).where(models.SavedView.id == str(id)) + ) + src = result.scalars().first() + if not src: + raise GraphQLError("View not found") + if src.owner_user_id != user.id and src.visibility != SavedViewVisibility.LINK_SHARED.value: + raise GraphQLError("Not found or access denied") + + clone = models.SavedView( + name=name.strip(), + base_entity_type=src.base_entity_type, + filter_definition=src.filter_definition, + sort_definition=src.sort_definition, + parameters=src.parameters, + owner_user_id=user.id, + visibility=SavedViewVisibility.PRIVATE.value, + ) + db.add(clone) + await db.commit() + await db.refresh(clone) + return SavedViewType.from_model(clone, current_user_id=user.id) diff --git a/backend/api/types/saved_view.py b/backend/api/types/saved_view.py new file mode 100644 index 0000000..b9eca42 --- /dev/null +++ b/backend/api/types/saved_view.py @@ -0,0 +1,41 @@ +from __future__ import annotations + +import strawberry + +from api.inputs import SavedViewEntityType, SavedViewVisibility +from database.models.saved_view import SavedView as SavedViewModel + + +@strawberry.type(name="SavedView") +class SavedViewType: + id: strawberry.ID + name: str + base_entity_type: SavedViewEntityType + filter_definition: str + sort_definition: str + parameters: str + owner_user_id: strawberry.ID + visibility: SavedViewVisibility + created_at: str + updated_at: str + is_owner: bool + + @staticmethod + def from_model( + row: SavedViewModel, + *, + current_user_id: str | None, + ) -> "SavedViewType": + return SavedViewType( + id=strawberry.ID(row.id), + name=row.name, + base_entity_type=SavedViewEntityType(row.base_entity_type), + filter_definition=row.filter_definition, + sort_definition=row.sort_definition, + parameters=row.parameters, + owner_user_id=strawberry.ID(row.owner_user_id), + visibility=SavedViewVisibility(row.visibility), + created_at=row.created_at.isoformat() if row.created_at else "", + updated_at=row.updated_at.isoformat() if row.updated_at else "", + is_owner=current_user_id is not None and row.owner_user_id == current_user_id, + ) diff --git a/backend/database/migrations/versions/add_saved_views_table.py b/backend/database/migrations/versions/add_saved_views_table.py new file mode 100644 index 0000000..82cc9e0 --- /dev/null +++ b/backend/database/migrations/versions/add_saved_views_table.py @@ -0,0 +1,38 @@ +"""Add saved_views table for persistent user views. + +Revision ID: add_saved_views_table +Revises: add_property_value_user_value +Create Date: 2026-02-10 + +""" +from typing import Sequence, Union + +from alembic import op +import sqlalchemy as sa + +revision: str = "add_saved_views_table" +down_revision: Union[str, Sequence[str], None] = "add_property_value_user_value" +branch_labels: Union[str, Sequence[str], None] = None +depends_on: Union[str, Sequence[str], None] = None + + +def upgrade() -> None: + op.create_table( + "saved_views", + sa.Column("id", sa.String(), nullable=False), + sa.Column("name", sa.String(), nullable=False), + sa.Column("base_entity_type", sa.String(), nullable=False), + sa.Column("filter_definition", sa.Text(), nullable=False), + sa.Column("sort_definition", sa.Text(), nullable=False), + sa.Column("parameters", sa.Text(), nullable=False), + sa.Column("owner_user_id", sa.String(), nullable=False), + sa.Column("visibility", sa.String(), nullable=False), + sa.Column("created_at", sa.DateTime(timezone=True), server_default=sa.func.now()), + sa.Column("updated_at", sa.DateTime(timezone=True), server_default=sa.func.now()), + sa.ForeignKeyConstraint(["owner_user_id"], ["users.id"]), + sa.PrimaryKeyConstraint("id"), + ) + + +def downgrade() -> None: + op.drop_table("saved_views") diff --git a/backend/database/models/__init__.py b/backend/database/models/__init__.py index 8108c8c..09b1c11 100644 --- a/backend/database/models/__init__.py +++ b/backend/database/models/__init__.py @@ -4,3 +4,4 @@ from .task import Task, task_dependencies # noqa: F401 from .property import PropertyDefinition, PropertyValue # noqa: F401 from .scaffold import ScaffoldImportState # noqa: F401 +from .saved_view import SavedView # noqa: F401 diff --git a/backend/database/models/saved_view.py b/backend/database/models/saved_view.py new file mode 100644 index 0000000..e885d18 --- /dev/null +++ b/backend/database/models/saved_view.py @@ -0,0 +1,48 @@ +from __future__ import annotations + +import uuid +from datetime import datetime +from typing import TYPE_CHECKING + +from database.models.base import Base +from sqlalchemy import DateTime, ForeignKey, String, Text, func +from sqlalchemy.orm import Mapped, mapped_column, relationship + +if TYPE_CHECKING: + from .user import User + + +class SavedView(Base): + """ + Persistent user-defined view: saved filters, sort, scope (parameters), and entity type. + filter_definition / sort_definition / parameters store JSON as text (SQLite + Postgres compatible). + """ + + __tablename__ = "saved_views" + + id: Mapped[str] = mapped_column( + String, + primary_key=True, + default=lambda: str(uuid.uuid4()), + ) + name: Mapped[str] = mapped_column(String, nullable=False) + base_entity_type: Mapped[str] = mapped_column( + String, nullable=False + ) # 'task' | 'patient' + filter_definition: Mapped[str] = mapped_column(Text, nullable=False, default="{}") + sort_definition: Mapped[str] = mapped_column(Text, nullable=False, default="{}") + parameters: Mapped[str] = mapped_column(Text, nullable=False, default="{}") + owner_user_id: Mapped[str] = mapped_column( + String, ForeignKey("users.id"), nullable=False + ) + visibility: Mapped[str] = mapped_column( + String, nullable=False, default="private" + ) # 'private' | 'link_shared' + created_at: Mapped[datetime] = mapped_column( + DateTime(timezone=True), server_default=func.now() + ) + updated_at: Mapped[datetime] = mapped_column( + DateTime(timezone=True), server_default=func.now(), onupdate=func.now() + ) + + owner: Mapped["User"] = relationship("User", back_populates="saved_views") diff --git a/backend/database/models/user.py b/backend/database/models/user.py index f8b0f0a..7c904ab 100644 --- a/backend/database/models/user.py +++ b/backend/database/models/user.py @@ -10,6 +10,7 @@ if TYPE_CHECKING: from .location import LocationNode + from .saved_view import SavedView from .task import Task user_root_locations = Table( @@ -44,6 +45,9 @@ class User(Base): ) tasks: Mapped[list[Task]] = relationship("Task", back_populates="assignee") + saved_views: Mapped[list["SavedView"]] = relationship( + "SavedView", back_populates="owner" + ) root_locations: Mapped[list[LocationNode]] = relationship( "LocationNode", secondary=user_root_locations, diff --git a/docs/VIEWS_ARCHITECTURE.md b/docs/VIEWS_ARCHITECTURE.md new file mode 100644 index 0000000..a21af7b --- /dev/null +++ b/docs/VIEWS_ARCHITECTURE.md @@ -0,0 +1,79 @@ +# Saved views (persistent views) + +## Concept + +A **SavedView** stores a named configuration for list screens: + +| Field | Purpose | +|--------|---------| +| `filterDefinition` | JSON string: column filters (same wire format as `useStorageSyncedTableState` filters). | +| `sortDefinition` | JSON string: TanStack `SortingState` array. | +| `parameters` | JSON string: **scope** and cross-entity context — `rootLocationIds`, `locationId`, `searchQuery` (patient), `assigneeId` (task / my tasks). | +| `baseEntityType` | `PATIENT` or `TASK` — primary tab when opening `/view/:uid`. | +| `visibility` | `PRIVATE` or `LINK_SHARED` (share by link / UID). | + +Location is **not** a separate route anymore for saved views: it is encoded in `parameters` (`rootLocationIds`, `locationId`). + +## Cross-entity model + +- **Patient view** + - **Patients tab**: `PatientList` hydrated from `filterDefinition` / `sortDefinition` / parameters. + - **Tasks tab**: `PatientViewTasksPanel` runs the **same patient query** (`usePatients` with identical filters/sort/scope) and flattens tasks from those patients — the task universe is *derived from the patient universe*, not an ad-hoc client filter. + +- **Task view** + - **Tasks tab**: `useTasksPaginated` with filters from the view + scope from parameters (`rootLocationIds`, `assigneeId`). + - **Patients tab**: `TaskViewPatientsPanel` runs **`useTasks` without pagination** with the same task filters/sort/scope and builds **distinct patients** from `tasks[].patient`. + +## GraphQL (examples) + +```graphql +query { + savedView(id: "…") { + id + name + baseEntityType + filterDefinition + sortDefinition + parameters + isOwner + visibility + } +} + +mutation { + createSavedView(data: { + name: "ICU patients" + baseEntityType: PATIENT + filterDefinition: "[]" + sortDefinition: "[]" + parameters: "{\"rootLocationIds\":[\"…\"],\"locationId\":null,\"searchQuery\":\"\"}" + visibility: PRIVATE + }) { id } +} +``` + +```graphql +mutation { + duplicateSavedView(id: "…", name: "Copy of shared view") { id } +} +``` + +## Frontend entry points + +| Area | Path / component | +|------|-------------------| +| Open view | `/view/[uid]` | +| Save from patients | `PatientList` → `SaveViewDialog` | +| Save from my tasks | `/tasks` → `SaveViewDialog` | +| Sidebar | `Page` → expandable **Saved views** + link to settings | +| Manage | `/settings/views` (table: open, rename, share link, duplicate, delete) | + +## Migrations + +Apply Alembic migration `add_saved_views_table` (or your project’s revision chain) so the `saved_views` table exists before using the API. + +## Follow-ups + +- **Update view** from UI (owner edits in place → `updateSavedView`) instead of only “save as new”. +- **Share visibility** UI (`LINK_SHARED`) and server checks are already modeled; expose in settings. +- **Redirect** `/location/[id]` → a default view or keep both during transition. diff --git a/web/api/gql/generated.ts b/web/api/gql/generated.ts index c614ec5..15c77f9 100644 --- a/web/api/gql/generated.ts +++ b/web/api/gql/generated.ts @@ -61,6 +61,15 @@ export type CreatePropertyDefinitionInput = { options?: InputMaybe>; }; +export type CreateSavedViewInput = { + baseEntityType: SavedViewEntityType; + filterDefinition: Scalars['String']['input']; + name: Scalars['String']['input']; + parameters: Scalars['String']['input']; + sortDefinition: Scalars['String']['input']; + visibility?: SavedViewVisibility; +}; + export type CreateTaskInput = { assigneeId?: InputMaybe; assigneeTeamId?: InputMaybe; @@ -195,12 +204,15 @@ export type Mutation = { createLocationNode: LocationNodeType; createPatient: PatientType; createPropertyDefinition: PropertyDefinitionType; + createSavedView: SavedView; createTask: TaskType; deleteLocationNode: Scalars['Boolean']['output']; deletePatient: Scalars['Boolean']['output']; deletePropertyDefinition: Scalars['Boolean']['output']; + deleteSavedView: Scalars['Boolean']['output']; deleteTask: Scalars['Boolean']['output']; dischargePatient: PatientType; + duplicateSavedView: SavedView; markPatientDead: PatientType; reopenTask: TaskType; unassignTask: TaskType; @@ -209,6 +221,7 @@ export type Mutation = { updatePatient: PatientType; updateProfilePicture: UserType; updatePropertyDefinition: PropertyDefinitionType; + updateSavedView: SavedView; updateTask: TaskType; waitPatient: PatientType; }; @@ -251,6 +264,11 @@ export type MutationCreatePropertyDefinitionArgs = { }; +export type MutationCreateSavedViewArgs = { + data: CreateSavedViewInput; +}; + + export type MutationCreateTaskArgs = { data: CreateTaskInput; }; @@ -271,6 +289,11 @@ export type MutationDeletePropertyDefinitionArgs = { }; +export type MutationDeleteSavedViewArgs = { + id: Scalars['ID']['input']; +}; + + export type MutationDeleteTaskArgs = { id: Scalars['ID']['input']; }; @@ -281,6 +304,12 @@ export type MutationDischargePatientArgs = { }; +export type MutationDuplicateSavedViewArgs = { + id: Scalars['ID']['input']; + name: Scalars['String']['input']; +}; + + export type MutationMarkPatientDeadArgs = { id: Scalars['ID']['input']; }; @@ -324,6 +353,12 @@ export type MutationUpdatePropertyDefinitionArgs = { }; +export type MutationUpdateSavedViewArgs = { + data: UpdateSavedViewInput; + id: Scalars['ID']['input']; +}; + + export type MutationUpdateTaskArgs = { data: UpdateTaskInput; id: Scalars['ID']['input']; @@ -426,6 +461,7 @@ export type Query = { locationNodes: Array; locationRoots: Array; me?: Maybe; + mySavedViews: Array; patient?: Maybe; patients: Array; patientsTotal: Scalars['Int']['output']; @@ -434,6 +470,7 @@ export type Query = { recentPatientsTotal: Scalars['Int']['output']; recentTasks: Array; recentTasksTotal: Scalars['Int']['output']; + savedView?: Maybe; task?: Maybe; tasks: Array; tasksTotal: Scalars['Int']['output']; @@ -494,6 +531,7 @@ export type QueryPatientsTotalArgs = { export type QueryRecentPatientsArgs = { filtering?: InputMaybe>; pagination?: InputMaybe; + rootLocationIds?: InputMaybe>; search?: InputMaybe; sorting?: InputMaybe>; }; @@ -501,6 +539,7 @@ export type QueryRecentPatientsArgs = { export type QueryRecentPatientsTotalArgs = { filtering?: InputMaybe>; + rootLocationIds?: InputMaybe>; search?: InputMaybe; sorting?: InputMaybe>; }; @@ -509,6 +548,7 @@ export type QueryRecentPatientsTotalArgs = { export type QueryRecentTasksArgs = { filtering?: InputMaybe>; pagination?: InputMaybe; + rootLocationIds?: InputMaybe>; search?: InputMaybe; sorting?: InputMaybe>; }; @@ -516,11 +556,17 @@ export type QueryRecentTasksArgs = { export type QueryRecentTasksTotalArgs = { filtering?: InputMaybe>; + rootLocationIds?: InputMaybe>; search?: InputMaybe; sorting?: InputMaybe>; }; +export type QuerySavedViewArgs = { + id: Scalars['ID']['input']; +}; + + export type QueryTaskArgs = { id: Scalars['ID']['input']; }; @@ -561,6 +607,31 @@ export type QueryUsersArgs = { sorting?: InputMaybe>; }; +export type SavedView = { + __typename?: 'SavedView'; + baseEntityType: SavedViewEntityType; + createdAt: Scalars['String']['output']; + filterDefinition: Scalars['String']['output']; + id: Scalars['ID']['output']; + isOwner: Scalars['Boolean']['output']; + name: Scalars['String']['output']; + ownerUserId: Scalars['ID']['output']; + parameters: Scalars['String']['output']; + sortDefinition: Scalars['String']['output']; + updatedAt: Scalars['String']['output']; + visibility: SavedViewVisibility; +}; + +export enum SavedViewEntityType { + Patient = 'PATIENT', + Task = 'TASK' +} + +export enum SavedViewVisibility { + LinkShared = 'LINK_SHARED', + Private = 'PRIVATE' +} + export enum Sex { Female = 'FEMALE', Male = 'MALE', @@ -697,6 +768,14 @@ export type UpdatePropertyDefinitionInput = { options?: InputMaybe>; }; +export type UpdateSavedViewInput = { + filterDefinition?: InputMaybe; + name?: InputMaybe; + parameters?: InputMaybe; + sortDefinition?: InputMaybe; + visibility?: InputMaybe; +}; + export type UpdateTaskInput = { assigneeId?: InputMaybe; assigneeTeamId?: InputMaybe; @@ -921,6 +1000,48 @@ export type GetPropertiesForSubjectQueryVariables = Exact<{ export type GetPropertiesForSubjectQuery = { __typename?: 'Query', propertyDefinitions: Array<{ __typename?: 'PropertyDefinitionType', id: string, name: string, description?: string | null, fieldType: FieldType, isActive: boolean, allowedEntities: Array, options: Array }> }; +export type MySavedViewsQueryVariables = Exact<{ [key: string]: never; }>; + + +export type MySavedViewsQuery = { __typename?: 'Query', mySavedViews: Array<{ __typename?: 'SavedView', id: string, name: string, baseEntityType: SavedViewEntityType, filterDefinition: string, sortDefinition: string, parameters: string, ownerUserId: string, visibility: SavedViewVisibility, createdAt: string, updatedAt: string, isOwner: boolean }> }; + +export type SavedViewQueryVariables = Exact<{ + id: Scalars['ID']['input']; +}>; + + +export type SavedViewQuery = { __typename?: 'Query', savedView?: { __typename?: 'SavedView', id: string, name: string, baseEntityType: SavedViewEntityType, filterDefinition: string, sortDefinition: string, parameters: string, ownerUserId: string, visibility: SavedViewVisibility, createdAt: string, updatedAt: string, isOwner: boolean } | null }; + +export type CreateSavedViewMutationVariables = Exact<{ + data: CreateSavedViewInput; +}>; + + +export type CreateSavedViewMutation = { __typename?: 'Mutation', createSavedView: { __typename?: 'SavedView', id: string, name: string, baseEntityType: SavedViewEntityType, filterDefinition: string, sortDefinition: string, parameters: string, ownerUserId: string, visibility: SavedViewVisibility, createdAt: string, updatedAt: string, isOwner: boolean } }; + +export type UpdateSavedViewMutationVariables = Exact<{ + id: Scalars['ID']['input']; + data: UpdateSavedViewInput; +}>; + + +export type UpdateSavedViewMutation = { __typename?: 'Mutation', updateSavedView: { __typename?: 'SavedView', id: string, name: string, baseEntityType: SavedViewEntityType, filterDefinition: string, sortDefinition: string, parameters: string, ownerUserId: string, visibility: SavedViewVisibility, createdAt: string, updatedAt: string, isOwner: boolean } }; + +export type DeleteSavedViewMutationVariables = Exact<{ + id: Scalars['ID']['input']; +}>; + + +export type DeleteSavedViewMutation = { __typename?: 'Mutation', deleteSavedView: boolean }; + +export type DuplicateSavedViewMutationVariables = Exact<{ + id: Scalars['ID']['input']; + name: Scalars['String']['input']; +}>; + + +export type DuplicateSavedViewMutation = { __typename?: 'Mutation', duplicateSavedView: { __typename?: 'SavedView', id: string, name: string, baseEntityType: SavedViewEntityType, filterDefinition: string, sortDefinition: string, parameters: string, ownerUserId: string, visibility: SavedViewVisibility, createdAt: string, updatedAt: string, isOwner: boolean } }; + export type PatientCreatedSubscriptionVariables = Exact<{ rootLocationIds?: InputMaybe | Scalars['ID']['input']>; }>; @@ -1061,7 +1182,7 @@ export const GetAuditLogsDocument = {"kind":"Document","definitions":[{"kind":"O export const GetLocationNodeDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"GetLocationNode"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"id"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"ID"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"locationNode"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"id"},"value":{"kind":"Variable","name":{"kind":"Name","value":"id"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"title"}},{"kind":"Field","name":{"kind":"Name","value":"kind"}},{"kind":"Field","name":{"kind":"Name","value":"parentId"}},{"kind":"Field","name":{"kind":"Name","value":"parent"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"title"}},{"kind":"Field","name":{"kind":"Name","value":"kind"}},{"kind":"Field","name":{"kind":"Name","value":"parentId"}},{"kind":"Field","name":{"kind":"Name","value":"parent"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"title"}},{"kind":"Field","name":{"kind":"Name","value":"kind"}},{"kind":"Field","name":{"kind":"Name","value":"parentId"}},{"kind":"Field","name":{"kind":"Name","value":"parent"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"title"}},{"kind":"Field","name":{"kind":"Name","value":"kind"}},{"kind":"Field","name":{"kind":"Name","value":"parentId"}},{"kind":"Field","name":{"kind":"Name","value":"parent"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"title"}},{"kind":"Field","name":{"kind":"Name","value":"kind"}},{"kind":"Field","name":{"kind":"Name","value":"parentId"}}]}}]}}]}}]}}]}}]}}]} as unknown as DocumentNode; export const GetLocationsDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"GetLocations"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"limit"}},"type":{"kind":"NamedType","name":{"kind":"Name","value":"Int"}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"offset"}},"type":{"kind":"NamedType","name":{"kind":"Name","value":"Int"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"locationNodes"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"limit"},"value":{"kind":"Variable","name":{"kind":"Name","value":"limit"}}},{"kind":"Argument","name":{"kind":"Name","value":"offset"},"value":{"kind":"Variable","name":{"kind":"Name","value":"offset"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"title"}},{"kind":"Field","name":{"kind":"Name","value":"kind"}},{"kind":"Field","name":{"kind":"Name","value":"parentId"}}]}}]}}]} as unknown as DocumentNode; export const GetMyTasksDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"GetMyTasks"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"me"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"tasks"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"title"}},{"kind":"Field","name":{"kind":"Name","value":"description"}},{"kind":"Field","name":{"kind":"Name","value":"done"}},{"kind":"Field","name":{"kind":"Name","value":"dueDate"}},{"kind":"Field","name":{"kind":"Name","value":"priority"}},{"kind":"Field","name":{"kind":"Name","value":"estimatedTime"}},{"kind":"Field","name":{"kind":"Name","value":"creationDate"}},{"kind":"Field","name":{"kind":"Name","value":"updateDate"}},{"kind":"Field","name":{"kind":"Name","value":"patient"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"assignedLocation"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"title"}},{"kind":"Field","name":{"kind":"Name","value":"parent"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"title"}}]}}]}},{"kind":"Field","name":{"kind":"Name","value":"assignedLocations"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"title"}},{"kind":"Field","name":{"kind":"Name","value":"kind"}},{"kind":"Field","name":{"kind":"Name","value":"parent"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"title"}},{"kind":"Field","name":{"kind":"Name","value":"parent"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"title"}}]}}]}}]}}]}},{"kind":"Field","name":{"kind":"Name","value":"assignee"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"avatarUrl"}},{"kind":"Field","name":{"kind":"Name","value":"lastOnline"}},{"kind":"Field","name":{"kind":"Name","value":"isOnline"}}]}}]}}]}}]}}]} as unknown as DocumentNode; -export const GetOverviewDataDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"GetOverviewData"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"rootLocationIds"}},"type":{"kind":"ListType","type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"ID"}}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"recentPatientsFiltering"}},"type":{"kind":"ListType","type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"FilterInput"}}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"recentPatientsSorting"}},"type":{"kind":"ListType","type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"SortInput"}}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"recentPatientsPagination"}},"type":{"kind":"NamedType","name":{"kind":"Name","value":"PaginationInput"}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"recentPatientsSearch"}},"type":{"kind":"NamedType","name":{"kind":"Name","value":"FullTextSearchInput"}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"recentTasksFiltering"}},"type":{"kind":"ListType","type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"FilterInput"}}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"recentTasksSorting"}},"type":{"kind":"ListType","type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"SortInput"}}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"recentTasksPagination"}},"type":{"kind":"NamedType","name":{"kind":"Name","value":"PaginationInput"}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"recentTasksSearch"}},"type":{"kind":"NamedType","name":{"kind":"Name","value":"FullTextSearchInput"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"recentPatients"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"filtering"},"value":{"kind":"Variable","name":{"kind":"Name","value":"recentPatientsFiltering"}}},{"kind":"Argument","name":{"kind":"Name","value":"sorting"},"value":{"kind":"Variable","name":{"kind":"Name","value":"recentPatientsSorting"}}},{"kind":"Argument","name":{"kind":"Name","value":"pagination"},"value":{"kind":"Variable","name":{"kind":"Name","value":"recentPatientsPagination"}}},{"kind":"Argument","name":{"kind":"Name","value":"search"},"value":{"kind":"Variable","name":{"kind":"Name","value":"recentPatientsSearch"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"sex"}},{"kind":"Field","name":{"kind":"Name","value":"birthdate"}},{"kind":"Field","name":{"kind":"Name","value":"position"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"title"}},{"kind":"Field","name":{"kind":"Name","value":"kind"}},{"kind":"Field","name":{"kind":"Name","value":"parent"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"title"}}]}}]}},{"kind":"Field","name":{"kind":"Name","value":"tasks"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"updateDate"}}]}},{"kind":"Field","name":{"kind":"Name","value":"properties"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"definition"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"description"}},{"kind":"Field","name":{"kind":"Name","value":"fieldType"}},{"kind":"Field","name":{"kind":"Name","value":"isActive"}},{"kind":"Field","name":{"kind":"Name","value":"allowedEntities"}},{"kind":"Field","name":{"kind":"Name","value":"options"}}]}},{"kind":"Field","name":{"kind":"Name","value":"textValue"}},{"kind":"Field","name":{"kind":"Name","value":"numberValue"}},{"kind":"Field","name":{"kind":"Name","value":"booleanValue"}},{"kind":"Field","name":{"kind":"Name","value":"dateValue"}},{"kind":"Field","name":{"kind":"Name","value":"dateTimeValue"}},{"kind":"Field","name":{"kind":"Name","value":"selectValue"}},{"kind":"Field","name":{"kind":"Name","value":"multiSelectValues"}},{"kind":"Field","name":{"kind":"Name","value":"userValue"}},{"kind":"Field","name":{"kind":"Name","value":"user"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"avatarUrl"}},{"kind":"Field","name":{"kind":"Name","value":"lastOnline"}},{"kind":"Field","name":{"kind":"Name","value":"isOnline"}}]}},{"kind":"Field","name":{"kind":"Name","value":"team"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"title"}},{"kind":"Field","name":{"kind":"Name","value":"kind"}}]}}]}}]}},{"kind":"Field","name":{"kind":"Name","value":"recentPatientsTotal"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"rootLocationIds"},"value":{"kind":"Variable","name":{"kind":"Name","value":"rootLocationIds"}}},{"kind":"Argument","name":{"kind":"Name","value":"filtering"},"value":{"kind":"Variable","name":{"kind":"Name","value":"recentPatientsFiltering"}}},{"kind":"Argument","name":{"kind":"Name","value":"sorting"},"value":{"kind":"Variable","name":{"kind":"Name","value":"recentPatientsSorting"}}},{"kind":"Argument","name":{"kind":"Name","value":"search"},"value":{"kind":"Variable","name":{"kind":"Name","value":"recentPatientsSearch"}}}]},{"kind":"Field","name":{"kind":"Name","value":"recentTasks"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"filtering"},"value":{"kind":"Variable","name":{"kind":"Name","value":"recentTasksFiltering"}}},{"kind":"Argument","name":{"kind":"Name","value":"sorting"},"value":{"kind":"Variable","name":{"kind":"Name","value":"recentTasksSorting"}}},{"kind":"Argument","name":{"kind":"Name","value":"pagination"},"value":{"kind":"Variable","name":{"kind":"Name","value":"recentTasksPagination"}}},{"kind":"Argument","name":{"kind":"Name","value":"search"},"value":{"kind":"Variable","name":{"kind":"Name","value":"recentTasksSearch"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"title"}},{"kind":"Field","name":{"kind":"Name","value":"description"}},{"kind":"Field","name":{"kind":"Name","value":"done"}},{"kind":"Field","name":{"kind":"Name","value":"dueDate"}},{"kind":"Field","name":{"kind":"Name","value":"updateDate"}},{"kind":"Field","name":{"kind":"Name","value":"priority"}},{"kind":"Field","name":{"kind":"Name","value":"assignee"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"avatarUrl"}},{"kind":"Field","name":{"kind":"Name","value":"lastOnline"}},{"kind":"Field","name":{"kind":"Name","value":"isOnline"}}]}},{"kind":"Field","name":{"kind":"Name","value":"patient"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"position"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"title"}},{"kind":"Field","name":{"kind":"Name","value":"kind"}},{"kind":"Field","name":{"kind":"Name","value":"parent"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"title"}}]}}]}}]}},{"kind":"Field","name":{"kind":"Name","value":"properties"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"definition"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"description"}},{"kind":"Field","name":{"kind":"Name","value":"fieldType"}},{"kind":"Field","name":{"kind":"Name","value":"isActive"}},{"kind":"Field","name":{"kind":"Name","value":"allowedEntities"}},{"kind":"Field","name":{"kind":"Name","value":"options"}}]}},{"kind":"Field","name":{"kind":"Name","value":"textValue"}},{"kind":"Field","name":{"kind":"Name","value":"numberValue"}},{"kind":"Field","name":{"kind":"Name","value":"booleanValue"}},{"kind":"Field","name":{"kind":"Name","value":"dateValue"}},{"kind":"Field","name":{"kind":"Name","value":"dateTimeValue"}},{"kind":"Field","name":{"kind":"Name","value":"selectValue"}},{"kind":"Field","name":{"kind":"Name","value":"multiSelectValues"}},{"kind":"Field","name":{"kind":"Name","value":"userValue"}},{"kind":"Field","name":{"kind":"Name","value":"user"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"avatarUrl"}},{"kind":"Field","name":{"kind":"Name","value":"lastOnline"}},{"kind":"Field","name":{"kind":"Name","value":"isOnline"}}]}},{"kind":"Field","name":{"kind":"Name","value":"team"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"title"}},{"kind":"Field","name":{"kind":"Name","value":"kind"}}]}}]}}]}},{"kind":"Field","name":{"kind":"Name","value":"recentTasksTotal"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"rootLocationIds"},"value":{"kind":"Variable","name":{"kind":"Name","value":"rootLocationIds"}}},{"kind":"Argument","name":{"kind":"Name","value":"filtering"},"value":{"kind":"Variable","name":{"kind":"Name","value":"recentTasksFiltering"}}},{"kind":"Argument","name":{"kind":"Name","value":"sorting"},"value":{"kind":"Variable","name":{"kind":"Name","value":"recentTasksSorting"}}},{"kind":"Argument","name":{"kind":"Name","value":"search"},"value":{"kind":"Variable","name":{"kind":"Name","value":"recentTasksSearch"}}}]}]}}]} as unknown as DocumentNode; +export const GetOverviewDataDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"GetOverviewData"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"rootLocationIds"}},"type":{"kind":"ListType","type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"ID"}}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"recentPatientsFiltering"}},"type":{"kind":"ListType","type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"FilterInput"}}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"recentPatientsSorting"}},"type":{"kind":"ListType","type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"SortInput"}}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"recentPatientsPagination"}},"type":{"kind":"NamedType","name":{"kind":"Name","value":"PaginationInput"}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"recentPatientsSearch"}},"type":{"kind":"NamedType","name":{"kind":"Name","value":"FullTextSearchInput"}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"recentTasksFiltering"}},"type":{"kind":"ListType","type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"FilterInput"}}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"recentTasksSorting"}},"type":{"kind":"ListType","type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"SortInput"}}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"recentTasksPagination"}},"type":{"kind":"NamedType","name":{"kind":"Name","value":"PaginationInput"}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"recentTasksSearch"}},"type":{"kind":"NamedType","name":{"kind":"Name","value":"FullTextSearchInput"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"recentPatients"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"rootLocationIds"},"value":{"kind":"Variable","name":{"kind":"Name","value":"rootLocationIds"}}},{"kind":"Argument","name":{"kind":"Name","value":"filtering"},"value":{"kind":"Variable","name":{"kind":"Name","value":"recentPatientsFiltering"}}},{"kind":"Argument","name":{"kind":"Name","value":"sorting"},"value":{"kind":"Variable","name":{"kind":"Name","value":"recentPatientsSorting"}}},{"kind":"Argument","name":{"kind":"Name","value":"pagination"},"value":{"kind":"Variable","name":{"kind":"Name","value":"recentPatientsPagination"}}},{"kind":"Argument","name":{"kind":"Name","value":"search"},"value":{"kind":"Variable","name":{"kind":"Name","value":"recentPatientsSearch"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"sex"}},{"kind":"Field","name":{"kind":"Name","value":"birthdate"}},{"kind":"Field","name":{"kind":"Name","value":"position"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"title"}},{"kind":"Field","name":{"kind":"Name","value":"kind"}},{"kind":"Field","name":{"kind":"Name","value":"parent"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"title"}}]}}]}},{"kind":"Field","name":{"kind":"Name","value":"tasks"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"updateDate"}}]}},{"kind":"Field","name":{"kind":"Name","value":"properties"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"definition"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"description"}},{"kind":"Field","name":{"kind":"Name","value":"fieldType"}},{"kind":"Field","name":{"kind":"Name","value":"isActive"}},{"kind":"Field","name":{"kind":"Name","value":"allowedEntities"}},{"kind":"Field","name":{"kind":"Name","value":"options"}}]}},{"kind":"Field","name":{"kind":"Name","value":"textValue"}},{"kind":"Field","name":{"kind":"Name","value":"numberValue"}},{"kind":"Field","name":{"kind":"Name","value":"booleanValue"}},{"kind":"Field","name":{"kind":"Name","value":"dateValue"}},{"kind":"Field","name":{"kind":"Name","value":"dateTimeValue"}},{"kind":"Field","name":{"kind":"Name","value":"selectValue"}},{"kind":"Field","name":{"kind":"Name","value":"multiSelectValues"}},{"kind":"Field","name":{"kind":"Name","value":"userValue"}},{"kind":"Field","name":{"kind":"Name","value":"user"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"avatarUrl"}},{"kind":"Field","name":{"kind":"Name","value":"lastOnline"}},{"kind":"Field","name":{"kind":"Name","value":"isOnline"}}]}},{"kind":"Field","name":{"kind":"Name","value":"team"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"title"}},{"kind":"Field","name":{"kind":"Name","value":"kind"}}]}}]}}]}},{"kind":"Field","name":{"kind":"Name","value":"recentPatientsTotal"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"rootLocationIds"},"value":{"kind":"Variable","name":{"kind":"Name","value":"rootLocationIds"}}},{"kind":"Argument","name":{"kind":"Name","value":"filtering"},"value":{"kind":"Variable","name":{"kind":"Name","value":"recentPatientsFiltering"}}},{"kind":"Argument","name":{"kind":"Name","value":"sorting"},"value":{"kind":"Variable","name":{"kind":"Name","value":"recentPatientsSorting"}}},{"kind":"Argument","name":{"kind":"Name","value":"search"},"value":{"kind":"Variable","name":{"kind":"Name","value":"recentPatientsSearch"}}}]},{"kind":"Field","name":{"kind":"Name","value":"recentTasks"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"rootLocationIds"},"value":{"kind":"Variable","name":{"kind":"Name","value":"rootLocationIds"}}},{"kind":"Argument","name":{"kind":"Name","value":"filtering"},"value":{"kind":"Variable","name":{"kind":"Name","value":"recentTasksFiltering"}}},{"kind":"Argument","name":{"kind":"Name","value":"sorting"},"value":{"kind":"Variable","name":{"kind":"Name","value":"recentTasksSorting"}}},{"kind":"Argument","name":{"kind":"Name","value":"pagination"},"value":{"kind":"Variable","name":{"kind":"Name","value":"recentTasksPagination"}}},{"kind":"Argument","name":{"kind":"Name","value":"search"},"value":{"kind":"Variable","name":{"kind":"Name","value":"recentTasksSearch"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"title"}},{"kind":"Field","name":{"kind":"Name","value":"description"}},{"kind":"Field","name":{"kind":"Name","value":"done"}},{"kind":"Field","name":{"kind":"Name","value":"dueDate"}},{"kind":"Field","name":{"kind":"Name","value":"updateDate"}},{"kind":"Field","name":{"kind":"Name","value":"priority"}},{"kind":"Field","name":{"kind":"Name","value":"assignee"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"avatarUrl"}},{"kind":"Field","name":{"kind":"Name","value":"lastOnline"}},{"kind":"Field","name":{"kind":"Name","value":"isOnline"}}]}},{"kind":"Field","name":{"kind":"Name","value":"patient"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"position"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"title"}},{"kind":"Field","name":{"kind":"Name","value":"kind"}},{"kind":"Field","name":{"kind":"Name","value":"parent"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"title"}}]}}]}}]}},{"kind":"Field","name":{"kind":"Name","value":"properties"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"definition"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"description"}},{"kind":"Field","name":{"kind":"Name","value":"fieldType"}},{"kind":"Field","name":{"kind":"Name","value":"isActive"}},{"kind":"Field","name":{"kind":"Name","value":"allowedEntities"}},{"kind":"Field","name":{"kind":"Name","value":"options"}}]}},{"kind":"Field","name":{"kind":"Name","value":"textValue"}},{"kind":"Field","name":{"kind":"Name","value":"numberValue"}},{"kind":"Field","name":{"kind":"Name","value":"booleanValue"}},{"kind":"Field","name":{"kind":"Name","value":"dateValue"}},{"kind":"Field","name":{"kind":"Name","value":"dateTimeValue"}},{"kind":"Field","name":{"kind":"Name","value":"selectValue"}},{"kind":"Field","name":{"kind":"Name","value":"multiSelectValues"}},{"kind":"Field","name":{"kind":"Name","value":"userValue"}},{"kind":"Field","name":{"kind":"Name","value":"user"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"avatarUrl"}},{"kind":"Field","name":{"kind":"Name","value":"lastOnline"}},{"kind":"Field","name":{"kind":"Name","value":"isOnline"}}]}},{"kind":"Field","name":{"kind":"Name","value":"team"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"title"}},{"kind":"Field","name":{"kind":"Name","value":"kind"}}]}}]}}]}},{"kind":"Field","name":{"kind":"Name","value":"recentTasksTotal"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"rootLocationIds"},"value":{"kind":"Variable","name":{"kind":"Name","value":"rootLocationIds"}}},{"kind":"Argument","name":{"kind":"Name","value":"filtering"},"value":{"kind":"Variable","name":{"kind":"Name","value":"recentTasksFiltering"}}},{"kind":"Argument","name":{"kind":"Name","value":"sorting"},"value":{"kind":"Variable","name":{"kind":"Name","value":"recentTasksSorting"}}},{"kind":"Argument","name":{"kind":"Name","value":"search"},"value":{"kind":"Variable","name":{"kind":"Name","value":"recentTasksSearch"}}}]}]}}]} as unknown as DocumentNode; export const GetPatientDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"GetPatient"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"id"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"ID"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"patient"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"id"},"value":{"kind":"Variable","name":{"kind":"Name","value":"id"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"firstname"}},{"kind":"Field","name":{"kind":"Name","value":"lastname"}},{"kind":"Field","name":{"kind":"Name","value":"birthdate"}},{"kind":"Field","name":{"kind":"Name","value":"sex"}},{"kind":"Field","name":{"kind":"Name","value":"state"}},{"kind":"Field","name":{"kind":"Name","value":"description"}},{"kind":"Field","name":{"kind":"Name","value":"checksum"}},{"kind":"Field","name":{"kind":"Name","value":"assignedLocation"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"title"}}]}},{"kind":"Field","name":{"kind":"Name","value":"assignedLocations"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"title"}}]}},{"kind":"Field","name":{"kind":"Name","value":"clinic"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"title"}},{"kind":"Field","name":{"kind":"Name","value":"kind"}},{"kind":"Field","name":{"kind":"Name","value":"parent"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"title"}},{"kind":"Field","name":{"kind":"Name","value":"parent"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"title"}},{"kind":"Field","name":{"kind":"Name","value":"parent"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"title"}},{"kind":"Field","name":{"kind":"Name","value":"parent"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"title"}}]}}]}}]}}]}}]}},{"kind":"Field","name":{"kind":"Name","value":"position"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"title"}},{"kind":"Field","name":{"kind":"Name","value":"kind"}},{"kind":"Field","name":{"kind":"Name","value":"parent"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"title"}},{"kind":"Field","name":{"kind":"Name","value":"parent"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"title"}},{"kind":"Field","name":{"kind":"Name","value":"parent"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"title"}},{"kind":"Field","name":{"kind":"Name","value":"parent"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"title"}}]}}]}}]}}]}}]}},{"kind":"Field","name":{"kind":"Name","value":"teams"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"title"}},{"kind":"Field","name":{"kind":"Name","value":"kind"}},{"kind":"Field","name":{"kind":"Name","value":"parent"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"title"}},{"kind":"Field","name":{"kind":"Name","value":"parent"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"title"}},{"kind":"Field","name":{"kind":"Name","value":"parent"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"title"}},{"kind":"Field","name":{"kind":"Name","value":"parent"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"title"}}]}}]}}]}}]}}]}},{"kind":"Field","name":{"kind":"Name","value":"tasks"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"title"}},{"kind":"Field","name":{"kind":"Name","value":"description"}},{"kind":"Field","name":{"kind":"Name","value":"done"}},{"kind":"Field","name":{"kind":"Name","value":"dueDate"}},{"kind":"Field","name":{"kind":"Name","value":"priority"}},{"kind":"Field","name":{"kind":"Name","value":"estimatedTime"}},{"kind":"Field","name":{"kind":"Name","value":"updateDate"}},{"kind":"Field","name":{"kind":"Name","value":"assignee"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"avatarUrl"}},{"kind":"Field","name":{"kind":"Name","value":"lastOnline"}},{"kind":"Field","name":{"kind":"Name","value":"isOnline"}}]}},{"kind":"Field","name":{"kind":"Name","value":"assigneeTeam"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"title"}},{"kind":"Field","name":{"kind":"Name","value":"kind"}}]}}]}},{"kind":"Field","name":{"kind":"Name","value":"properties"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"definition"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"description"}},{"kind":"Field","name":{"kind":"Name","value":"fieldType"}},{"kind":"Field","name":{"kind":"Name","value":"isActive"}},{"kind":"Field","name":{"kind":"Name","value":"allowedEntities"}},{"kind":"Field","name":{"kind":"Name","value":"options"}}]}},{"kind":"Field","name":{"kind":"Name","value":"textValue"}},{"kind":"Field","name":{"kind":"Name","value":"numberValue"}},{"kind":"Field","name":{"kind":"Name","value":"booleanValue"}},{"kind":"Field","name":{"kind":"Name","value":"dateValue"}},{"kind":"Field","name":{"kind":"Name","value":"dateTimeValue"}},{"kind":"Field","name":{"kind":"Name","value":"selectValue"}},{"kind":"Field","name":{"kind":"Name","value":"multiSelectValues"}},{"kind":"Field","name":{"kind":"Name","value":"userValue"}},{"kind":"Field","name":{"kind":"Name","value":"user"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"avatarUrl"}},{"kind":"Field","name":{"kind":"Name","value":"lastOnline"}},{"kind":"Field","name":{"kind":"Name","value":"isOnline"}}]}},{"kind":"Field","name":{"kind":"Name","value":"team"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"title"}},{"kind":"Field","name":{"kind":"Name","value":"kind"}}]}}]}}]}}]}}]} as unknown as DocumentNode; export const GetPatientsDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"GetPatients"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"locationId"}},"type":{"kind":"NamedType","name":{"kind":"Name","value":"ID"}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"rootLocationIds"}},"type":{"kind":"ListType","type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"ID"}}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"states"}},"type":{"kind":"ListType","type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"PatientState"}}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"filtering"}},"type":{"kind":"ListType","type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"FilterInput"}}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"sorting"}},"type":{"kind":"ListType","type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"SortInput"}}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"pagination"}},"type":{"kind":"NamedType","name":{"kind":"Name","value":"PaginationInput"}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"search"}},"type":{"kind":"NamedType","name":{"kind":"Name","value":"FullTextSearchInput"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"patients"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"locationNodeId"},"value":{"kind":"Variable","name":{"kind":"Name","value":"locationId"}}},{"kind":"Argument","name":{"kind":"Name","value":"rootLocationIds"},"value":{"kind":"Variable","name":{"kind":"Name","value":"rootLocationIds"}}},{"kind":"Argument","name":{"kind":"Name","value":"states"},"value":{"kind":"Variable","name":{"kind":"Name","value":"states"}}},{"kind":"Argument","name":{"kind":"Name","value":"filtering"},"value":{"kind":"Variable","name":{"kind":"Name","value":"filtering"}}},{"kind":"Argument","name":{"kind":"Name","value":"sorting"},"value":{"kind":"Variable","name":{"kind":"Name","value":"sorting"}}},{"kind":"Argument","name":{"kind":"Name","value":"pagination"},"value":{"kind":"Variable","name":{"kind":"Name","value":"pagination"}}},{"kind":"Argument","name":{"kind":"Name","value":"search"},"value":{"kind":"Variable","name":{"kind":"Name","value":"search"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"firstname"}},{"kind":"Field","name":{"kind":"Name","value":"lastname"}},{"kind":"Field","name":{"kind":"Name","value":"birthdate"}},{"kind":"Field","name":{"kind":"Name","value":"sex"}},{"kind":"Field","name":{"kind":"Name","value":"state"}},{"kind":"Field","name":{"kind":"Name","value":"assignedLocation"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"title"}},{"kind":"Field","name":{"kind":"Name","value":"parent"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"title"}}]}}]}},{"kind":"Field","name":{"kind":"Name","value":"assignedLocations"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"title"}},{"kind":"Field","name":{"kind":"Name","value":"kind"}},{"kind":"Field","name":{"kind":"Name","value":"parent"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"title"}},{"kind":"Field","name":{"kind":"Name","value":"parent"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"title"}},{"kind":"Field","name":{"kind":"Name","value":"parent"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"title"}}]}}]}}]}}]}},{"kind":"Field","name":{"kind":"Name","value":"clinic"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"title"}},{"kind":"Field","name":{"kind":"Name","value":"kind"}},{"kind":"Field","name":{"kind":"Name","value":"parent"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"title"}},{"kind":"Field","name":{"kind":"Name","value":"parent"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"title"}},{"kind":"Field","name":{"kind":"Name","value":"parent"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"title"}},{"kind":"Field","name":{"kind":"Name","value":"parent"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"title"}}]}}]}}]}}]}}]}},{"kind":"Field","name":{"kind":"Name","value":"position"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"title"}},{"kind":"Field","name":{"kind":"Name","value":"kind"}},{"kind":"Field","name":{"kind":"Name","value":"parent"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"title"}},{"kind":"Field","name":{"kind":"Name","value":"kind"}},{"kind":"Field","name":{"kind":"Name","value":"parent"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"title"}},{"kind":"Field","name":{"kind":"Name","value":"kind"}},{"kind":"Field","name":{"kind":"Name","value":"parent"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"title"}},{"kind":"Field","name":{"kind":"Name","value":"kind"}},{"kind":"Field","name":{"kind":"Name","value":"parent"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"title"}},{"kind":"Field","name":{"kind":"Name","value":"kind"}}]}}]}}]}}]}}]}},{"kind":"Field","name":{"kind":"Name","value":"teams"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"title"}},{"kind":"Field","name":{"kind":"Name","value":"kind"}},{"kind":"Field","name":{"kind":"Name","value":"parent"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"title"}},{"kind":"Field","name":{"kind":"Name","value":"parent"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"title"}},{"kind":"Field","name":{"kind":"Name","value":"parent"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"title"}},{"kind":"Field","name":{"kind":"Name","value":"parent"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"title"}}]}}]}}]}}]}}]}},{"kind":"Field","name":{"kind":"Name","value":"tasks"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"title"}},{"kind":"Field","name":{"kind":"Name","value":"description"}},{"kind":"Field","name":{"kind":"Name","value":"done"}},{"kind":"Field","name":{"kind":"Name","value":"dueDate"}},{"kind":"Field","name":{"kind":"Name","value":"priority"}},{"kind":"Field","name":{"kind":"Name","value":"estimatedTime"}},{"kind":"Field","name":{"kind":"Name","value":"creationDate"}},{"kind":"Field","name":{"kind":"Name","value":"updateDate"}},{"kind":"Field","name":{"kind":"Name","value":"assignee"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"avatarUrl"}},{"kind":"Field","name":{"kind":"Name","value":"lastOnline"}},{"kind":"Field","name":{"kind":"Name","value":"isOnline"}}]}},{"kind":"Field","name":{"kind":"Name","value":"assigneeTeam"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"title"}},{"kind":"Field","name":{"kind":"Name","value":"kind"}}]}}]}},{"kind":"Field","name":{"kind":"Name","value":"properties"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"definition"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"description"}},{"kind":"Field","name":{"kind":"Name","value":"fieldType"}},{"kind":"Field","name":{"kind":"Name","value":"isActive"}},{"kind":"Field","name":{"kind":"Name","value":"allowedEntities"}},{"kind":"Field","name":{"kind":"Name","value":"options"}}]}},{"kind":"Field","name":{"kind":"Name","value":"textValue"}},{"kind":"Field","name":{"kind":"Name","value":"numberValue"}},{"kind":"Field","name":{"kind":"Name","value":"booleanValue"}},{"kind":"Field","name":{"kind":"Name","value":"dateValue"}},{"kind":"Field","name":{"kind":"Name","value":"dateTimeValue"}},{"kind":"Field","name":{"kind":"Name","value":"selectValue"}},{"kind":"Field","name":{"kind":"Name","value":"multiSelectValues"}},{"kind":"Field","name":{"kind":"Name","value":"userValue"}},{"kind":"Field","name":{"kind":"Name","value":"user"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"avatarUrl"}},{"kind":"Field","name":{"kind":"Name","value":"lastOnline"}},{"kind":"Field","name":{"kind":"Name","value":"isOnline"}}]}},{"kind":"Field","name":{"kind":"Name","value":"team"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"title"}},{"kind":"Field","name":{"kind":"Name","value":"kind"}}]}}]}}]}},{"kind":"Field","name":{"kind":"Name","value":"patientsTotal"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"locationNodeId"},"value":{"kind":"Variable","name":{"kind":"Name","value":"locationId"}}},{"kind":"Argument","name":{"kind":"Name","value":"rootLocationIds"},"value":{"kind":"Variable","name":{"kind":"Name","value":"rootLocationIds"}}},{"kind":"Argument","name":{"kind":"Name","value":"states"},"value":{"kind":"Variable","name":{"kind":"Name","value":"states"}}},{"kind":"Argument","name":{"kind":"Name","value":"filtering"},"value":{"kind":"Variable","name":{"kind":"Name","value":"filtering"}}},{"kind":"Argument","name":{"kind":"Name","value":"sorting"},"value":{"kind":"Variable","name":{"kind":"Name","value":"sorting"}}},{"kind":"Argument","name":{"kind":"Name","value":"search"},"value":{"kind":"Variable","name":{"kind":"Name","value":"search"}}}]}]}}]} as unknown as DocumentNode; export const GetTaskDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"GetTask"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"id"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"ID"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"task"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"id"},"value":{"kind":"Variable","name":{"kind":"Name","value":"id"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"title"}},{"kind":"Field","name":{"kind":"Name","value":"description"}},{"kind":"Field","name":{"kind":"Name","value":"done"}},{"kind":"Field","name":{"kind":"Name","value":"dueDate"}},{"kind":"Field","name":{"kind":"Name","value":"priority"}},{"kind":"Field","name":{"kind":"Name","value":"estimatedTime"}},{"kind":"Field","name":{"kind":"Name","value":"checksum"}},{"kind":"Field","name":{"kind":"Name","value":"updateDate"}},{"kind":"Field","name":{"kind":"Name","value":"patient"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}}]}},{"kind":"Field","name":{"kind":"Name","value":"assignee"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"avatarUrl"}},{"kind":"Field","name":{"kind":"Name","value":"lastOnline"}},{"kind":"Field","name":{"kind":"Name","value":"isOnline"}}]}},{"kind":"Field","name":{"kind":"Name","value":"assigneeTeam"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"title"}},{"kind":"Field","name":{"kind":"Name","value":"kind"}}]}},{"kind":"Field","name":{"kind":"Name","value":"properties"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"definition"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"description"}},{"kind":"Field","name":{"kind":"Name","value":"fieldType"}},{"kind":"Field","name":{"kind":"Name","value":"isActive"}},{"kind":"Field","name":{"kind":"Name","value":"allowedEntities"}},{"kind":"Field","name":{"kind":"Name","value":"options"}}]}},{"kind":"Field","name":{"kind":"Name","value":"textValue"}},{"kind":"Field","name":{"kind":"Name","value":"numberValue"}},{"kind":"Field","name":{"kind":"Name","value":"booleanValue"}},{"kind":"Field","name":{"kind":"Name","value":"dateValue"}},{"kind":"Field","name":{"kind":"Name","value":"dateTimeValue"}},{"kind":"Field","name":{"kind":"Name","value":"selectValue"}},{"kind":"Field","name":{"kind":"Name","value":"multiSelectValues"}},{"kind":"Field","name":{"kind":"Name","value":"userValue"}},{"kind":"Field","name":{"kind":"Name","value":"user"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"avatarUrl"}},{"kind":"Field","name":{"kind":"Name","value":"lastOnline"}},{"kind":"Field","name":{"kind":"Name","value":"isOnline"}}]}},{"kind":"Field","name":{"kind":"Name","value":"team"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"title"}},{"kind":"Field","name":{"kind":"Name","value":"kind"}}]}}]}}]}}]}}]} as unknown as DocumentNode; @@ -1081,6 +1202,12 @@ export const UpdatePropertyDefinitionDocument = {"kind":"Document","definitions" export const DeletePropertyDefinitionDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"DeletePropertyDefinition"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"id"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"ID"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"deletePropertyDefinition"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"id"},"value":{"kind":"Variable","name":{"kind":"Name","value":"id"}}}]}]}}]} as unknown as DocumentNode; export const GetPropertyDefinitionsDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"GetPropertyDefinitions"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"propertyDefinitions"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"description"}},{"kind":"Field","name":{"kind":"Name","value":"fieldType"}},{"kind":"Field","name":{"kind":"Name","value":"isActive"}},{"kind":"Field","name":{"kind":"Name","value":"allowedEntities"}},{"kind":"Field","name":{"kind":"Name","value":"options"}}]}}]}}]} as unknown as DocumentNode; export const GetPropertiesForSubjectDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"GetPropertiesForSubject"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"subjectId"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"ID"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"subjectType"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"PropertyEntity"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"propertyDefinitions"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"description"}},{"kind":"Field","name":{"kind":"Name","value":"fieldType"}},{"kind":"Field","name":{"kind":"Name","value":"isActive"}},{"kind":"Field","name":{"kind":"Name","value":"allowedEntities"}},{"kind":"Field","name":{"kind":"Name","value":"options"}}]}}]}}]} as unknown as DocumentNode; +export const MySavedViewsDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"MySavedViews"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"mySavedViews"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"baseEntityType"}},{"kind":"Field","name":{"kind":"Name","value":"filterDefinition"}},{"kind":"Field","name":{"kind":"Name","value":"sortDefinition"}},{"kind":"Field","name":{"kind":"Name","value":"parameters"}},{"kind":"Field","name":{"kind":"Name","value":"ownerUserId"}},{"kind":"Field","name":{"kind":"Name","value":"visibility"}},{"kind":"Field","name":{"kind":"Name","value":"createdAt"}},{"kind":"Field","name":{"kind":"Name","value":"updatedAt"}},{"kind":"Field","name":{"kind":"Name","value":"isOwner"}}]}}]}}]} as unknown as DocumentNode; +export const SavedViewDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"SavedView"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"id"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"ID"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"savedView"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"id"},"value":{"kind":"Variable","name":{"kind":"Name","value":"id"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"baseEntityType"}},{"kind":"Field","name":{"kind":"Name","value":"filterDefinition"}},{"kind":"Field","name":{"kind":"Name","value":"sortDefinition"}},{"kind":"Field","name":{"kind":"Name","value":"parameters"}},{"kind":"Field","name":{"kind":"Name","value":"ownerUserId"}},{"kind":"Field","name":{"kind":"Name","value":"visibility"}},{"kind":"Field","name":{"kind":"Name","value":"createdAt"}},{"kind":"Field","name":{"kind":"Name","value":"updatedAt"}},{"kind":"Field","name":{"kind":"Name","value":"isOwner"}}]}}]}}]} as unknown as DocumentNode; +export const CreateSavedViewDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"CreateSavedView"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"data"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"CreateSavedViewInput"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"createSavedView"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"data"},"value":{"kind":"Variable","name":{"kind":"Name","value":"data"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"baseEntityType"}},{"kind":"Field","name":{"kind":"Name","value":"filterDefinition"}},{"kind":"Field","name":{"kind":"Name","value":"sortDefinition"}},{"kind":"Field","name":{"kind":"Name","value":"parameters"}},{"kind":"Field","name":{"kind":"Name","value":"ownerUserId"}},{"kind":"Field","name":{"kind":"Name","value":"visibility"}},{"kind":"Field","name":{"kind":"Name","value":"createdAt"}},{"kind":"Field","name":{"kind":"Name","value":"updatedAt"}},{"kind":"Field","name":{"kind":"Name","value":"isOwner"}}]}}]}}]} as unknown as DocumentNode; +export const UpdateSavedViewDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"UpdateSavedView"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"id"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"ID"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"data"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"UpdateSavedViewInput"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"updateSavedView"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"id"},"value":{"kind":"Variable","name":{"kind":"Name","value":"id"}}},{"kind":"Argument","name":{"kind":"Name","value":"data"},"value":{"kind":"Variable","name":{"kind":"Name","value":"data"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"baseEntityType"}},{"kind":"Field","name":{"kind":"Name","value":"filterDefinition"}},{"kind":"Field","name":{"kind":"Name","value":"sortDefinition"}},{"kind":"Field","name":{"kind":"Name","value":"parameters"}},{"kind":"Field","name":{"kind":"Name","value":"ownerUserId"}},{"kind":"Field","name":{"kind":"Name","value":"visibility"}},{"kind":"Field","name":{"kind":"Name","value":"createdAt"}},{"kind":"Field","name":{"kind":"Name","value":"updatedAt"}},{"kind":"Field","name":{"kind":"Name","value":"isOwner"}}]}}]}}]} as unknown as DocumentNode; +export const DeleteSavedViewDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"DeleteSavedView"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"id"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"ID"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"deleteSavedView"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"id"},"value":{"kind":"Variable","name":{"kind":"Name","value":"id"}}}]}]}}]} as unknown as DocumentNode; +export const DuplicateSavedViewDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"DuplicateSavedView"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"id"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"ID"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"name"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"duplicateSavedView"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"id"},"value":{"kind":"Variable","name":{"kind":"Name","value":"id"}}},{"kind":"Argument","name":{"kind":"Name","value":"name"},"value":{"kind":"Variable","name":{"kind":"Name","value":"name"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"baseEntityType"}},{"kind":"Field","name":{"kind":"Name","value":"filterDefinition"}},{"kind":"Field","name":{"kind":"Name","value":"sortDefinition"}},{"kind":"Field","name":{"kind":"Name","value":"parameters"}},{"kind":"Field","name":{"kind":"Name","value":"ownerUserId"}},{"kind":"Field","name":{"kind":"Name","value":"visibility"}},{"kind":"Field","name":{"kind":"Name","value":"createdAt"}},{"kind":"Field","name":{"kind":"Name","value":"updatedAt"}},{"kind":"Field","name":{"kind":"Name","value":"isOwner"}}]}}]}}]} as unknown as DocumentNode; export const PatientCreatedDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"subscription","name":{"kind":"Name","value":"PatientCreated"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"rootLocationIds"}},"type":{"kind":"ListType","type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"ID"}}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"patientCreated"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"rootLocationIds"},"value":{"kind":"Variable","name":{"kind":"Name","value":"rootLocationIds"}}}]}]}}]} as unknown as DocumentNode; export const PatientUpdatedDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"subscription","name":{"kind":"Name","value":"PatientUpdated"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"patientId"}},"type":{"kind":"NamedType","name":{"kind":"Name","value":"ID"}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"rootLocationIds"}},"type":{"kind":"ListType","type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"ID"}}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"patientUpdated"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"patientId"},"value":{"kind":"Variable","name":{"kind":"Name","value":"patientId"}}},{"kind":"Argument","name":{"kind":"Name","value":"rootLocationIds"},"value":{"kind":"Variable","name":{"kind":"Name","value":"rootLocationIds"}}}]}]}}]} as unknown as DocumentNode; export const PatientStateChangedDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"subscription","name":{"kind":"Name","value":"PatientStateChanged"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"patientId"}},"type":{"kind":"NamedType","name":{"kind":"Name","value":"ID"}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"rootLocationIds"}},"type":{"kind":"ListType","type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"ID"}}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"patientStateChanged"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"patientId"},"value":{"kind":"Variable","name":{"kind":"Name","value":"patientId"}}},{"kind":"Argument","name":{"kind":"Name","value":"rootLocationIds"},"value":{"kind":"Variable","name":{"kind":"Name","value":"rootLocationIds"}}}]}]}}]} as unknown as DocumentNode; diff --git a/web/api/graphql/SavedView.graphql b/web/api/graphql/SavedView.graphql new file mode 100644 index 0000000..dea0394 --- /dev/null +++ b/web/api/graphql/SavedView.graphql @@ -0,0 +1,83 @@ +query MySavedViews { + mySavedViews { + id + name + baseEntityType + filterDefinition + sortDefinition + parameters + ownerUserId + visibility + createdAt + updatedAt + isOwner + } +} + +query SavedView($id: ID!) { + savedView(id: $id) { + id + name + baseEntityType + filterDefinition + sortDefinition + parameters + ownerUserId + visibility + createdAt + updatedAt + isOwner + } +} + +mutation CreateSavedView($data: CreateSavedViewInput!) { + createSavedView(data: $data) { + id + name + baseEntityType + filterDefinition + sortDefinition + parameters + ownerUserId + visibility + createdAt + updatedAt + isOwner + } +} + +mutation UpdateSavedView($id: ID!, $data: UpdateSavedViewInput!) { + updateSavedView(id: $id, data: $data) { + id + name + baseEntityType + filterDefinition + sortDefinition + parameters + ownerUserId + visibility + createdAt + updatedAt + isOwner + } +} + +mutation DeleteSavedView($id: ID!) { + deleteSavedView(id: $id) +} + +mutation DuplicateSavedView($id: ID!, $name: String!) { + duplicateSavedView(id: $id, name: $name) { + id + name + baseEntityType + filterDefinition + sortDefinition + parameters + ownerUserId + visibility + createdAt + updatedAt + isOwner + } +} diff --git a/web/components/layout/Page.tsx b/web/components/layout/Page.tsx index 0f17f84..7a62217 100644 --- a/web/components/layout/Page.tsx +++ b/web/components/layout/Page.tsx @@ -31,12 +31,14 @@ import { Users, Menu as MenuIcon, X, - MessageSquare + MessageSquare, + LayoutList } from 'lucide-react' import { TasksLogo } from '@/components/TasksLogo' import { useRouter } from 'next/router' import { useTasksContext } from '@/hooks/useTasksContext' -import { useLocations } from '@/data' +import { useLocations, useMySavedViews } from '@/data' +import type { MySavedViewsQuery } from '@/api/gql/generated' import { hashString } from '@/utils/hash' import { useSwipeGesture } from '@/hooks/useSwipeGesture' import { LocationSelectionDialog } from '@/components/locations/LocationSelectionDialog' @@ -495,6 +497,9 @@ export const Sidebar = ({ isOpen, onClose, ...props }: SidebarProps) => { const translation = useTasksTranslation() const locationRoute = '/location' const context = useTasksContext() + const { data: savedViewsData } = useMySavedViews() + const savedViews = savedViewsData?.mySavedViews ?? [] + const [isSavedViewsOpen, setIsSavedViewsOpen] = useState(true) return ( <> @@ -549,6 +554,30 @@ export const Sidebar = ({ isOpen, onClose, ...props }: SidebarProps) => { {translation('patients')} {context?.totalPatientsCount !== undefined && ({context.totalPatientsCount})} + {savedViews.length > 0 && ( + + +
+ + {translation('savedViews')} +
+
+ + {savedViews.map((v: MySavedViewsQuery['mySavedViews'][number]) => ( + + {v.name} + + ))} + + {translation('viewSettings')} + + +
+ )} {(context?.teams?.length ?? 0) > 0 && ( void, } -export const PatientList = forwardRef(({ initialPatientId, onInitialPatientOpened, acceptedStates: _acceptedStates, rootLocationIds, locationId }, ref) => { +export const PatientList = forwardRef(({ initialPatientId, onInitialPatientOpened, acceptedStates: _acceptedStates, rootLocationIds, locationId, storageKeyPrefix, viewDefaultFilters, viewDefaultSorting, viewDefaultSearchQuery, readOnly: _readOnly, hideSaveView, onSavedViewCreated }, ref) => { const translation = useTasksTranslation() const { locale } = useLocale() const { selectedRootLocationIds } = useTasksContext() @@ -67,7 +78,7 @@ export const PatientList = forwardRef(({ initi const effectiveRootLocationIds = rootLocationIds ?? selectedRootLocationIds const [isPanelOpen, setIsPanelOpen] = useState(false) const [selectedPatient, setSelectedPatient] = useState(undefined) - const [searchQuery, setSearchQuery] = useState('') + const [searchQuery, setSearchQuery] = useState(viewDefaultSearchQuery ?? '') const [openedPatientId, setOpenedPatientId] = useState(null) const [isShowFilters, setIsShowFilters] = useState(false) const [isShowSorting, setIsShowSorting] = useState(false) @@ -81,18 +92,26 @@ export const PatientList = forwardRef(({ initi setFilters, columnVisibility, setColumnVisibility, - } = useStorageSyncedTableState('patient-list') - - // TODO get from the fast access id - const initialFilters: IdentifierFilterValue[] = [] - const initialSorting: SortingState = [] - - // TODO make the comparison more robust - const filtersChanged = useMemo(() => filters !== initialFilters, [filters, initialFilters]) - const sortingChanged = useMemo(() => sorting !== initialSorting, [sorting, initialSorting]) + } = useStorageSyncedTableState(storageKeyPrefix ?? 'patient-list', { + defaultFilters: viewDefaultFilters, + defaultSorting: viewDefaultSorting, + }) + + const baselineFilters = useMemo(() => viewDefaultFilters ?? [], [viewDefaultFilters]) + const baselineSorting = useMemo(() => viewDefaultSorting ?? [], [viewDefaultSorting]) + const baselineSearch = useMemo(() => viewDefaultSearchQuery ?? '', [viewDefaultSearchQuery]) + + const filtersChanged = useMemo( + () => serializeColumnFiltersForView(filters as ColumnFiltersState) !== serializeColumnFiltersForView(baselineFilters), + [filters, baselineFilters] + ) + const sortingChanged = useMemo( + () => serializeSortingForView(sorting) !== serializeSortingForView(baselineSorting), + [sorting, baselineSorting] + ) + const searchChanged = useMemo(() => searchQuery !== baselineSearch, [searchQuery, baselineSearch]) - const [isShowingFastAccessDialog, setIsShowingFastAccessDialog] = useState(false) - const [fastAccessName, setFastAccessName] = useState('') + const [isSaveViewDialogOpen, setIsSaveViewDialogOpen] = useState(false) usePropertyColumnVisibility( propertyDefinitionsData, @@ -508,9 +527,9 @@ export const PatientList = forwardRef(({ initi {translation('sorting') + ` (${sorting.length})`} - - {/* TODO Offer undo in case this is already a fast access and add a update button */} @@ -569,46 +588,19 @@ export const PatientList = forwardRef(({ initi onSuccess={refetch} /> - setIsShowingFastAccessDialog(false)} - titleElement={translation('addFastAccess')} - description={translation('addFastAccessDescription')} - > -
-
- - setFastAccessName(e.target.value)} - /> -
-
- - -
-
-
+ setIsSaveViewDialogOpen(false)} + baseEntityType={SavedViewEntityType.Patient} + filterDefinition={serializeColumnFiltersForView(filters as ColumnFiltersState)} + sortDefinition={serializeSortingForView(sorting)} + parameters={stringifyViewParameters({ + rootLocationIds: effectiveRootLocationIds ?? undefined, + locationId: locationId ?? undefined, + searchQuery: searchQuery || undefined, + } satisfies ViewParameters)} + onCreated={onSavedViewCreated} + /> ) diff --git a/web/components/views/PatientViewTasksPanel.tsx b/web/components/views/PatientViewTasksPanel.tsx new file mode 100644 index 0000000..dda2b00 --- /dev/null +++ b/web/components/views/PatientViewTasksPanel.tsx @@ -0,0 +1,109 @@ +'use client' + +import { useMemo } from 'react' +import { usePatients } from '@/data' +import { PatientState } from '@/api/gql/generated' +import type { FullTextSearchInput } from '@/api/gql/generated' +import { columnFiltersToFilterInput, sortingStateToSortInput } from '@/utils/tableStateToApi' +import { deserializeColumnFiltersFromView, deserializeSortingFromView } from '@/utils/viewDefinition' +import type { ViewParameters } from '@/utils/viewDefinition' +import { TaskList } from '@/components/tables/TaskList' +import type { TaskViewModel } from '@/components/tables/TaskList' + +const ADMITTED_OR_WAITING: PatientState[] = [PatientState.Admitted, PatientState.Wait] + +type PatientViewTasksPanelProps = { + filterDefinitionJson: string, + sortDefinitionJson: string, + parameters: ViewParameters, +} + +/** + * Task universe derived from the same patient query as the patient tab (patients matching the + * saved filters + scope), flattened to tasks — not a separate client hack. + */ +export function PatientViewTasksPanel({ + filterDefinitionJson, + sortDefinitionJson, + parameters, +}: PatientViewTasksPanelProps) { + const filters = deserializeColumnFiltersFromView(filterDefinitionJson) + const sorting = deserializeSortingFromView(sortDefinitionJson) + const apiFiltering = useMemo(() => columnFiltersToFilterInput(filters), [filters]) + const apiSorting = useMemo(() => sortingStateToSortInput(sorting), [sorting]) + + const allPatientStates: PatientState[] = useMemo(() => [ + PatientState.Admitted, + PatientState.Discharged, + PatientState.Dead, + PatientState.Wait, + ], []) + + const patientStates = useMemo(() => { + const stateFilter = apiFiltering.find( + f => f.column === 'state' && + (f.operator === 'TAGS_SINGLE_EQUALS' || f.operator === 'TAGS_SINGLE_CONTAINS') && + f.parameter?.searchTags != null && + f.parameter.searchTags.length > 0 + ) + if (!stateFilter?.parameter?.searchTags) return allPatientStates + const allowed = new Set(allPatientStates as unknown as string[]) + const filtered = (stateFilter.parameter.searchTags as string[]).filter(s => allowed.has(s)) + return filtered.length > 0 ? (filtered as PatientState[]) : allPatientStates + }, [apiFiltering, allPatientStates]) + + const searchInput: FullTextSearchInput | undefined = parameters.searchQuery + ? { searchText: parameters.searchQuery, includeProperties: true } + : undefined + + const { data: patientsData, loading, refetch } = usePatients({ + locationId: parameters.locationId, + rootLocationIds: parameters.rootLocationIds && parameters.rootLocationIds.length > 0 ? parameters.rootLocationIds : undefined, + states: patientStates, + filtering: apiFiltering.length > 0 ? apiFiltering : undefined, + sorting: apiSorting.length > 0 ? apiSorting : undefined, + search: searchInput, + }) + + const tasks: TaskViewModel[] = useMemo(() => { + if (!patientsData?.patients) return [] + return patientsData.patients.flatMap(patient => { + if (!ADMITTED_OR_WAITING.includes(patient.state) || !patient.tasks) return [] + const mergedLocations = [ + ...(patient.clinic ? [patient.clinic] : []), + ...(patient.position ? [patient.position] : []), + ...(patient.teams || []) + ] + return patient.tasks.map(task => ({ + id: task.id, + name: task.title, + description: task.description || undefined, + updateDate: task.updateDate ? new Date(task.updateDate) : new Date(task.creationDate), + dueDate: task.dueDate ? new Date(task.dueDate) : undefined, + priority: task.priority || null, + estimatedTime: task.estimatedTime ?? null, + done: task.done, + patient: { + id: patient.id, + name: patient.name, + locations: mergedLocations + }, + assignee: task.assignee + ? { id: task.assignee.id, name: task.assignee.name, avatarURL: task.assignee.avatarUrl, isOnline: task.assignee.isOnline ?? null } + : undefined, + assigneeTeam: task.assigneeTeam + ? { id: task.assigneeTeam.id, title: task.assigneeTeam.title } + : undefined, + })) + }) + }, [patientsData]) + + return ( + + ) +} diff --git a/web/components/views/SaveViewDialog.tsx b/web/components/views/SaveViewDialog.tsx new file mode 100644 index 0000000..a8d4f66 --- /dev/null +++ b/web/components/views/SaveViewDialog.tsx @@ -0,0 +1,102 @@ +'use client' + +import { useCallback, useState } from 'react' +import { useMutation } from '@apollo/client/react' +import { Button, Dialog, Input } from '@helpwave/hightide' +import type { + SavedViewEntityType } from '@/api/gql/generated' +import { + CreateSavedViewDocument, + type CreateSavedViewMutation, + type CreateSavedViewMutationVariables, + SavedViewVisibility +} from '@/api/gql/generated' +import { getParsedDocument } from '@/data/hooks/queryHelpers' +import { useTasksTranslation } from '@/i18n/useTasksTranslation' + +type SaveViewDialogProps = { + isOpen: boolean, + onClose: () => void, + /** Entity this view is saved from */ + baseEntityType: SavedViewEntityType, + filterDefinition: string, + sortDefinition: string, + parameters: string, + /** Optional: navigate or toast after save */ + onCreated?: (id: string) => void, +} + +export function SaveViewDialog({ + isOpen, + onClose, + baseEntityType, + filterDefinition, + sortDefinition, + parameters, + onCreated, +}: SaveViewDialogProps) { + const translation = useTasksTranslation() + const [name, setName] = useState('') + + const handleClose = useCallback(() => { + onClose() + setName('') + }, [onClose]) + + const [createSavedView, { loading }] = useMutation< + CreateSavedViewMutation, + CreateSavedViewMutationVariables + >(getParsedDocument(CreateSavedViewDocument), { + onCompleted(data) { + onCreated?.(data?.createSavedView?.id) + handleClose() + }, + }) + + return ( + +
+
+ + setName(e.target.value)} + /> +
+
+ + +
+
+
+ ) +} diff --git a/web/components/views/TaskViewPatientsPanel.tsx b/web/components/views/TaskViewPatientsPanel.tsx new file mode 100644 index 0000000..42d1af1 --- /dev/null +++ b/web/components/views/TaskViewPatientsPanel.tsx @@ -0,0 +1,91 @@ +'use client' + +import { useMemo } from 'react' +import Link from 'next/link' +import { useTasks } from '@/data' +import { columnFiltersToFilterInput, sortingStateToSortInput } from '@/utils/tableStateToApi' +import { deserializeColumnFiltersFromView, deserializeSortingFromView } from '@/utils/viewDefinition' +import type { ViewParameters } from '@/utils/viewDefinition' +import { LocationChips } from '@/components/locations/LocationChips' +import { LoadingContainer } from '@helpwave/hightide' +import { useTasksTranslation } from '@/i18n/useTasksTranslation' + +type TaskViewPatientsPanelProps = { + filterDefinitionJson: string, + sortDefinitionJson: string, + parameters: ViewParameters, +} + +type DistinctPatientRow = { + id: string, + name: string, + locations: Array<{ id: string, title: string }>, +} + +/** + * Distinct patients from the same task query as the task tab (no duplicate task-fetch hack). + */ +export function TaskViewPatientsPanel({ + filterDefinitionJson, + sortDefinitionJson, + parameters, +}: TaskViewPatientsPanelProps) { + const translation = useTasksTranslation() + const filters = deserializeColumnFiltersFromView(filterDefinitionJson) + const sorting = deserializeSortingFromView(sortDefinitionJson) + const apiFiltering = useMemo(() => columnFiltersToFilterInput(filters, 'task'), [filters]) + const apiSorting = useMemo(() => sortingStateToSortInput(sorting, 'task'), [sorting]) + + const { data, loading } = useTasks( + { + rootLocationIds: parameters.rootLocationIds, + assigneeId: parameters.assigneeId, + filtering: apiFiltering.length > 0 ? apiFiltering : undefined, + sorting: apiSorting.length > 0 ? apiSorting : undefined, + }, + { + skip: !parameters.rootLocationIds?.length && !parameters.assigneeId, + } + ) + + const rows = useMemo((): DistinctPatientRow[] => { + const map = new Map() + for (const t of data?.tasks ?? []) { + if (!t.patient) continue + if (!map.has(t.patient.id)) { + map.set(t.patient.id, { + id: t.patient.id, + name: t.patient.name, + locations: (t.patient.assignedLocations ?? []).map(l => ({ id: l.id, title: l.title })), + }) + } + } + return [...map.values()].sort((a, b) => a.name.localeCompare(b.name)) + }, [data]) + + if (loading) { + return + } + + return ( +
+

{translation('viewDerivedPatientsHint')}

+
    + {rows.map((p) => ( +
  • + + {p.name} + + +
  • + ))} +
+ {rows.length === 0 && ( + {translation('noPatientsInTaskView')} + )} +
+ ) +} diff --git a/web/data/hooks/index.ts b/web/data/hooks/index.ts index c0db2b5..5b53d5f 100644 --- a/web/data/hooks/index.ts +++ b/web/data/hooks/index.ts @@ -36,3 +36,4 @@ export { useCreatePropertyDefinition } from './useCreatePropertyDefinition' export { useUpdatePropertyDefinition } from './useUpdatePropertyDefinition' export { useDeletePropertyDefinition } from './useDeletePropertyDefinition' export { useUpdateProfilePicture } from './useUpdateProfilePicture' +export { useMySavedViews, useSavedView } from './useSavedViews' diff --git a/web/data/hooks/useSavedViews.ts b/web/data/hooks/useSavedViews.ts new file mode 100644 index 0000000..52514b7 --- /dev/null +++ b/web/data/hooks/useSavedViews.ts @@ -0,0 +1,25 @@ +import { + MySavedViewsDocument, + SavedViewDocument, + type MySavedViewsQuery, + type MySavedViewsQueryVariables, + type SavedViewQuery, + type SavedViewQueryVariables +} from '@/api/gql/generated' +import { useQueryWhenReady } from './queryHelpers' + +export function useMySavedViews(options?: { skip?: boolean }) { + return useQueryWhenReady( + MySavedViewsDocument, + {}, + options + ) +} + +export function useSavedView(id: string | undefined, options?: { skip?: boolean }) { + return useQueryWhenReady( + SavedViewDocument, + { id: id ?? '' }, + { skip: options?.skip ?? !id } + ) +} diff --git a/web/data/index.ts b/web/data/index.ts index 6ce1ae9..1102914 100644 --- a/web/data/index.ts +++ b/web/data/index.ts @@ -70,6 +70,8 @@ export { useUpdatePropertyDefinition, useDeletePropertyDefinition, useUpdateProfilePicture, + useMySavedViews, + useSavedView, } from './hooks' export type { ClientMutationId, diff --git a/web/i18n/translations.ts b/web/i18n/translations.ts index 4a2e85e..13eb60c 100644 --- a/web/i18n/translations.ts +++ b/web/i18n/translations.ts @@ -35,6 +35,7 @@ export type TasksTranslationEntries = { 'closedTasks': string, 'collapseAll': string, 'confirm': string, + 'confirmDeleteView': string, 'confirmSelection': string, 'confirmShiftHandover': string, 'confirmShiftHandoverDescription': string, @@ -43,6 +44,8 @@ export type TasksTranslationEntries = { 'connectionConnected': string, 'connectionConnecting': string, 'connectionDisconnected': string, + 'copyShareLink': string, + 'copyViewToMyViews': string, 'create': string, 'createTask': string, 'currentTime': string, @@ -67,6 +70,7 @@ export type TasksTranslationEntries = { 'diverse': string, 'done': string, 'dueDate': string, + 'duplicate': string, 'edit': string, 'editPatient': string, 'editTask': string, @@ -116,6 +120,7 @@ export type TasksTranslationEntries = { 'noNotifications': string, 'noOpenTasks': string, 'noPatient': string, + 'noPatientsInTaskView': string, 'noResultsFound': string, 'notAssigned': string, 'notifications': string, @@ -127,6 +132,7 @@ export type TasksTranslationEntries = { 'ok': string, 'openSurvey': string, 'openTasks': string, + 'openView': string, 'option': string, 'organizations': string, 'overview': string, @@ -156,6 +162,7 @@ export type TasksTranslationEntries = { 'property': string, 'pThemes': (values: { count: number }) => string, 'rAdd': (values: { name: string }) => string, + 'readOnlyView': string, 'recentPatients': string, 'recentTasks': string, 'rEdit': (values: { name: string }) => string, @@ -164,6 +171,9 @@ export type TasksTranslationEntries = { 'removePropertyConfirmation': string, 'retakeSurvey': string, 'rShow': (values: { name: string }) => string, + 'savedViews': string, + 'saveView': string, + 'saveViewDescription': string, 'search': string, 'searchLocations': string, 'searchUserOrTeam': string, @@ -212,6 +222,11 @@ export type TasksTranslationEntries = { 'user': string, 'userInformation': string, 'users': string, + 'viewDerivedPatientsHint': string, + 'viewsEntityPatient': string, + 'viewsEntityTask': string, + 'viewSettings': string, + 'viewSettingsDescription': string, 'waitingForPatient': string, 'waitPatient': string, 'wards': string, @@ -245,6 +260,7 @@ export const tasksTranslation: Translation { return `${name} hinzufügen` }, + 'readOnlyView': `Nur lesen`, 'recentPatients': `Deine kürzlichen Patienten`, 'recentTasks': `Deine kürzlichen Aufgaben`, 'rEdit': ({ name }): string => { @@ -455,6 +477,9 @@ export const tasksTranslation: Translation { return `${name} anzeigen` }, + 'savedViews': `Gespeicherte Ansichten`, + 'saveView': `Ansicht speichern`, + 'saveViewDescription': `Geben Sie dieser Ansicht einen Namen. Filter, Sortierung, Suche und Standort werden gespeichert.`, 'search': `Suchen`, 'searchLocations': `Standorte suchen...`, 'searchUserOrTeam': `Nach Benutzer (oder Team) suchen...`, @@ -530,6 +555,11 @@ export const tasksTranslation: Translation { return `Add ${name}` }, + 'readOnlyView': `Read-only`, 'recentPatients': `Your Recent Patients`, 'recentTasks': `Your Recent Tasks`, 'rEdit': ({ name }): string => { @@ -771,6 +808,9 @@ export const tasksTranslation: Translation { return `Show ${name}` }, + 'savedViews': `Saved views`, + 'saveView': `Save view`, + 'saveViewDescription': `Name this view. Filters, sorting, search, and location scope are stored.`, 'search': `Search`, 'searchLocations': `Search locations...`, 'searchUserOrTeam': `Search for user (or team)...`, @@ -846,6 +886,11 @@ export const tasksTranslation: Translation { return `Añadir ${name}` }, + 'readOnlyView': `Read-only`, 'recentPatients': `Tus pacientes recientes`, 'recentTasks': `Tus tareas recientes`, 'rEdit': ({ name }): string => { @@ -1086,6 +1138,9 @@ export const tasksTranslation: Translation { return `Mostrar ${name}` }, + 'savedViews': `Saved views`, + 'saveView': `Save view`, + 'saveViewDescription': `Name this view. Filters, sorting, search, and location scope are stored.`, 'search': `Buscar`, 'searchLocations': `Buscar ubicaciones...`, 'searchUserOrTeam': `Buscar usuario (o equipo)...`, @@ -1161,6 +1216,11 @@ export const tasksTranslation: Translation { return `Ajouter ${name}` }, + 'readOnlyView': `Read-only`, 'recentPatients': `Vos patients récents`, 'recentTasks': `Vos tâches récentes`, 'rEdit': ({ name }): string => { @@ -1401,6 +1468,9 @@ export const tasksTranslation: Translation { return `Afficher ${name}` }, + 'savedViews': `Saved views`, + 'saveView': `Save view`, + 'saveViewDescription': `Name this view. Filters, sorting, search, and location scope are stored.`, 'search': `Rechercher`, 'searchLocations': `Rechercher des emplacements...`, 'searchUserOrTeam': `Rechercher un utilisateur (ou une équipe)...`, @@ -1476,6 +1546,11 @@ export const tasksTranslation: Translation { return `${name} toevoegen` }, + 'readOnlyView': `Read-only`, 'recentPatients': `Uw recente patiënten`, 'recentTasks': `Uw recente taken`, 'rEdit': ({ name }): string => { @@ -1719,6 +1801,9 @@ export const tasksTranslation: Translation { return `${name} tonen` }, + 'savedViews': `Saved views`, + 'saveView': `Save view`, + 'saveViewDescription': `Name this view. Filters, sorting, search, and location scope are stored.`, 'search': `Zoeken`, 'searchLocations': `Locaties zoeken...`, 'searchUserOrTeam': `Zoek gebruiker (of team)...`, @@ -1794,6 +1879,11 @@ export const tasksTranslation: Translation { return `Adicionar ${name}` }, + 'readOnlyView': `Read-only`, 'recentPatients': `Seus pacientes recentes`, 'recentTasks': `Suas tarefas recentes`, 'rEdit': ({ name }): string => { @@ -2034,6 +2131,9 @@ export const tasksTranslation: Translation { return `Mostrar ${name}` }, + 'savedViews': `Saved views`, + 'saveView': `Save view`, + 'saveViewDescription': `Name this view. Filters, sorting, search, and location scope are stored.`, 'search': `Pesquisar`, 'searchLocations': `Pesquisar localizações...`, 'searchUserOrTeam': `Pesquisar usuário (ou equipe)...`, @@ -2109,6 +2209,11 @@ export const tasksTranslation: Translation { + diff --git a/web/pages/settings/views.tsx b/web/pages/settings/views.tsx new file mode 100644 index 0000000..95748fe --- /dev/null +++ b/web/pages/settings/views.tsx @@ -0,0 +1,271 @@ +'use client' + +import type { NextPage } from 'next' +import { useCallback, useMemo, useState } from 'react' +import { useMutation } from '@apollo/client/react' +import Link from 'next/link' +import { useRouter } from 'next/router' +import { Page } from '@/components/layout/Page' +import titleWrapper from '@/utils/titleWrapper' +import { useTasksTranslation } from '@/i18n/useTasksTranslation' +import { ContentPanel } from '@/components/layout/ContentPanel' +import { Button, Chip, ConfirmDialog, Dialog, FillerCell, IconButton, Input, LoadingContainer, Table } from '@helpwave/hightide' +import { useMySavedViews } from '@/data' +import { + DeleteSavedViewDocument, + DuplicateSavedViewDocument, + type DeleteSavedViewMutation, + type DeleteSavedViewMutationVariables, + type DuplicateSavedViewMutation, + type DuplicateSavedViewMutationVariables, + SavedViewEntityType, + UpdateSavedViewDocument, + type UpdateSavedViewMutation, + type UpdateSavedViewMutationVariables +} from '@/api/gql/generated' +import { getParsedDocument } from '@/data/hooks/queryHelpers' +import type { ColumnDef } from '@tanstack/table-core' +import { EditIcon, ExternalLink, Trash2, Share2, CopyPlus } from 'lucide-react' +import type { MySavedViewsQuery } from '@/api/gql/generated' + +type SavedViewRowGql = MySavedViewsQuery['mySavedViews'][number] + +type SavedViewRow = { + id: string, + name: string, + baseEntityType: SavedViewEntityType, + updatedAt: string, + visibility: string, +} + +const ViewsSettingsPage: NextPage = () => { + const translation = useTasksTranslation() + const router = useRouter() + const { data, loading, refetch } = useMySavedViews() + const rows: SavedViewRow[] = useMemo(() => { + return (data?.mySavedViews ?? []).map((v: SavedViewRowGql) => ({ + id: v.id, + name: v.name, + baseEntityType: v.baseEntityType, + updatedAt: v.updatedAt, + visibility: v.visibility, + })) + }, [data]) + + const fillerRowCell = useCallback(() => (), []) + + const [renameOpen, setRenameOpen] = useState(false) + const [renameId, setRenameId] = useState(null) + const [renameValue, setRenameValue] = useState('') + + const [deleteOpen, setDeleteOpen] = useState(false) + const [deleteId, setDeleteId] = useState(null) + + const [duplicateOpen, setDuplicateOpen] = useState(false) + const [duplicateId, setDuplicateId] = useState(null) + const [duplicateName, setDuplicateName] = useState('') + + const [updateSavedView] = useMutation( + getParsedDocument(UpdateSavedViewDocument) + ) + const [deleteSavedView] = useMutation( + getParsedDocument(DeleteSavedViewDocument) + ) + const [duplicateSavedView] = useMutation( + getParsedDocument(DuplicateSavedViewDocument) + ) + + const copyLink = useCallback((id: string) => { + if (typeof window === 'undefined') return + void navigator.clipboard.writeText(`${window.location.origin}/view/${id}`) + }, []) + + const handleRename = useCallback(async () => { + if (!renameId || renameValue.trim().length < 1) return + await updateSavedView({ variables: { id: renameId, data: { name: renameValue.trim() } } }) + setRenameOpen(false) + setRenameId(null) + void refetch() + }, [renameId, renameValue, updateSavedView, refetch]) + + const handleDelete = useCallback(async () => { + if (!deleteId) return + await deleteSavedView({ variables: { id: deleteId } }) + setDeleteOpen(false) + setDeleteId(null) + void refetch() + }, [deleteId, deleteSavedView, refetch]) + + const handleDuplicate = useCallback(async () => { + if (!duplicateId || duplicateName.trim().length < 2) return + const { data: d } = await duplicateSavedView({ + variables: { id: duplicateId, name: duplicateName.trim() }, + }) + setDuplicateOpen(false) + setDuplicateId(null) + setDuplicateName('') + void refetch() + const newId = d?.duplicateSavedView?.id + if (newId) router.push(`/view/${newId}`) + }, [duplicateId, duplicateName, duplicateSavedView, refetch, router]) + + const columns = useMemo[]>(() => [ + { + id: 'name', + header: translation('name'), + accessorKey: 'name', + minSize: 160, + }, + { + id: 'entity', + header: translation('subjectType'), + cell: ({ row }) => ( + + {row.original.baseEntityType === SavedViewEntityType.Patient + ? translation('viewsEntityPatient') + : translation('viewsEntityTask')} + + ), + minSize: 100, + }, + { + id: 'updated', + header: translation('updated'), + accessorKey: 'updatedAt', + minSize: 140, + }, + { + id: 'actions', + header: '', + cell: ({ row }) => ( +
+ router.push(`/view/${row.original.id}`)} + > + + + copyLink(row.original.id)} + > + + + { + setRenameId(row.original.id) + setRenameValue(row.original.name) + setRenameOpen(true) + }} + > + + + { + setDuplicateId(row.original.id) + setDuplicateName(`${row.original.name} (2)`) + setDuplicateOpen(true) + }} + > + + + { + setDeleteId(row.original.id) + setDeleteOpen(true) + }} + > + + +
+ ), + size: 220, + minSize: 220, + }, + ], [copyLink, router, translation]) + + return ( + + +
+ + ← {translation('settings')} + +
+ {loading ? ( + + ) : ( +
+ + + )} + + setRenameOpen(false)} + titleElement={translation('rEdit', { name: translation('name') })} + description={undefined} + > +
+ setRenameValue(e.target.value)} /> +
+ + +
+
+
+ + setDuplicateOpen(false)} + titleElement={translation('copyViewToMyViews')} + description={undefined} + > +
+ setDuplicateName(e.target.value)} /> +
+ + +
+
+
+ + setDeleteOpen(false)} + onConfirm={() => void handleDelete()} + titleElement={translation('delete')} + description={translation('confirmDeleteView')} + confirmType="negative" + /> + + + ) +} + +export default ViewsSettingsPage diff --git a/web/pages/tasks/index.tsx b/web/pages/tasks/index.tsx index d7a1ab2..b3bbef1 100644 --- a/web/pages/tasks/index.tsx +++ b/web/pages/tasks/index.tsx @@ -5,12 +5,17 @@ import { useTasksTranslation } from '@/i18n/useTasksTranslation' import { ContentPanel } from '@/components/layout/ContentPanel' import type { TaskViewModel } from '@/components/tables/TaskList' import { TaskList } from '@/components/tables/TaskList' -import { useMemo } from 'react' +import { useMemo, useState } from 'react' import { useRouter } from 'next/router' import { useTasksContext } from '@/hooks/useTasksContext' import { useTasksPaginated } from '@/data' import { useStorageSyncedTableState } from '@/hooks/useTableState' import { columnFiltersToFilterInput, paginationStateToPaginationInput, sortingStateToSortInput } from '@/utils/tableStateToApi' +import { Button, Visibility } from '@helpwave/hightide' +import { SaveViewDialog } from '@/components/views/SaveViewDialog' +import { SavedViewEntityType } from '@/api/gql/generated' +import { serializeColumnFiltersForView, stringifyViewParameters } from '@/utils/viewDefinition' +import type { ColumnFiltersState } from '@tanstack/react-table' const TasksPage: NextPage = () => { const translation = useTasksTranslation() @@ -32,6 +37,22 @@ const TasksPage: NextPage = () => { ], []), }) + const baselineSort = useMemo(() => [ + { id: 'done', desc: false }, + { id: 'dueDate', desc: false }, + ], []) + const baselineFilters = useMemo((): ColumnFiltersState => [], []) + const filtersChanged = useMemo( + () => serializeColumnFiltersForView(filters as ColumnFiltersState) !== serializeColumnFiltersForView(baselineFilters), + [filters, baselineFilters] + ) + const sortingChanged = useMemo( + () => JSON.stringify(sorting) !== JSON.stringify(baselineSort), + [sorting, baselineSort] + ) + + const [isSaveViewOpen, setIsSaveViewOpen] = useState(false) + const apiFiltering = useMemo(() => columnFiltersToFilterInput(filters, 'task'), [filters]) const apiSorting = useMemo(() => sortingStateToSortInput(sorting, 'task'), [sorting]) const apiPagination = useMemo(() => paginationStateToPaginationInput(pagination), [pagination]) @@ -80,6 +101,25 @@ const TasksPage: NextPage = () => { titleElement={translation('myTasks')} description={myTasksCount !== undefined ? translation('nTask', { count: myTasksCount }) : undefined} > + +
+ +
+
+ setIsSaveViewOpen(false)} + baseEntityType={SavedViewEntityType.Task} + filterDefinition={serializeColumnFiltersForView(filters as ColumnFiltersState)} + sortDefinition={JSON.stringify(sorting)} + parameters={stringifyViewParameters({ + rootLocationIds: selectedRootLocationIds ?? undefined, + assigneeId: user?.id, + })} + onCreated={(id) => router.push(`/view/${id}`)} + /> , +} + +function SavedTaskViewTab({ + viewId, + filterDefinition, + sortDefinition, + parameters, +}: SavedTaskViewTabProps) { + const { selectedRootLocationIds, user } = useTasksContext() + const defaultFilters = deserializeColumnFiltersFromView(filterDefinition) + const defaultSorting = deserializeSortingFromView(sortDefinition) + + const baselineSort = useMemo(() => [ + { id: 'done', desc: false }, + { id: 'dueDate', desc: false }, + ], []) + + const { + pagination, + setPagination, + sorting, + setSorting, + filters, + setFilters, + columnVisibility, + setColumnVisibility, + } = useStorageSyncedTableState(`saved-view-${viewId}-task`, { + defaultFilters, + defaultSorting: defaultSorting.length > 0 ? defaultSorting : baselineSort, + }) + + const apiFiltering = useMemo(() => columnFiltersToFilterInput(filters, 'task'), [filters]) + const apiSorting = useMemo(() => sortingStateToSortInput(sorting, 'task'), [sorting]) + const apiPagination = useMemo(() => paginationStateToPaginationInput(pagination), [pagination]) + + const rootIds = parameters.rootLocationIds?.length ? parameters.rootLocationIds : selectedRootLocationIds + const assigneeId = parameters.assigneeId ?? user?.id + + const { data: tasksData, refetch, totalCount, loading: tasksLoading } = useTasksPaginated( + rootIds && assigneeId + ? { rootLocationIds: rootIds, assigneeId } + : undefined, + { + pagination: apiPagination, + sorting: apiSorting.length > 0 ? apiSorting : undefined, + filtering: apiFiltering.length > 0 ? apiFiltering : undefined, + } + ) + + const tasks: TaskViewModel[] = useMemo(() => { + if (!tasksData || tasksData.length === 0) return [] + return tasksData.map((task) => ({ + id: task.id, + name: task.title, + description: task.description || undefined, + updateDate: task.updateDate ? new Date(task.updateDate) : new Date(task.creationDate), + dueDate: task.dueDate ? new Date(task.dueDate) : undefined, + priority: task.priority || null, + estimatedTime: task.estimatedTime ?? null, + done: task.done, + patient: task.patient + ? { + id: task.patient.id, + name: task.patient.name, + locations: task.patient.assignedLocations || [] + } + : undefined, + assignee: task.assignee + ? { id: task.assignee.id, name: task.assignee.name, avatarURL: task.assignee.avatarUrl, isOnline: task.assignee.isOnline ?? null } + : undefined, + properties: task.properties ?? [], + })) + }, [tasksData]) + + return ( + void refetch()} + showAssignee={false} + totalCount={totalCount} + loading={tasksLoading} + tableState={{ + pagination, + setPagination, + sorting, + setSorting, + filters, + setFilters, + columnVisibility, + setColumnVisibility, + }} + /> + ) +} + +const ViewPage: NextPage = () => { + const translation = useTasksTranslation() + const router = useRouter() + const uid = typeof router.query['uid'] === 'string' ? router.query['uid'] : undefined + const { data, loading, error } = useSavedView(uid) + const view = data?.savedView + const params = useMemo(() => (view ? parseViewParameters(view.parameters) : {}), [view]) + + const [duplicateOpen, setDuplicateOpen] = useState(false) + const [duplicateName, setDuplicateName] = useState('') + + const [duplicateSavedView] = useMutation< + DuplicateSavedViewMutation, + DuplicateSavedViewMutationVariables + >(getParsedDocument(DuplicateSavedViewDocument)) + + const handleDuplicate = useCallback(async () => { + if (!view?.id || duplicateName.trim().length < 2) return + const { data: d } = await duplicateSavedView({ + variables: { id: view.id, name: duplicateName.trim() }, + }) + setDuplicateOpen(false) + setDuplicateName('') + const newId = d?.duplicateSavedView?.id + if (newId) router.push(`/view/${newId}`) + }, [duplicateSavedView, duplicateName, router, view?.id]) + + const copyShareLink = useCallback(() => { + if (typeof window !== 'undefined' && uid) { + void navigator.clipboard.writeText(`${window.location.origin}/view/${uid}`) + } + }, [uid]) + + if (!router.isReady || !uid) { + return ( + + + + ) + } + + if (loading) { + return ( + + }> + + + + ) + } + + if (error || !view) { + return ( + + +
{translation('errorOccurred')}
+
+
+ ) + } + + const defaultFilters = deserializeColumnFiltersFromView(view.filterDefinition) + const defaultSorting = deserializeSortingFromView(view.sortDefinition) + + return ( + + +
+ {view.name} + + {view.baseEntityType === SavedViewEntityType.Patient + ? translation('viewsEntityPatient') + : translation('viewsEntityTask')} + + {!view.isOwner && ( + {translation('readOnlyView')} + )} +
+
+ + {!view.isOwner && ( + + )} +
+ + )} + > + {duplicateOpen && ( +
+
+ {translation('copyViewToMyViews')} + +
+ + +
+
+
+ )} + + {view.baseEntityType === SavedViewEntityType.Patient && ( + + + + router.push(`/view/${id}`)} + /> + + + + + + )} + + {view.baseEntityType === SavedViewEntityType.Task && ( + + + + + + + + + + )} +
+
+ ) +} + +export default ViewPage diff --git a/web/utils/viewDefinition.ts b/web/utils/viewDefinition.ts new file mode 100644 index 0000000..4675e90 --- /dev/null +++ b/web/utils/viewDefinition.ts @@ -0,0 +1,93 @@ +import type { ColumnFilter, ColumnFiltersState, SortingState } from '@tanstack/react-table' +import type { DataType, FilterOperator, FilterParameter, FilterValue } from '@helpwave/hightide' + +/** + * Stored JSON alongside filterDefinition / sortDefinition in SavedView.parameters. + * Location scope and other cross-cutting scope live here (not in separate routes). + */ +export type ViewParameters = { + rootLocationIds?: string[], + locationId?: string, + /** Patient list search (full-text) */ + searchQuery?: string, + /** Task list: assignee scope (e.g. my tasks page) */ + assigneeId?: string, +} + +export function parseViewParameters(json: string): ViewParameters { + try { + const v = JSON.parse(json) as unknown + if (!v || typeof v !== 'object') return {} + return v as ViewParameters + } catch { + return {} + } +} + +export function stringifyViewParameters(p: ViewParameters): string { + return JSON.stringify(p) +} + +/** Same wire format as useStorageSyncedTableState filter serialization. */ +export function serializeColumnFiltersForView(filters: ColumnFiltersState): string { + const mappedColumnFilter = filters.map((filter) => { + const tableFilterValue = filter.value as FilterValue + const filterParameter = tableFilterValue.parameter + const parameter: Record = { + ...filterParameter, + compareDate: filterParameter.compareDate ? filterParameter.compareDate.toISOString() : undefined, + minDate: filterParameter.minDate ? filterParameter.minDate.toISOString() : undefined, + maxDate: filterParameter.maxDate ? filterParameter.maxDate.toISOString() : undefined, + } + return { + ...filter, + id: filter.id, + value: { + ...tableFilterValue, + parameter, + }, + } + }) + return JSON.stringify(mappedColumnFilter) +} + +export function deserializeColumnFiltersFromView(json: string): ColumnFiltersState { + try { + const mappedColumnFilter = JSON.parse(json) as Record[] + return mappedColumnFilter.map((filter): ColumnFilter => { + const value = filter['value'] as Record + const parameter = value['parameter'] as Record + const filterParameter: FilterParameter = { + ...parameter, + compareDate: parameter['compareDate'] ? new Date(parameter['compareDate'] as string) : undefined, + minDate: parameter['minDate'] ? new Date(parameter['minDate'] as string) : undefined, + maxDate: parameter['maxDate'] ? new Date(parameter['maxDate'] as string) : undefined, + } as FilterParameter + const mappedValue: FilterValue = { + operator: value['operator'] as FilterOperator, + dataType: value['dataType'] as DataType, + parameter: filterParameter, + } + return { + ...filter, + value: mappedValue, + } as ColumnFilter + }) + } catch { + return [] + } +} + +export function serializeSortingForView(sorting: SortingState): string { + return JSON.stringify(sorting) +} + +export function deserializeSortingFromView(json: string): SortingState { + try { + const v = JSON.parse(json) as unknown + if (!Array.isArray(v)) return [] + return v as SortingState + } catch { + return [] + } +} From 585115deff1fbaa47ae21d3491033ad8eb443b0a Mon Sep 17 00:00:00 2001 From: Felix Evers Date: Sun, 22 Mar 2026 20:40:32 +0100 Subject: [PATCH 09/20] rewrite filter and sorting backend logic --- backend/api/decorators/__init__.py | 14 - backend/api/decorators/filter_sort.py | 735 --------------------- backend/api/decorators/full_text_search.py | 116 ---- backend/api/inputs.py | 102 --- backend/api/query/__init__.py | 0 backend/api/query/adapters/patient.py | 391 +++++++++++ backend/api/query/adapters/task.py | 526 +++++++++++++++ backend/api/query/adapters/user.py | 144 ++++ backend/api/query/context.py | 14 + backend/api/query/engine.py | 80 +++ backend/api/query/enums.py | 55 ++ backend/api/query/execute.py | 88 +++ backend/api/query/field_ops.py | 218 ++++++ backend/api/query/graphql_types.py | 36 + backend/api/query/inputs.py | 42 ++ backend/api/query/metadata_service.py | 265 ++++++++ backend/api/query/property_sql.py | 61 ++ backend/api/query/registry.py | 38 ++ backend/api/query/sql_expr.py | 33 + backend/api/resolvers/__init__.py | 2 + backend/api/resolvers/patient.py | 104 +-- backend/api/resolvers/query_metadata.py | 14 + backend/api/resolvers/task.py | 135 ++-- backend/api/resolvers/user.py | 24 +- web/api/gql/generated.ts | 301 +++++---- web/api/graphql/GetOverviewData.graphql | 10 +- web/api/graphql/GetPatients.graphql | 6 +- web/api/graphql/GetTasks.graphql | 7 +- web/api/graphql/QueryableFields.graphql | 22 + web/codegen.ts | 3 +- web/components/tables/PatientList.tsx | 161 +++-- web/components/tables/TaskList.tsx | 76 ++- web/data/cache/policies.ts | 4 +- web/data/hooks/index.ts | 1 + web/data/hooks/usePaginatedEntityQuery.ts | 21 +- web/data/hooks/usePatientsPaginated.ts | 12 +- web/data/hooks/useQueryableFields.ts | 14 + web/data/hooks/useTasksPaginated.ts | 12 +- web/data/index.ts | 1 + web/package-lock.json | 16 + web/pages/tasks/index.tsx | 19 +- web/schema.graphql | 434 ++++++++++++ web/utils/propertyColumn.tsx | 4 +- web/utils/queryableFilterList.tsx | 48 ++ web/utils/tableStateToApi.ts | 243 ++++--- 45 files changed, 3128 insertions(+), 1524 deletions(-) delete mode 100644 backend/api/decorators/filter_sort.py delete mode 100644 backend/api/decorators/full_text_search.py create mode 100644 backend/api/query/__init__.py create mode 100644 backend/api/query/adapters/patient.py create mode 100644 backend/api/query/adapters/task.py create mode 100644 backend/api/query/adapters/user.py create mode 100644 backend/api/query/context.py create mode 100644 backend/api/query/engine.py create mode 100644 backend/api/query/enums.py create mode 100644 backend/api/query/execute.py create mode 100644 backend/api/query/field_ops.py create mode 100644 backend/api/query/graphql_types.py create mode 100644 backend/api/query/inputs.py create mode 100644 backend/api/query/metadata_service.py create mode 100644 backend/api/query/property_sql.py create mode 100644 backend/api/query/registry.py create mode 100644 backend/api/query/sql_expr.py create mode 100644 backend/api/resolvers/query_metadata.py create mode 100644 web/api/graphql/QueryableFields.graphql create mode 100644 web/data/hooks/useQueryableFields.ts create mode 100644 web/schema.graphql create mode 100644 web/utils/queryableFilterList.tsx diff --git a/backend/api/decorators/__init__.py b/backend/api/decorators/__init__.py index ef53512..3090f26 100644 --- a/backend/api/decorators/__init__.py +++ b/backend/api/decorators/__init__.py @@ -1,20 +1,6 @@ -from api.decorators.filter_sort import ( - apply_filtering, - apply_sorting, - filtered_and_sorted_query, -) -from api.decorators.full_text_search import ( - apply_full_text_search, - full_text_search_query, -) from api.decorators.pagination import apply_pagination, paginated_query __all__ = [ "apply_pagination", "paginated_query", - "apply_sorting", - "apply_filtering", - "filtered_and_sorted_query", - "apply_full_text_search", - "full_text_search_query", ] diff --git a/backend/api/decorators/filter_sort.py b/backend/api/decorators/filter_sort.py deleted file mode 100644 index d7e3d35..0000000 --- a/backend/api/decorators/filter_sort.py +++ /dev/null @@ -1,735 +0,0 @@ -from datetime import date as date_type -from functools import wraps -from typing import Any, Callable, TypeVar - -import strawberry -from api.decorators.pagination import apply_pagination -from api.inputs import ( - ColumnType, - FilterInput, - FilterOperator, - PaginationInput, - SortDirection, - SortInput, -) -from database import models -from database.models.base import Base -from sqlalchemy import Select, and_, func, or_, select -from sqlalchemy.ext.asyncio import AsyncSession -from sqlalchemy.orm import aliased - -T = TypeVar("T") - - -async def get_property_field_types( - db: AsyncSession, - filtering: list[FilterInput] | None, - sorting: list[SortInput] | None, -) -> dict[str, str]: - property_def_ids: set[str] = set() - if filtering: - for f in filtering: - if ( - f.column_type == ColumnType.PROPERTY - and f.property_definition_id - ): - property_def_ids.add(f.property_definition_id) - if sorting: - for s in sorting: - if ( - s.column_type == ColumnType.PROPERTY - and s.property_definition_id - ): - property_def_ids.add(s.property_definition_id) - if not property_def_ids: - return {} - result = await db.execute( - select(models.PropertyDefinition).where( - models.PropertyDefinition.id.in_(property_def_ids) - ) - ) - prop_defs = result.scalars().all() - return {str(p.id): p.field_type for p in prop_defs} - - -def detect_entity_type(model_class: type[Base]) -> str | None: - if model_class == models.Patient: - return "patient" - if model_class == models.Task: - return "task" - return None - - -def get_property_value_column(field_type: str) -> str: - field_type_mapping = { - "FIELD_TYPE_TEXT": "text_value", - "FIELD_TYPE_NUMBER": "number_value", - "FIELD_TYPE_CHECKBOX": "boolean_value", - "FIELD_TYPE_DATE": "date_value", - "FIELD_TYPE_DATE_TIME": "date_time_value", - "FIELD_TYPE_SELECT": "select_value", - "FIELD_TYPE_MULTI_SELECT": "multi_select_values", - "FIELD_TYPE_USER": "user_value", - } - return field_type_mapping.get(field_type, "text_value") - - -def get_property_join_alias( - query: Select[Any], - model_class: type[Base], - property_definition_id: str, - field_type: str, -) -> Any: - entity_type = detect_entity_type(model_class) - if not entity_type: - raise ValueError( - f"Unsupported entity type for property filtering: {model_class}" - ) - - property_alias = aliased(models.PropertyValue) - value_column = get_property_value_column(field_type) - - if entity_type == "patient": - join_condition = and_( - property_alias.patient_id == model_class.id, - property_alias.definition_id == property_definition_id, - ) - else: - join_condition = and_( - property_alias.task_id == model_class.id, - property_alias.definition_id == property_definition_id, - ) - - query = query.outerjoin(property_alias, join_condition) - return query, property_alias, getattr(property_alias, value_column) - - -def apply_sorting( - query: Select[Any], - sorting: list[SortInput] | None, - model_class: type[Base], - property_field_types: dict[str, str] | None = None, -) -> Select[Any]: - if not sorting: - return query - - order_by_clauses = [] - property_field_types = property_field_types or {} - - for sort_input in sorting: - if sort_input.column_type == ColumnType.DIRECT_ATTRIBUTE: - try: - column = getattr(model_class, sort_input.column) - if sort_input.direction == SortDirection.DESC: - order_by_clauses.append(column.desc()) - else: - order_by_clauses.append(column.asc()) - except AttributeError: - continue - - elif sort_input.column_type == ColumnType.PROPERTY: - if not sort_input.property_definition_id: - continue - - field_type = property_field_types.get( - sort_input.property_definition_id, - "FIELD_TYPE_TEXT" - ) - query, property_alias, value_column = ( - get_property_join_alias( - query, - model_class, - sort_input.property_definition_id, - field_type, - ) - ) - - if sort_input.direction == SortDirection.DESC: - order_by_clauses.append(value_column.desc().nulls_last()) - else: - order_by_clauses.append(value_column.asc().nulls_first()) - - if order_by_clauses: - query = query.order_by(*order_by_clauses) - - return query - - -def apply_text_filter( - column: Any, operator: FilterOperator, parameter: Any -) -> Any: - search_text = parameter.search_text - if search_text is None: - return None - - is_case_sensitive = parameter.is_case_sensitive - - if is_case_sensitive: - if operator == FilterOperator.TEXT_EQUALS: - return column.like(search_text) - if operator == FilterOperator.TEXT_NOT_EQUALS: - return ~column.like(search_text) - if operator == FilterOperator.TEXT_NOT_WHITESPACE: - return func.trim(column) != "" - if operator == FilterOperator.TEXT_CONTAINS: - return column.like(f"%{search_text}%") - if operator == FilterOperator.TEXT_NOT_CONTAINS: - return ~column.like(f"%{search_text}%") - if operator == FilterOperator.TEXT_STARTS_WITH: - return column.like(f"{search_text}%") - if operator == FilterOperator.TEXT_ENDS_WITH: - return column.like(f"%{search_text}") - else: - if operator == FilterOperator.TEXT_EQUALS: - return column.ilike(search_text) - if operator == FilterOperator.TEXT_NOT_EQUALS: - return ~column.ilike(search_text) - if operator == FilterOperator.TEXT_NOT_WHITESPACE: - return func.trim(column) != "" - if operator == FilterOperator.TEXT_CONTAINS: - return column.ilike(f"%{search_text}%") - if operator == FilterOperator.TEXT_NOT_CONTAINS: - return ~column.ilike(f"%{search_text}%") - if operator == FilterOperator.TEXT_STARTS_WITH: - return column.ilike(f"{search_text}%") - if operator == FilterOperator.TEXT_ENDS_WITH: - return column.ilike(f"%{search_text}") - - return None - - -def apply_number_filter( - column: Any, operator: FilterOperator, parameter: Any -) -> Any: - compare_value = parameter.compare_value - min_value = parameter.min - max_value = parameter.max - - if operator == FilterOperator.NUMBER_EQUALS: - if compare_value is not None: - return column == compare_value - elif operator == FilterOperator.NUMBER_NOT_EQUALS: - if compare_value is not None: - return column != compare_value - elif operator == FilterOperator.NUMBER_GREATER_THAN: - if compare_value is not None: - return column > compare_value - elif operator == FilterOperator.NUMBER_GREATER_THAN_OR_EQUAL: - if compare_value is not None: - return column >= compare_value - elif operator == FilterOperator.NUMBER_LESS_THAN: - if compare_value is not None: - return column < compare_value - elif operator == FilterOperator.NUMBER_LESS_THAN_OR_EQUAL: - if compare_value is not None: - return column <= compare_value - elif operator == FilterOperator.NUMBER_BETWEEN: - if min_value is not None and max_value is not None: - return column.between(min_value, max_value) - elif operator == FilterOperator.NUMBER_NOT_BETWEEN: - if min_value is not None and max_value is not None: - return ~column.between(min_value, max_value) - - return None - - -def normalize_date_for_comparison(date_value: Any) -> Any: - return date_value - - -def apply_date_filter( - column: Any, operator: FilterOperator, parameter: Any -) -> Any: - compare_date = parameter.compare_date - min_date = parameter.min_date - max_date = parameter.max_date - - if operator == FilterOperator.DATE_EQUALS: - if compare_date is not None: - if isinstance(compare_date, date_type): - return func.date(column) == compare_date - return column == compare_date - elif operator == FilterOperator.DATE_NOT_EQUALS: - if compare_date is not None: - if isinstance(compare_date, date_type): - return func.date(column) != compare_date - return column != compare_date - elif operator == FilterOperator.DATE_GREATER_THAN: - if compare_date is not None: - if isinstance(compare_date, date_type): - return func.date(column) > compare_date - return column > compare_date - elif operator == FilterOperator.DATE_GREATER_THAN_OR_EQUAL: - if compare_date is not None: - if isinstance(compare_date, date_type): - return func.date(column) >= compare_date - return column >= compare_date - elif operator == FilterOperator.DATE_LESS_THAN: - if compare_date is not None: - if isinstance(compare_date, date_type): - return func.date(column) < compare_date - return column < compare_date - elif operator == FilterOperator.DATE_LESS_THAN_OR_EQUAL: - if compare_date is not None: - if isinstance(compare_date, date_type): - return func.date(column) <= compare_date - return column <= compare_date - elif operator == FilterOperator.DATE_BETWEEN: - if min_date is not None and max_date is not None: - if isinstance(min_date, date_type) and isinstance(max_date, date_type): - return func.date(column).between(min_date, max_date) - return column.between(min_date, max_date) - elif operator == FilterOperator.DATE_NOT_BETWEEN: - if min_date is not None and max_date is not None: - if isinstance(min_date, date_type) and isinstance(max_date, date_type): - return ~func.date(column).between(min_date, max_date) - return ~column.between(min_date, max_date) - - return None - - -def apply_datetime_filter( - column: Any, operator: FilterOperator, parameter: Any -) -> Any: - compare_date_time = parameter.compare_date_time - min_date_time = parameter.min_date_time - max_date_time = parameter.max_date_time - - if operator == FilterOperator.DATETIME_EQUALS: - if compare_date_time is not None: - return column == compare_date_time - elif operator == FilterOperator.DATETIME_NOT_EQUALS: - if compare_date_time is not None: - return column != compare_date_time - elif operator == FilterOperator.DATETIME_GREATER_THAN: - if compare_date_time is not None: - return column > compare_date_time - elif operator == FilterOperator.DATETIME_GREATER_THAN_OR_EQUAL: - if compare_date_time is not None: - return column >= compare_date_time - elif operator == FilterOperator.DATETIME_LESS_THAN: - if compare_date_time is not None: - return column < compare_date_time - elif operator == FilterOperator.DATETIME_LESS_THAN_OR_EQUAL: - if compare_date_time is not None: - return column <= compare_date_time - elif operator == FilterOperator.DATETIME_BETWEEN: - if min_date_time is not None and max_date_time is not None: - return column.between(min_date_time, max_date_time) - elif operator == FilterOperator.DATETIME_NOT_BETWEEN: - if min_date_time is not None and max_date_time is not None: - return ~column.between(min_date_time, max_date_time) - - return None - - -def apply_boolean_filter( - column: Any, operator: FilterOperator, parameter: Any -) -> Any: - if operator == FilterOperator.BOOLEAN_IS_TRUE: - return column.is_(True) - if operator == FilterOperator.BOOLEAN_IS_FALSE: - return column.is_(False) - return None - - -def apply_tags_filter(column: Any, operator: FilterOperator, parameter: Any) -> Any: - search_tags = parameter.search_tags - if not search_tags: - return None - - if operator == FilterOperator.TAGS_EQUALS: - tags_str = ",".join(sorted(search_tags)) - return column == tags_str - if operator == FilterOperator.TAGS_NOT_EQUALS: - tags_str = ",".join(sorted(search_tags)) - return column != tags_str - if operator == FilterOperator.TAGS_CONTAINS: - conditions = [] - for tag in search_tags: - conditions.append(column.contains(tag)) - return or_(*conditions) - if operator == FilterOperator.TAGS_NOT_CONTAINS: - conditions = [] - for tag in search_tags: - conditions.append(~column.contains(tag)) - return and_(*conditions) - - return None - - -def apply_tags_single_filter( - column: Any, operator: FilterOperator, parameter: Any -) -> Any: - search_tags = parameter.search_tags - if not search_tags: - return None - - if operator == FilterOperator.TAGS_SINGLE_EQUALS: - if len(search_tags) == 1: - return column == search_tags[0] - if operator == FilterOperator.TAGS_SINGLE_NOT_EQUALS: - if len(search_tags) == 1: - return column != search_tags[0] - if operator == FilterOperator.TAGS_SINGLE_CONTAINS: - conditions = [] - for tag in search_tags: - conditions.append(column == tag) - return or_(*conditions) - if operator == FilterOperator.TAGS_SINGLE_NOT_CONTAINS: - conditions = [] - for tag in search_tags: - conditions.append(column != tag) - return and_(*conditions) - - return None - - -def apply_null_filter( - column: Any, operator: FilterOperator, parameter: Any -) -> Any: - if operator == FilterOperator.IS_NULL: - return column.is_(None) - if operator == FilterOperator.IS_NOT_NULL: - return column.isnot(None) - return None - - -def apply_filtering( - query: Select[Any], - filtering: list[FilterInput] | None, - model_class: type[Base], - property_field_types: dict[str, str] | None = None, -) -> Select[Any]: - if not filtering: - return query - - filter_conditions = [] - property_field_types = property_field_types or {} - - for filter_input in filtering: - condition = None - - if filter_input.column_type == ColumnType.DIRECT_ATTRIBUTE: - try: - column = getattr(model_class, filter_input.column) - except AttributeError: - continue - - operator = filter_input.operator - parameter = filter_input.parameter - - if operator in [ - FilterOperator.TEXT_EQUALS, - FilterOperator.TEXT_NOT_EQUALS, - FilterOperator.TEXT_NOT_WHITESPACE, - FilterOperator.TEXT_CONTAINS, - FilterOperator.TEXT_NOT_CONTAINS, - FilterOperator.TEXT_STARTS_WITH, - FilterOperator.TEXT_ENDS_WITH, - ]: - condition = apply_text_filter(column, operator, parameter) - - elif operator in [ - FilterOperator.NUMBER_EQUALS, - FilterOperator.NUMBER_NOT_EQUALS, - FilterOperator.NUMBER_GREATER_THAN, - FilterOperator.NUMBER_GREATER_THAN_OR_EQUAL, - FilterOperator.NUMBER_LESS_THAN, - FilterOperator.NUMBER_LESS_THAN_OR_EQUAL, - FilterOperator.NUMBER_BETWEEN, - FilterOperator.NUMBER_NOT_BETWEEN, - ]: - condition = apply_number_filter(column, operator, parameter) - - elif operator in [ - FilterOperator.DATE_EQUALS, - FilterOperator.DATE_NOT_EQUALS, - FilterOperator.DATE_GREATER_THAN, - FilterOperator.DATE_GREATER_THAN_OR_EQUAL, - FilterOperator.DATE_LESS_THAN, - FilterOperator.DATE_LESS_THAN_OR_EQUAL, - FilterOperator.DATE_BETWEEN, - FilterOperator.DATE_NOT_BETWEEN, - ]: - condition = apply_date_filter(column, operator, parameter) - - elif operator in [ - FilterOperator.DATETIME_EQUALS, - FilterOperator.DATETIME_NOT_EQUALS, - FilterOperator.DATETIME_GREATER_THAN, - FilterOperator.DATETIME_GREATER_THAN_OR_EQUAL, - FilterOperator.DATETIME_LESS_THAN, - FilterOperator.DATETIME_LESS_THAN_OR_EQUAL, - FilterOperator.DATETIME_BETWEEN, - FilterOperator.DATETIME_NOT_BETWEEN, - ]: - condition = apply_datetime_filter(column, operator, parameter) - - elif operator in [ - FilterOperator.BOOLEAN_IS_TRUE, - FilterOperator.BOOLEAN_IS_FALSE, - ]: - condition = apply_boolean_filter(column, operator, parameter) - - elif operator in [ - FilterOperator.TAGS_SINGLE_EQUALS, - FilterOperator.TAGS_SINGLE_NOT_EQUALS, - FilterOperator.TAGS_SINGLE_CONTAINS, - FilterOperator.TAGS_SINGLE_NOT_CONTAINS, - ]: - condition = apply_tags_single_filter(column, operator, parameter) - - elif operator in [ - FilterOperator.IS_NULL, - FilterOperator.IS_NOT_NULL, - ]: - condition = apply_null_filter(column, operator, parameter) - - elif filter_input.column_type == ColumnType.PROPERTY: - if not filter_input.property_definition_id: - continue - - field_type = property_field_types.get( - filter_input.property_definition_id, - "FIELD_TYPE_TEXT" - ) - query, property_alias, value_column = get_property_join_alias( - query, model_class, filter_input.property_definition_id, field_type - ) - - operator = filter_input.operator - parameter = filter_input.parameter - - if operator in [ - FilterOperator.TEXT_EQUALS, - FilterOperator.TEXT_NOT_EQUALS, - FilterOperator.TEXT_NOT_WHITESPACE, - FilterOperator.TEXT_CONTAINS, - FilterOperator.TEXT_NOT_CONTAINS, - FilterOperator.TEXT_STARTS_WITH, - FilterOperator.TEXT_ENDS_WITH, - ]: - condition = apply_text_filter( - value_column, operator, parameter - ) - - elif operator in [ - FilterOperator.NUMBER_EQUALS, - FilterOperator.NUMBER_NOT_EQUALS, - FilterOperator.NUMBER_GREATER_THAN, - FilterOperator.NUMBER_GREATER_THAN_OR_EQUAL, - FilterOperator.NUMBER_LESS_THAN, - FilterOperator.NUMBER_LESS_THAN_OR_EQUAL, - FilterOperator.NUMBER_BETWEEN, - FilterOperator.NUMBER_NOT_BETWEEN, - ]: - condition = apply_number_filter(value_column, operator, parameter) - - elif operator in [ - FilterOperator.DATE_EQUALS, - FilterOperator.DATE_NOT_EQUALS, - FilterOperator.DATE_GREATER_THAN, - FilterOperator.DATE_GREATER_THAN_OR_EQUAL, - FilterOperator.DATE_LESS_THAN, - FilterOperator.DATE_LESS_THAN_OR_EQUAL, - FilterOperator.DATE_BETWEEN, - FilterOperator.DATE_NOT_BETWEEN, - ]: - condition = apply_date_filter( - value_column, operator, parameter - ) - - elif operator in [ - FilterOperator.DATETIME_EQUALS, - FilterOperator.DATETIME_NOT_EQUALS, - FilterOperator.DATETIME_GREATER_THAN, - FilterOperator.DATETIME_GREATER_THAN_OR_EQUAL, - FilterOperator.DATETIME_LESS_THAN, - FilterOperator.DATETIME_LESS_THAN_OR_EQUAL, - FilterOperator.DATETIME_BETWEEN, - FilterOperator.DATETIME_NOT_BETWEEN, - ]: - condition = apply_datetime_filter(value_column, operator, parameter) - - elif operator in [ - FilterOperator.BOOLEAN_IS_TRUE, - FilterOperator.BOOLEAN_IS_FALSE, - ]: - condition = apply_boolean_filter( - value_column, operator, parameter - ) - - elif operator in [ - FilterOperator.TAGS_EQUALS, - FilterOperator.TAGS_NOT_EQUALS, - FilterOperator.TAGS_CONTAINS, - FilterOperator.TAGS_NOT_CONTAINS, - ]: - condition = apply_tags_filter( - value_column, operator, parameter - ) - - elif operator in [ - FilterOperator.TAGS_SINGLE_EQUALS, - FilterOperator.TAGS_SINGLE_NOT_EQUALS, - FilterOperator.TAGS_SINGLE_CONTAINS, - FilterOperator.TAGS_SINGLE_NOT_CONTAINS, - ]: - condition = apply_tags_single_filter(value_column, operator, parameter) - - elif operator in [ - FilterOperator.IS_NULL, - FilterOperator.IS_NOT_NULL, - ]: - condition = apply_null_filter( - value_column, operator, parameter - ) - - if condition is not None: - filter_conditions.append(condition) - - if filter_conditions: - query = query.where(and_(*filter_conditions)) - - return query - - -def filtered_and_sorted_query( - filtering_param: str = "filtering", - sorting_param: str = "sorting", - pagination_param: str = "pagination", -): - def decorator(func: Callable[..., Any]) -> Callable[..., Any]: - @wraps(func) - async def wrapper(*args: Any, **kwargs: Any) -> Any: - filtering: list[FilterInput] | None = kwargs.get(filtering_param) - sorting: list[SortInput] | None = kwargs.get(sorting_param) - pagination: PaginationInput | None = kwargs.get(pagination_param) - - result = await func(*args, **kwargs) - - if not isinstance(result, Select): - return result - - model_class = result.column_descriptions[0]["entity"] - if not model_class: - if isinstance(result, Select): - for arg in args: - if ( - hasattr(arg, "context") - and hasattr(arg.context, "db") - ): - db = arg.context.db - query_result = await db.execute(result) - return query_result.scalars().all() - else: - info = kwargs.get("info") - if ( - info - and hasattr(info, "context") - and hasattr(info.context, "db") - ): - db = info.context.db - query_result = await db.execute(result) - return query_result.scalars().all() - return result - - property_field_types: dict[str, str] = {} - - if filtering or sorting: - property_def_ids = set() - if filtering: - for f in filtering: - if ( - f.column_type == ColumnType.PROPERTY - and f.property_definition_id - ): - property_def_ids.add(f.property_definition_id) - if sorting: - for s in sorting: - if ( - s.column_type == ColumnType.PROPERTY - and s.property_definition_id - ): - property_def_ids.add(s.property_definition_id) - - if property_def_ids: - for arg in args: - if ( - hasattr(arg, "context") - and hasattr(arg.context, "db") - ): - db = arg.context.db - prop_defs_result = await db.execute( - select(models.PropertyDefinition).where( - models.PropertyDefinition.id.in_(property_def_ids) - ) - ) - prop_defs = prop_defs_result.scalars().all() - property_field_types = { - str(prop_def.id): prop_def.field_type - for prop_def in prop_defs - } - break - else: - info = kwargs.get("info") - if ( - info - and hasattr(info, "context") - and hasattr(info.context, "db") - ): - db = info.context.db - prop_defs_result = await db.execute( - select(models.PropertyDefinition).where( - models.PropertyDefinition.id.in_(property_def_ids) - ) - ) - prop_defs = prop_defs_result.scalars().all() - property_field_types = { - str(prop_def.id): prop_def.field_type - for prop_def in prop_defs - } - - if filtering: - result = apply_filtering( - result, filtering, model_class, property_field_types - ) - - if sorting: - result = apply_sorting( - result, sorting, model_class, property_field_types - ) - - if pagination and pagination is not strawberry.UNSET: - page_index = pagination.page_index - page_size = pagination.page_size - if page_size: - offset = page_index * page_size - result = apply_pagination(result, limit=page_size, offset=offset) - - if isinstance(result, Select): - for arg in args: - if ( - hasattr(arg, "context") - and hasattr(arg.context, "db") - ): - db = arg.context.db - query_result = await db.execute(result) - return query_result.scalars().all() - else: - info = kwargs.get("info") - if ( - info - and hasattr(info, "context") - and hasattr(info.context, "db") - ): - db = info.context.db - query_result = await db.execute(result) - return query_result.scalars().all() - - return result - - return wrapper - - return decorator diff --git a/backend/api/decorators/full_text_search.py b/backend/api/decorators/full_text_search.py deleted file mode 100644 index b251996..0000000 --- a/backend/api/decorators/full_text_search.py +++ /dev/null @@ -1,116 +0,0 @@ -from functools import wraps -from typing import Any, Callable, TypeVar - -import strawberry -from api.inputs import FullTextSearchInput -from database import models -from database.models.base import Base -from sqlalchemy import Select, String, and_, inspect, or_ -from sqlalchemy.orm import aliased - -T = TypeVar("T") - - -def detect_entity_type(model_class: type[Base]) -> str | None: - if model_class == models.Patient: - return "patient" - if model_class == models.Task: - return "task" - return None - - -def get_text_columns_from_model(model_class: type[Base]) -> list[str]: - mapper = inspect(model_class) - text_columns = [] - for column in mapper.columns: - if isinstance(column.type, String): - text_columns.append(column.key) - return text_columns - - -def apply_full_text_search( - query: Select[Any], - search_input: FullTextSearchInput, - model_class: type[Base], -) -> Select[Any]: - if not search_input.search_text or not search_input.search_text.strip(): - return query - - search_text = search_input.search_text.strip() - search_pattern = f"%{search_text}%" - - search_conditions = [] - - columns_to_search = search_input.search_columns - if columns_to_search is None: - columns_to_search = get_text_columns_from_model(model_class) - - for column_name in columns_to_search: - try: - column = getattr(model_class, column_name) - search_conditions.append(column.ilike(search_pattern)) - except AttributeError: - continue - - if search_input.include_properties: - entity_type = detect_entity_type(model_class) - if entity_type: - property_alias = aliased(models.PropertyValue) - - if entity_type == "patient": - join_condition = property_alias.patient_id == model_class.id - else: - join_condition = property_alias.task_id == model_class.id - - if search_input.property_definition_ids: - property_filter = and_( - property_alias.text_value.ilike(search_pattern), - property_alias.definition_id.in_( - search_input.property_definition_ids - ), - ) - else: - property_filter = ( - property_alias.text_value.ilike(search_pattern) - ) - - query = query.outerjoin(property_alias, join_condition) - search_conditions.append(property_filter) - - if not search_conditions: - return query - - combined_condition = or_(*search_conditions) - query = query.where(combined_condition) - - if search_input.include_properties: - query = query.distinct() - - return query - - -def full_text_search_query(search_param: str = "search"): - def decorator(func: Callable[..., Any]) -> Callable[..., Any]: - @wraps(func) - async def wrapper(*args: Any, **kwargs: Any) -> Any: - search_input: FullTextSearchInput | None = kwargs.get(search_param) - - result = await func(*args, **kwargs) - - if not isinstance(result, Select): - return result - - if not search_input or search_input is strawberry.UNSET: - return result - - model_class = result.column_descriptions[0]["entity"] - if not model_class: - return result - - result = apply_full_text_search(result, search_input, model_class) - - return result - - return wrapper - - return decorator diff --git a/backend/api/inputs.py b/backend/api/inputs.py index 204a80f..b6f9e68 100644 --- a/backend/api/inputs.py +++ b/backend/api/inputs.py @@ -180,109 +180,7 @@ class SortDirection(Enum): DESC = "DESC" -@strawberry.enum -class FilterOperator(Enum): - TEXT_EQUALS = "TEXT_EQUALS" - TEXT_NOT_EQUALS = "TEXT_NOT_EQUALS" - TEXT_NOT_WHITESPACE = "TEXT_NOT_WHITESPACE" - TEXT_CONTAINS = "TEXT_CONTAINS" - TEXT_NOT_CONTAINS = "TEXT_NOT_CONTAINS" - TEXT_STARTS_WITH = "TEXT_STARTS_WITH" - TEXT_ENDS_WITH = "TEXT_ENDS_WITH" - NUMBER_EQUALS = "NUMBER_EQUALS" - NUMBER_NOT_EQUALS = "NUMBER_NOT_EQUALS" - NUMBER_GREATER_THAN = "NUMBER_GREATER_THAN" - NUMBER_GREATER_THAN_OR_EQUAL = "NUMBER_GREATER_THAN_OR_EQUAL" - NUMBER_LESS_THAN = "NUMBER_LESS_THAN" - NUMBER_LESS_THAN_OR_EQUAL = "NUMBER_LESS_THAN_OR_EQUAL" - NUMBER_BETWEEN = "NUMBER_BETWEEN" - NUMBER_NOT_BETWEEN = "NUMBER_NOT_BETWEEN" - DATE_EQUALS = "DATE_EQUALS" - DATE_NOT_EQUALS = "DATE_NOT_EQUALS" - DATE_GREATER_THAN = "DATE_GREATER_THAN" - DATE_GREATER_THAN_OR_EQUAL = "DATE_GREATER_THAN_OR_EQUAL" - DATE_LESS_THAN = "DATE_LESS_THAN" - DATE_LESS_THAN_OR_EQUAL = "DATE_LESS_THAN_OR_EQUAL" - DATE_BETWEEN = "DATE_BETWEEN" - DATE_NOT_BETWEEN = "DATE_NOT_BETWEEN" - DATETIME_EQUALS = "DATETIME_EQUALS" - DATETIME_NOT_EQUALS = "DATETIME_NOT_EQUALS" - DATETIME_GREATER_THAN = "DATETIME_GREATER_THAN" - DATETIME_GREATER_THAN_OR_EQUAL = "DATETIME_GREATER_THAN_OR_EQUAL" - DATETIME_LESS_THAN = "DATETIME_LESS_THAN" - DATETIME_LESS_THAN_OR_EQUAL = "DATETIME_LESS_THAN_OR_EQUAL" - DATETIME_BETWEEN = "DATETIME_BETWEEN" - DATETIME_NOT_BETWEEN = "DATETIME_NOT_BETWEEN" - BOOLEAN_IS_TRUE = "BOOLEAN_IS_TRUE" - BOOLEAN_IS_FALSE = "BOOLEAN_IS_FALSE" - TAGS_EQUALS = "TAGS_EQUALS" - TAGS_NOT_EQUALS = "TAGS_NOT_EQUALS" - TAGS_CONTAINS = "TAGS_CONTAINS" - TAGS_NOT_CONTAINS = "TAGS_NOT_CONTAINS" - TAGS_SINGLE_EQUALS = "TAGS_SINGLE_EQUALS" - TAGS_SINGLE_NOT_EQUALS = "TAGS_SINGLE_NOT_EQUALS" - TAGS_SINGLE_CONTAINS = "TAGS_SINGLE_CONTAINS" - TAGS_SINGLE_NOT_CONTAINS = "TAGS_SINGLE_NOT_CONTAINS" - IS_NULL = "IS_NULL" - IS_NOT_NULL = "IS_NOT_NULL" - - -@strawberry.enum -class ColumnType(Enum): - DIRECT_ATTRIBUTE = "DIRECT_ATTRIBUTE" - PROPERTY = "PROPERTY" - - -@strawberry.input -class FilterParameter: - search_text: str | None = None - is_case_sensitive: bool = False - compare_value: float | None = None - min: float | None = None - max: float | None = None - compare_date: date | None = None - min_date: date | None = None - max_date: date | None = None - compare_date_time: datetime | None = None - min_date_time: datetime | None = None - max_date_time: datetime | None = None - search_tags: list[str] | None = None - property_definition_id: str | None = None - - -@strawberry.input -class SortInput: - column: str - direction: SortDirection - column_type: ColumnType = ColumnType.DIRECT_ATTRIBUTE - property_definition_id: str | None = None - - -@strawberry.input -class FilterInput: - column: str - operator: FilterOperator - parameter: FilterParameter - column_type: ColumnType = ColumnType.DIRECT_ATTRIBUTE - property_definition_id: str | None = None - - @strawberry.input class PaginationInput: page_index: int = 0 page_size: int | None = None - - -@strawberry.input -class QueryOptionsInput: - sorting: list[SortInput] | None = None - filtering: list[FilterInput] | None = None - pagination: PaginationInput | None = None - - -@strawberry.input -class FullTextSearchInput: - search_text: str - search_columns: list[str] | None = None - include_properties: bool = False - property_definition_ids: list[str] | None = None diff --git a/backend/api/query/__init__.py b/backend/api/query/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/backend/api/query/adapters/patient.py b/backend/api/query/adapters/patient.py new file mode 100644 index 0000000..4451751 --- /dev/null +++ b/backend/api/query/adapters/patient.py @@ -0,0 +1,391 @@ +from typing import Any + +from sqlalchemy import Select, and_, case, func, or_, select +from sqlalchemy.orm import aliased + +from api.inputs import PatientState, Sex, SortDirection +from api.query.enums import ( + QueryOperator, + QueryableFieldKind, + QueryableValueType, + ReferenceFilterMode, +) +from api.query.field_ops import apply_ops_to_column +from api.query.graphql_types import QueryableChoiceMeta, QueryableField, QueryableRelationMeta +from api.query.inputs import QueryFilterClauseInput, QuerySearchInput, QuerySortClauseInput +from api.query.property_sql import join_property_value +from api.query.sql_expr import location_title_expr, patient_display_name_expr +from database import models + + +def _state_order_case() -> Any: + return case( + (models.Patient.state == PatientState.WAIT.value, 0), + (models.Patient.state == PatientState.ADMITTED.value, 1), + (models.Patient.state == PatientState.DISCHARGED.value, 2), + (models.Patient.state == PatientState.DEAD.value, 3), + else_=4, + ) + + +def _ensure_position_join(query: Select[Any], ctx: dict[str, Any]) -> tuple[Select[Any], Any]: + if "position_node" in ctx: + return query, ctx["position_node"] + ln = aliased(models.LocationNode) + ctx["position_node"] = ln + query = query.outerjoin(ln, models.Patient.position_id == ln.id) + ctx["needs_distinct"] = True + return query, ln + + +def _parse_property_key(field_key: str) -> str | None: + if not field_key.startswith("property_"): + return None + return field_key.removeprefix("property_") + + +def apply_patient_filter_clause( + query: Select[Any], + clause: QueryFilterClauseInput, + ctx: dict[str, Any], + property_field_types: dict[str, str], +) -> Select[Any]: + key = clause.field_key + op = clause.operator + val = clause.value + + prop_id = _parse_property_key(key) + if prop_id: + ft = property_field_types.get(prop_id, "FIELD_TYPE_TEXT") + query, _pa, col = join_property_value( + query, models.Patient, prop_id, ft, "patient" + ) + ctx["needs_distinct"] = True + if ft == "FIELD_TYPE_MULTI_SELECT": + cond = apply_ops_to_column(col, op, val) + elif ft == "FIELD_TYPE_DATE": + cond = apply_ops_to_column(col, op, val, as_date=True) + elif ft == "FIELD_TYPE_DATE_TIME": + cond = apply_ops_to_column(col, op, val, as_datetime=True) + elif ft == "FIELD_TYPE_CHECKBOX": + if op == QueryOperator.EQ and val and val.bool_value is not None: + cond = col == val.bool_value + else: + cond = apply_ops_to_column(col, op, val) + elif ft == "FIELD_TYPE_USER": + cond = apply_ops_to_column(col, op, val) + elif ft == "FIELD_TYPE_SELECT": + cond = apply_ops_to_column(col, op, val) + else: + cond = apply_ops_to_column(col, op, val) + if cond is not None: + query = query.where(cond) + return query + + if key == "firstname": + c = apply_ops_to_column(models.Patient.firstname, op, val) + if c is not None: + query = query.where(c) + return query + if key == "lastname": + c = apply_ops_to_column(models.Patient.lastname, op, val) + if c is not None: + query = query.where(c) + return query + if key == "name": + expr = patient_display_name_expr(models.Patient) + c = apply_ops_to_column(expr, op, val) + if c is not None: + query = query.where(c) + return query + if key == "state": + c = apply_ops_to_column(models.Patient.state, op, val) + if c is not None: + query = query.where(c) + return query + if key == "sex": + c = apply_ops_to_column(models.Patient.sex, op, val) + if c is not None: + query = query.where(c) + return query + if key == "birthdate": + c = apply_ops_to_column(models.Patient.birthdate, op, val, as_date=True) + if c is not None: + query = query.where(c) + return query + if key == "description": + c = apply_ops_to_column(models.Patient.description, op, val) + if c is not None: + query = query.where(c) + return query + if key == "position": + query, ln = _ensure_position_join(query, ctx) + expr = location_title_expr(ln) + if op in (QueryOperator.EQ, QueryOperator.IN) and val and val.uuid_value: + query = query.where(models.Patient.position_id == val.uuid_value) + elif op in ( + QueryOperator.CONTAINS, + QueryOperator.STARTS_WITH, + QueryOperator.ENDS_WITH, + ): + c = apply_ops_to_column(expr, op, val) + if c is not None: + query = query.where(c) + elif op in (QueryOperator.IS_NULL, QueryOperator.IS_NOT_NULL): + c = apply_ops_to_column(models.Patient.position_id, op, None) + if c is not None: + query = query.where(c) + return query + + return query + + +def apply_patient_sorts( + query: Select[Any], + sorts: list[QuerySortClauseInput] | None, + ctx: dict[str, Any], + property_field_types: dict[str, str], +) -> Select[Any]: + if not sorts: + return query.order_by(models.Patient.id.asc()) + + order_parts: list[Any] = [] + + for s in sorts: + key = s.field_key + desc_order = s.direction == SortDirection.DESC + prop_id = _parse_property_key(key) + if prop_id: + ft = property_field_types.get(prop_id, "FIELD_TYPE_TEXT") + query, _pa, col = join_property_value( + query, models.Patient, prop_id, ft, "patient" + ) + ctx["needs_distinct"] = True + if desc_order: + order_parts.append(col.desc().nulls_last()) + else: + order_parts.append(col.asc().nulls_first()) + continue + + if key == "firstname": + order_parts.append( + models.Patient.firstname.desc() + if desc_order + else models.Patient.firstname.asc() + ) + elif key == "lastname": + order_parts.append( + models.Patient.lastname.desc() + if desc_order + else models.Patient.lastname.asc() + ) + elif key == "name": + expr = patient_display_name_expr(models.Patient) + order_parts.append( + expr.desc().nulls_last() if desc_order else expr.asc().nulls_first() + ) + elif key == "state": + order_parts.append( + _state_order_case().desc() + if desc_order + else _state_order_case().asc() + ) + elif key == "sex": + order_parts.append( + models.Patient.sex.desc() if desc_order else models.Patient.sex.asc() + ) + elif key == "birthdate": + order_parts.append( + models.Patient.birthdate.desc().nulls_last() + if desc_order + else models.Patient.birthdate.asc().nulls_first() + ) + elif key == "description": + order_parts.append( + models.Patient.description.desc().nulls_last() + if desc_order + else models.Patient.description.asc().nulls_first() + ) + elif key == "position": + query, ln = _ensure_position_join(query, ctx) + t = location_title_expr(ln) + order_parts.append( + t.desc().nulls_last() if desc_order else t.asc().nulls_first() + ) + + order_parts.append(models.Patient.id.asc()) + return query.order_by(*order_parts) + + +def apply_patient_search( + query: Select[Any], + search: QuerySearchInput | None, + ctx: dict[str, Any], +) -> Select[Any]: + if not search or not search.search_text or not search.search_text.strip(): + return query + pattern = f"%{search.search_text.strip()}%" + expr = patient_display_name_expr(models.Patient) + parts: list[Any] = [ + models.Patient.firstname.ilike(pattern), + models.Patient.lastname.ilike(pattern), + expr.ilike(pattern), + models.Patient.description.ilike(pattern), + ] + if search.include_properties: + pv = aliased(models.PropertyValue) + query = query.outerjoin( + pv, + and_( + pv.patient_id == models.Patient.id, + pv.text_value.isnot(None), + ), + ) + parts.append(pv.text_value.ilike(pattern)) + ctx["needs_distinct"] = True + query = query.where(or_(*parts)) + return query + + +def build_patient_queryable_fields_static() -> list[QueryableField]: + str_ops = [ + QueryOperator.EQ, + QueryOperator.NEQ, + QueryOperator.CONTAINS, + QueryOperator.STARTS_WITH, + QueryOperator.ENDS_WITH, + QueryOperator.IN, + QueryOperator.NOT_IN, + QueryOperator.IS_NULL, + QueryOperator.IS_NOT_NULL, + ] + date_ops = [ + QueryOperator.EQ, + QueryOperator.NEQ, + QueryOperator.GT, + QueryOperator.GTE, + QueryOperator.LT, + QueryOperator.LTE, + QueryOperator.BETWEEN, + QueryOperator.IS_NULL, + QueryOperator.IS_NOT_NULL, + ] + choice_ops = [ + QueryOperator.EQ, + QueryOperator.NEQ, + QueryOperator.IN, + QueryOperator.NOT_IN, + QueryOperator.IS_NULL, + QueryOperator.IS_NOT_NULL, + ] + states = [ + PatientState.WAIT, + PatientState.ADMITTED, + PatientState.DISCHARGED, + PatientState.DEAD, + ] + state_keys = [s.value for s in states] + state_labels = [s.value for s in states] + + sex_keys = [Sex.MALE.value, Sex.FEMALE.value, Sex.UNKNOWN.value] + sex_labels = ["Male", "Female", "Unknown"] + + return [ + QueryableField( + key="name", + label="Name", + kind=QueryableFieldKind.SCALAR, + value_type=QueryableValueType.STRING, + allowed_operators=str_ops, + sortable=True, + searchable=True, + ), + QueryableField( + key="firstname", + label="First name", + kind=QueryableFieldKind.SCALAR, + value_type=QueryableValueType.STRING, + allowed_operators=str_ops, + sortable=True, + searchable=True, + ), + QueryableField( + key="lastname", + label="Last name", + kind=QueryableFieldKind.SCALAR, + value_type=QueryableValueType.STRING, + allowed_operators=str_ops, + sortable=True, + searchable=True, + ), + QueryableField( + key="state", + label="State", + kind=QueryableFieldKind.CHOICE, + value_type=QueryableValueType.STRING, + allowed_operators=choice_ops, + sortable=True, + searchable=False, + choice=QueryableChoiceMeta( + option_keys=state_keys, + option_labels=state_labels, + ), + ), + QueryableField( + key="sex", + label="Sex", + kind=QueryableFieldKind.CHOICE, + value_type=QueryableValueType.STRING, + allowed_operators=choice_ops, + sortable=True, + searchable=False, + choice=QueryableChoiceMeta( + option_keys=sex_keys, + option_labels=sex_labels, + ), + ), + QueryableField( + key="birthdate", + label="Birthdate", + kind=QueryableFieldKind.SCALAR, + value_type=QueryableValueType.DATE, + allowed_operators=date_ops, + sortable=True, + searchable=False, + ), + QueryableField( + key="description", + label="Description", + kind=QueryableFieldKind.SCALAR, + value_type=QueryableValueType.STRING, + allowed_operators=str_ops, + sortable=True, + searchable=True, + ), + QueryableField( + key="position", + label="Location", + kind=QueryableFieldKind.REFERENCE, + value_type=QueryableValueType.UUID, + allowed_operators=[ + QueryOperator.EQ, + QueryOperator.IN, + QueryOperator.CONTAINS, + QueryOperator.STARTS_WITH, + QueryOperator.ENDS_WITH, + QueryOperator.IS_NULL, + QueryOperator.IS_NOT_NULL, + ], + sortable=True, + searchable=False, + relation=QueryableRelationMeta( + target_entity="LocationNode", + id_field_key="id", + label_field_key="title", + allowed_filter_modes=[ + ReferenceFilterMode.ID, + ReferenceFilterMode.LABEL, + ], + ), + ), + ] diff --git a/backend/api/query/adapters/task.py b/backend/api/query/adapters/task.py new file mode 100644 index 0000000..5899213 --- /dev/null +++ b/backend/api/query/adapters/task.py @@ -0,0 +1,526 @@ +from typing import Any + +from sqlalchemy import Select, and_, case, func, or_, select +from sqlalchemy.orm import aliased + +from api.inputs import SortDirection +from api.query.enums import ( + QueryOperator, + QueryableFieldKind, + QueryableValueType, + ReferenceFilterMode, +) +from api.query.field_ops import apply_ops_to_column +from api.query.graphql_types import QueryableChoiceMeta, QueryableField, QueryableRelationMeta +from api.query.inputs import ( + QueryFilterClauseInput, + QuerySearchInput, + QuerySortClauseInput, +) +from api.query.property_sql import join_property_value +from api.query.sql_expr import location_title_expr, patient_display_name_expr, user_display_label_expr +from database import models + + +def _prio_order_case() -> Any: + return case( + (models.Task.priority == "P1", 1), + (models.Task.priority == "P2", 2), + (models.Task.priority == "P3", 3), + (models.Task.priority == "P4", 4), + else_=99, + ) + + +def _ensure_assignee_join(query: Select[Any], ctx: dict[str, Any]) -> tuple[Select[Any], Any]: + if "assignee_user" in ctx: + return query, ctx["assignee_user"] + u = aliased(models.User) + ctx["assignee_user"] = u + query = query.outerjoin(u, models.Task.assignee_id == u.id) + ctx["needs_distinct"] = True + return query, u + + +def _ensure_team_join(query: Select[Any], ctx: dict[str, Any]) -> tuple[Select[Any], Any]: + if "assignee_team" in ctx: + return query, ctx["assignee_team"] + ln = aliased(models.LocationNode) + ctx["assignee_team"] = ln + query = query.outerjoin(ln, models.Task.assignee_team_id == ln.id) + ctx["needs_distinct"] = True + return query, ln + + +def _parse_property_key(field_key: str) -> str | None: + if not field_key.startswith("property_"): + return None + return field_key.removeprefix("property_") + + +def apply_task_filter_clause( + query: Select[Any], + clause: QueryFilterClauseInput, + ctx: dict[str, Any], + property_field_types: dict[str, str], +) -> Select[Any]: + key = clause.field_key + op = clause.operator + val = clause.value + + prop_id = _parse_property_key(key) + if prop_id: + ft = property_field_types.get(prop_id, "FIELD_TYPE_TEXT") + ent = "task" + query, _pa, col = join_property_value( + query, models.Task, prop_id, ft, ent + ) + ctx["needs_distinct"] = True + ctx.setdefault("property_joins", set()).add(prop_id) + if ft == "FIELD_TYPE_MULTI_SELECT": + if op in ( + QueryOperator.IN, + QueryOperator.ANY_IN, + QueryOperator.ALL_IN, + QueryOperator.NONE_IN, + ): + cond = apply_ops_to_column(col, op, val) + else: + cond = apply_ops_to_column(col, op, val, as_date=False) + elif ft == "FIELD_TYPE_DATE": + cond = apply_ops_to_column(col, op, val, as_date=True) + elif ft == "FIELD_TYPE_DATE_TIME": + cond = apply_ops_to_column(col, op, val, as_datetime=True) + elif ft == "FIELD_TYPE_CHECKBOX": + if op == QueryOperator.EQ and val and val.bool_value is not None: + cond = col == val.bool_value + else: + cond = apply_ops_to_column(col, op, val) + elif ft == "FIELD_TYPE_USER": + cond = apply_ops_to_column(col, op, val) + elif ft == "FIELD_TYPE_SELECT": + cond = apply_ops_to_column(col, op, val) + else: + cond = apply_ops_to_column(col, op, val) + if cond is not None: + query = query.where(cond) + return query + + if key == "title": + c = apply_ops_to_column(models.Task.title, op, val) + if c is not None: + query = query.where(c) + return query + if key == "description": + c = apply_ops_to_column(models.Task.description, op, val) + if c is not None: + query = query.where(c) + return query + if key == "done": + if op == QueryOperator.EQ and val and val.bool_value is not None: + query = query.where(models.Task.done == val.bool_value) + elif op == QueryOperator.IS_NULL: + query = query.where(models.Task.done.is_(None)) + elif op == QueryOperator.IS_NOT_NULL: + query = query.where(models.Task.done.isnot(None)) + return query + if key == "dueDate": + c = apply_ops_to_column(models.Task.due_date, op, val, as_datetime=True) + if c is not None: + query = query.where(c) + return query + if key == "priority": + c = apply_ops_to_column(models.Task.priority, op, val) + if c is not None: + query = query.where(c) + return query + if key == "estimatedTime": + c = apply_ops_to_column(models.Task.estimated_time, op, val) + if c is not None: + query = query.where(c) + return query + if key == "creationDate": + c = apply_ops_to_column(models.Task.creation_date, op, val, as_datetime=True) + if c is not None: + query = query.where(c) + return query + if key == "updateDate": + c = apply_ops_to_column(models.Task.update_date, op, val, as_datetime=True) + if c is not None: + query = query.where(c) + return query + + if key == "assignee": + query, u = _ensure_assignee_join(query, ctx) + label = user_display_label_expr(u) + if op in (QueryOperator.EQ, QueryOperator.IN) and val and ( + val.uuid_value or val.uuid_values + ): + if val.uuid_value: + query = query.where(models.Task.assignee_id == val.uuid_value) + elif val.uuid_values: + query = query.where(models.Task.assignee_id.in_(val.uuid_values)) + elif op in ( + QueryOperator.CONTAINS, + QueryOperator.STARTS_WITH, + QueryOperator.ENDS_WITH, + ): + c = apply_ops_to_column(label, op, val) + if c is not None: + query = query.where(c) + elif op in (QueryOperator.IS_NULL, QueryOperator.IS_NOT_NULL): + c = apply_ops_to_column(models.Task.assignee_id, op, None) + if c is not None: + query = query.where(c) + return query + + if key == "assigneeTeam": + query, ln = _ensure_team_join(query, ctx) + expr = location_title_expr(ln) + if op in (QueryOperator.EQ, QueryOperator.IN) and val and val.uuid_value: + query = query.where(models.Task.assignee_team_id == val.uuid_value) + elif op in ( + QueryOperator.CONTAINS, + QueryOperator.STARTS_WITH, + QueryOperator.ENDS_WITH, + ): + c = apply_ops_to_column(expr, op, val) + if c is not None: + query = query.where(c) + elif op in (QueryOperator.IS_NULL, QueryOperator.IS_NOT_NULL): + c = apply_ops_to_column(models.Task.assignee_team_id, op, None) + if c is not None: + query = query.where(c) + return query + + if key == "patient": + p = models.Patient + expr = patient_display_name_expr(p) + if op in (QueryOperator.EQ, QueryOperator.IN) and val and ( + val.uuid_value or val.uuid_values + ): + if val.uuid_value: + query = query.where(models.Task.patient_id == val.uuid_value) + elif val.uuid_values: + query = query.where(models.Task.patient_id.in_(val.uuid_values)) + elif op in ( + QueryOperator.CONTAINS, + QueryOperator.STARTS_WITH, + QueryOperator.ENDS_WITH, + ): + c = apply_ops_to_column(expr, op, val) + if c is not None: + query = query.where(c) + return query + + return query + + +def apply_task_sorts( + query: Select[Any], + sorts: list[QuerySortClauseInput] | None, + ctx: dict[str, Any], + property_field_types: dict[str, str], +) -> Select[Any]: + if not sorts: + return query.order_by(models.Task.id.asc()) + + order_parts: list[Any] = [] + + for s in sorts: + key = s.field_key + desc_order = s.direction == SortDirection.DESC + prop_id = _parse_property_key(key) + if prop_id: + ft = property_field_types.get(prop_id, "FIELD_TYPE_TEXT") + query, _pa, col = join_property_value( + query, models.Task, prop_id, ft, "task" + ) + ctx["needs_distinct"] = True + if desc_order: + order_parts.append(col.desc().nulls_last()) + else: + order_parts.append(col.asc().nulls_first()) + continue + + if key == "title": + order_parts.append( + models.Task.title.desc() if desc_order else models.Task.title.asc() + ) + elif key == "description": + order_parts.append( + models.Task.description.desc() + if desc_order + else models.Task.description.asc() + ) + elif key == "done": + order_parts.append( + models.Task.done.desc() if desc_order else models.Task.done.asc() + ) + elif key == "dueDate": + order_parts.append( + models.Task.due_date.desc().nulls_last() + if desc_order + else models.Task.due_date.asc().nulls_first() + ) + elif key == "priority": + order_parts.append( + _prio_order_case().desc() + if desc_order + else _prio_order_case().asc() + ) + elif key == "estimatedTime": + order_parts.append( + models.Task.estimated_time.desc().nulls_last() + if desc_order + else models.Task.estimated_time.asc().nulls_first() + ) + elif key == "creationDate": + order_parts.append( + models.Task.creation_date.desc().nulls_last() + if desc_order + else models.Task.creation_date.asc().nulls_first() + ) + elif key == "updateDate": + order_parts.append( + models.Task.update_date.desc().nulls_last() + if desc_order + else models.Task.update_date.asc().nulls_first() + ) + elif key == "assignee": + query, u = _ensure_assignee_join(query, ctx) + label = user_display_label_expr(u) + order_parts.append( + label.desc().nulls_last() if desc_order else label.asc().nulls_first() + ) + elif key == "assigneeTeam": + query, ln = _ensure_team_join(query, ctx) + t = location_title_expr(ln) + order_parts.append( + t.desc().nulls_last() if desc_order else t.asc().nulls_first() + ) + elif key == "patient": + expr = patient_display_name_expr(models.Patient) + order_parts.append( + expr.desc().nulls_last() if desc_order else expr.asc().nulls_first() + ) + + order_parts.append(models.Task.id.asc()) + return query.order_by(*order_parts) + + +def apply_task_search( + query: Select[Any], + search: QuerySearchInput | None, + ctx: dict[str, Any], +) -> Select[Any]: + if not search or not search.search_text or not search.search_text.strip(): + return query + pattern = f"%{search.search_text.strip()}%" + query, u = _ensure_assignee_join(query, ctx) + parts: list[Any] = [ + models.Task.title.ilike(pattern), + models.Task.description.ilike(pattern), + patient_display_name_expr(models.Patient).ilike(pattern), + user_display_label_expr(u).ilike(pattern), + ] + if search.include_properties: + pv = aliased(models.PropertyValue) + query = query.outerjoin( + pv, + and_( + pv.task_id == models.Task.id, + pv.text_value.isnot(None), + ), + ) + parts.append(pv.text_value.ilike(pattern)) + ctx["needs_distinct"] = True + query = query.where(or_(*parts)) + return query + + +def build_task_queryable_fields_static() -> list[QueryableField]: + prio_ops = [ + QueryOperator.EQ, + QueryOperator.NEQ, + QueryOperator.IN, + QueryOperator.NOT_IN, + QueryOperator.IS_NULL, + QueryOperator.IS_NOT_NULL, + ] + ref_ops = [ + QueryOperator.EQ, + QueryOperator.IN, + QueryOperator.CONTAINS, + QueryOperator.STARTS_WITH, + QueryOperator.ENDS_WITH, + QueryOperator.IS_NULL, + QueryOperator.IS_NOT_NULL, + ] + str_ops = [ + QueryOperator.EQ, + QueryOperator.NEQ, + QueryOperator.CONTAINS, + QueryOperator.STARTS_WITH, + QueryOperator.ENDS_WITH, + QueryOperator.IN, + QueryOperator.NOT_IN, + QueryOperator.IS_NULL, + QueryOperator.IS_NOT_NULL, + ] + num_ops = [ + QueryOperator.EQ, + QueryOperator.NEQ, + QueryOperator.GT, + QueryOperator.GTE, + QueryOperator.LT, + QueryOperator.LTE, + QueryOperator.BETWEEN, + QueryOperator.IS_NULL, + QueryOperator.IS_NOT_NULL, + ] + dt_ops = [ + QueryOperator.EQ, + QueryOperator.NEQ, + QueryOperator.GT, + QueryOperator.GTE, + QueryOperator.LT, + QueryOperator.LTE, + QueryOperator.BETWEEN, + QueryOperator.IS_NULL, + QueryOperator.IS_NOT_NULL, + ] + bool_ops = [QueryOperator.EQ, QueryOperator.IS_NULL, QueryOperator.IS_NOT_NULL] + + return [ + QueryableField( + key="title", + label="Title", + kind=QueryableFieldKind.SCALAR, + value_type=QueryableValueType.STRING, + allowed_operators=str_ops, + sortable=True, + searchable=True, + ), + QueryableField( + key="description", + label="Description", + kind=QueryableFieldKind.SCALAR, + value_type=QueryableValueType.STRING, + allowed_operators=str_ops, + sortable=True, + searchable=True, + ), + QueryableField( + key="done", + label="Done", + kind=QueryableFieldKind.SCALAR, + value_type=QueryableValueType.BOOLEAN, + allowed_operators=bool_ops, + sortable=True, + searchable=False, + ), + QueryableField( + key="dueDate", + label="Due date", + kind=QueryableFieldKind.SCALAR, + value_type=QueryableValueType.DATETIME, + allowed_operators=dt_ops, + sortable=True, + searchable=False, + ), + QueryableField( + key="priority", + label="Priority", + kind=QueryableFieldKind.CHOICE, + value_type=QueryableValueType.STRING, + allowed_operators=prio_ops, + sortable=True, + searchable=False, + choice=QueryableChoiceMeta( + option_keys=["P1", "P2", "P3", "P4"], + option_labels=["P1", "P2", "P3", "P4"], + ), + ), + QueryableField( + key="estimatedTime", + label="Estimated time", + kind=QueryableFieldKind.SCALAR, + value_type=QueryableValueType.NUMBER, + allowed_operators=num_ops, + sortable=True, + searchable=False, + ), + QueryableField( + key="creationDate", + label="Creation date", + kind=QueryableFieldKind.SCALAR, + value_type=QueryableValueType.DATETIME, + allowed_operators=dt_ops, + sortable=True, + searchable=False, + ), + QueryableField( + key="updateDate", + label="Update date", + kind=QueryableFieldKind.SCALAR, + value_type=QueryableValueType.DATETIME, + allowed_operators=dt_ops, + sortable=True, + searchable=False, + ), + QueryableField( + key="patient", + label="Patient", + kind=QueryableFieldKind.REFERENCE, + value_type=QueryableValueType.UUID, + allowed_operators=ref_ops, + sortable=True, + searchable=True, + relation=QueryableRelationMeta( + target_entity="Patient", + id_field_key="id", + label_field_key="name", + allowed_filter_modes=[ + ReferenceFilterMode.ID, + ReferenceFilterMode.LABEL, + ], + ), + ), + QueryableField( + key="assignee", + label="Assignee", + kind=QueryableFieldKind.REFERENCE, + value_type=QueryableValueType.UUID, + allowed_operators=ref_ops, + sortable=True, + searchable=True, + relation=QueryableRelationMeta( + target_entity="User", + id_field_key="id", + label_field_key="name", + allowed_filter_modes=[ + ReferenceFilterMode.ID, + ReferenceFilterMode.LABEL, + ], + ), + ), + QueryableField( + key="assigneeTeam", + label="Assignee team", + kind=QueryableFieldKind.REFERENCE, + value_type=QueryableValueType.UUID, + allowed_operators=ref_ops, + sortable=True, + searchable=False, + relation=QueryableRelationMeta( + target_entity="LocationNode", + id_field_key="id", + label_field_key="title", + allowed_filter_modes=[ + ReferenceFilterMode.ID, + ReferenceFilterMode.LABEL, + ], + ), + ), + ] diff --git a/backend/api/query/adapters/user.py b/backend/api/query/adapters/user.py new file mode 100644 index 0000000..750f1b0 --- /dev/null +++ b/backend/api/query/adapters/user.py @@ -0,0 +1,144 @@ +from typing import Any + +from sqlalchemy import Select, or_ + +from api.inputs import SortDirection +from api.query.enums import QueryOperator, QueryableFieldKind, QueryableValueType +from api.query.field_ops import apply_ops_to_column +from api.query.graphql_types import QueryableField +from api.query.inputs import QueryFilterClauseInput, QuerySearchInput, QuerySortClauseInput +from api.query.sql_expr import user_display_label_expr +from database import models + + +def apply_user_filter_clause( + query: Select[Any], + clause: QueryFilterClauseInput, + ctx: dict[str, Any], + property_field_types: dict[str, str], +) -> Select[Any]: + key = clause.field_key + op = clause.operator + val = clause.value + + if key == "username": + c = apply_ops_to_column(models.User.username, op, val) + if c is not None: + query = query.where(c) + return query + if key == "email": + c = apply_ops_to_column(models.User.email, op, val) + if c is not None: + query = query.where(c) + return query + if key == "firstname": + c = apply_ops_to_column(models.User.firstname, op, val) + if c is not None: + query = query.where(c) + return query + if key == "lastname": + c = apply_ops_to_column(models.User.lastname, op, val) + if c is not None: + query = query.where(c) + return query + if key == "name": + expr = user_display_label_expr(models.User) + c = apply_ops_to_column(expr, op, val) + if c is not None: + query = query.where(c) + return query + return query + + +def apply_user_sorts( + query: Select[Any], + sorts: list[QuerySortClauseInput] | None, + ctx: dict[str, Any], + property_field_types: dict[str, str], +) -> Select[Any]: + if not sorts: + return query.order_by(models.User.id.asc()) + + order_parts: list[Any] = [] + for s in sorts: + key = s.field_key + desc_order = s.direction == SortDirection.DESC + if key == "username": + order_parts.append( + models.User.username.desc() + if desc_order + else models.User.username.asc() + ) + elif key == "email": + order_parts.append( + models.User.email.desc().nulls_last() + if desc_order + else models.User.email.asc().nulls_first() + ) + elif key == "name": + expr = user_display_label_expr(models.User) + order_parts.append( + expr.desc().nulls_last() if desc_order else expr.asc().nulls_first() + ) + order_parts.append(models.User.id.asc()) + return query.order_by(*order_parts) + + +def apply_user_search( + query: Select[Any], + search: QuerySearchInput | None, + ctx: dict[str, Any], +) -> Select[Any]: + if not search or not search.search_text or not search.search_text.strip(): + return query + pattern = f"%{search.search_text.strip()}%" + expr = user_display_label_expr(models.User) + query = query.where( + or_( + models.User.username.ilike(pattern), + models.User.email.ilike(pattern), + expr.ilike(pattern), + ) + ) + return query + + +def build_user_queryable_fields_static() -> list[QueryableField]: + str_ops = [ + QueryOperator.EQ, + QueryOperator.NEQ, + QueryOperator.CONTAINS, + QueryOperator.STARTS_WITH, + QueryOperator.ENDS_WITH, + QueryOperator.IS_NULL, + QueryOperator.IS_NOT_NULL, + ] + return [ + QueryableField( + key="username", + label="Username", + kind=QueryableFieldKind.SCALAR, + value_type=QueryableValueType.STRING, + allowed_operators=str_ops, + sortable=True, + searchable=True, + ), + QueryableField( + key="email", + label="Email", + kind=QueryableFieldKind.SCALAR, + value_type=QueryableValueType.STRING, + allowed_operators=str_ops, + sortable=True, + searchable=True, + ), + QueryableField( + key="name", + label="Name", + kind=QueryableFieldKind.SCALAR, + value_type=QueryableValueType.STRING, + allowed_operators=str_ops, + sortable=True, + searchable=True, + ), + ] diff --git a/backend/api/query/context.py b/backend/api/query/context.py new file mode 100644 index 0000000..e5b1bc6 --- /dev/null +++ b/backend/api/query/context.py @@ -0,0 +1,14 @@ +from dataclasses import dataclass, field +from typing import Any + +from sqlalchemy.ext.asyncio import AsyncSession +from sqlalchemy.orm import DeclarativeBase + + +@dataclass +class QueryCompileContext: + db: AsyncSession + root_model: type[DeclarativeBase] + entity_key: str + aliases: dict[str, Any] = field(default_factory=dict) + needs_distinct: bool = False diff --git a/backend/api/query/engine.py b/backend/api/query/engine.py new file mode 100644 index 0000000..3800926 --- /dev/null +++ b/backend/api/query/engine.py @@ -0,0 +1,80 @@ +from typing import Any + +import strawberry +from sqlalchemy import Select + +from api.inputs import PaginationInput +from api.query.inputs import QueryFilterClauseInput, QuerySearchInput, QuerySortClauseInput +from api.query.property_sql import load_property_field_types +from api.query.registry import get_entity_handler + + +def _property_ids_from_filters( + filters: list[QueryFilterClauseInput] | None, +) -> set[str]: + ids: set[str] = set() + if not filters: + return ids + for f in filters: + if f.field_key.startswith("property_"): + ids.add(f.field_key.removeprefix("property_")) + return ids + + +def _property_ids_from_sorts( + sorts: list[QuerySortClauseInput] | None, +) -> set[str]: + ids: set[str] = set() + if not sorts: + return ids + for s in sorts: + if s.field_key.startswith("property_"): + ids.add(s.field_key.removeprefix("property_")) + return ids + + +async def apply_unified_query( + stmt: Select[Any], + *, + entity: str, + db: Any, + filters: list[QueryFilterClauseInput] | None, + sorts: list[QuerySortClauseInput] | None, + search: QuerySearchInput | None, + pagination: PaginationInput | None, + for_count: bool = False, +) -> Select[Any]: + handler = get_entity_handler(entity) + if not handler: + return stmt + + prop_ids = _property_ids_from_filters(filters) | _property_ids_from_sorts(sorts) + property_field_types = await load_property_field_types(db, prop_ids) + + ctx: dict[str, Any] = {"needs_distinct": False} + + for clause in filters or []: + stmt = handler["apply_filter"](stmt, clause, ctx, property_field_types) + + if search is not None and search is not strawberry.UNSET: + text = (search.search_text or "").strip() + if text: + stmt = handler["apply_search"](stmt, search, ctx) + + if not for_count: + stmt = handler["apply_sorts"](stmt, sorts, ctx, property_field_types) + + if ctx.get("needs_distinct"): + stmt = stmt.distinct() + + if ( + not for_count + and pagination is not None + and pagination is not strawberry.UNSET + ): + page_size = pagination.page_size + if page_size: + offset = pagination.page_index * page_size + stmt = stmt.offset(offset).limit(page_size) + + return stmt diff --git a/backend/api/query/enums.py b/backend/api/query/enums.py new file mode 100644 index 0000000..cb09916 --- /dev/null +++ b/backend/api/query/enums.py @@ -0,0 +1,55 @@ +from enum import Enum + +import strawberry + + +@strawberry.enum +class QueryOperator(Enum): + EQ = "EQ" + NEQ = "NEQ" + GT = "GT" + GTE = "GTE" + LT = "LT" + LTE = "LTE" + BETWEEN = "BETWEEN" + IN = "IN" + NOT_IN = "NOT_IN" + CONTAINS = "CONTAINS" + STARTS_WITH = "STARTS_WITH" + ENDS_WITH = "ENDS_WITH" + IS_NULL = "IS_NULL" + IS_NOT_NULL = "IS_NOT_NULL" + ANY_EQ = "ANY_EQ" + ANY_IN = "ANY_IN" + ALL_IN = "ALL_IN" + NONE_IN = "NONE_IN" + IS_EMPTY = "IS_EMPTY" + IS_NOT_EMPTY = "IS_NOT_EMPTY" + + +@strawberry.enum +class QueryableFieldKind(Enum): + SCALAR = "SCALAR" + PROPERTY = "PROPERTY" + REFERENCE = "REFERENCE" + REFERENCE_LIST = "REFERENCE_LIST" + CHOICE = "CHOICE" + CHOICE_LIST = "CHOICE_LIST" + + +@strawberry.enum +class QueryableValueType(Enum): + STRING = "STRING" + NUMBER = "NUMBER" + BOOLEAN = "BOOLEAN" + DATE = "DATE" + DATETIME = "DATETIME" + UUID = "UUID" + STRING_LIST = "STRING_LIST" + UUID_LIST = "UUID_LIST" + + +@strawberry.enum +class ReferenceFilterMode(Enum): + ID = "ID" + LABEL = "LABEL" diff --git a/backend/api/query/execute.py b/backend/api/query/execute.py new file mode 100644 index 0000000..bf0e0c2 --- /dev/null +++ b/backend/api/query/execute.py @@ -0,0 +1,88 @@ +from functools import wraps +from typing import Any, Callable + +import strawberry +from sqlalchemy import Select, func, select + +from api.context import Info +from api.inputs import PaginationInput +from api.query.engine import apply_unified_query +from api.query.inputs import QueryFilterClauseInput, QuerySearchInput, QuerySortClauseInput + + +def unified_list_query( + entity: str, + *, + default_sorts_when_empty: list[QuerySortClauseInput] | None = None, +): + def decorator(func: Callable[..., Any]) -> Callable[..., Any]: + @wraps(func) + async def wrapper(*args: Any, **kwargs: Any) -> Any: + filters: list[QueryFilterClauseInput] | None = kwargs.get("filters") + sorts: list[QuerySortClauseInput] | None = kwargs.get("sorts") + if (not sorts) and default_sorts_when_empty: + sorts = list(default_sorts_when_empty) + search: QuerySearchInput | None = kwargs.get("search") + pagination: PaginationInput | None = kwargs.get("pagination") + + result = await func(*args, **kwargs) + + if not isinstance(result, Select): + return result + + info: Info | None = kwargs.get("info") + if not info: + for a in args: + if hasattr(a, "context"): + info = a + break + if not info or not hasattr(info, "context"): + return result + + stmt = await apply_unified_query( + result, + entity=entity, + db=info.context.db, + filters=filters, + sorts=sorts, + search=search, + pagination=pagination, + for_count=False, + ) + + db = info.context.db + query_result = await db.execute(stmt) + return query_result.scalars().all() + + return wrapper + + return decorator + + +async def count_unified_query( + stmt: Select[Any], + *, + entity: str, + db: Any, + filters: list[QueryFilterClauseInput] | None, + sorts: list[QuerySortClauseInput] | None, + search: QuerySearchInput | None, +) -> int: + stmt = await apply_unified_query( + stmt, + entity=entity, + db=db, + filters=filters, + sorts=sorts, + search=search, + pagination=None, + for_count=True, + ) + subquery = stmt.subquery() + count_query = select(func.count(func.distinct(subquery.c.id))) + result = await db.execute(count_query) + return result.scalar() or 0 + + +def is_unset(value: Any) -> bool: + return value is strawberry.UNSET or value is None diff --git a/backend/api/query/field_ops.py b/backend/api/query/field_ops.py new file mode 100644 index 0000000..3c7c32b --- /dev/null +++ b/backend/api/query/field_ops.py @@ -0,0 +1,218 @@ +from datetime import date, datetime +from typing import Any + +from sqlalchemy import String, and_, cast, func, not_, or_ +from sqlalchemy.sql import ColumnElement + +from api.query.enums import QueryOperator +from api.query.inputs import QueryFilterValueInput + + +def _str_norm(v: QueryFilterValueInput) -> str | None: + if v.string_value is not None: + return v.string_value + return None + + +def apply_ops_to_column( + column: Any, + operator: QueryOperator, + value: QueryFilterValueInput | None, + *, + as_date: bool = False, + as_datetime: bool = False, +) -> ColumnElement[bool] | None: + if operator in (QueryOperator.IS_NULL, QueryOperator.IS_NOT_NULL): + if operator == QueryOperator.IS_NULL: + return column.is_(None) + return column.isnot(None) + + if value is None and operator not in ( + QueryOperator.IS_EMPTY, + QueryOperator.IS_NOT_EMPTY, + ): + return None + + if operator == QueryOperator.IS_EMPTY: + return or_(column.is_(None), cast(column, String) == "") + if operator == QueryOperator.IS_NOT_EMPTY: + return and_(column.isnot(None), cast(column, String) != "") + + if as_date: + return _apply_date_ops(column, operator, value) + if as_datetime: + return _apply_datetime_ops(column, operator, value) + + if operator == QueryOperator.EQ: + if value is None: + return None + if value.uuid_value is not None: + return column == value.uuid_value + if value.string_value is not None: + return column == value.string_value + if value.float_value is not None: + return column == value.float_value + if value.int_value is not None: + return column == value.int_value + if value.bool_value is not None: + return column == value.bool_value + if value.date_value is not None: + return func.date(column) == value.date_value + if value.date_time_value is not None: + return column == value.date_time_value + return None + + if operator == QueryOperator.NEQ: + if value is None: + return None + if value.uuid_value is not None: + return column != value.uuid_value + if value.string_value is not None: + return column != value.string_value + if value.float_value is not None: + return column != value.float_value + if value.bool_value is not None: + return column != value.bool_value + return None + + if operator == QueryOperator.GT: + return _cmp(column, value, lambda c, x: c > x) + if operator == QueryOperator.GTE: + return _cmp(column, value, lambda c, x: c >= x) + if operator == QueryOperator.LT: + return _cmp(column, value, lambda c, x: c < x) + if operator == QueryOperator.LTE: + return _cmp(column, value, lambda c, x: c <= x) + + if operator == QueryOperator.BETWEEN: + if value is None: + return None + if value.date_min is not None and value.date_max is not None: + return func.date(column).between(value.date_min, value.date_max) + if value.float_min is not None and value.float_max is not None: + return column.between(value.float_min, value.float_max) + return None + + if operator == QueryOperator.IN: + if value and value.string_values: + return column.in_(value.string_values) + if value and value.uuid_values: + return column.in_(value.uuid_values) + return None + + if operator == QueryOperator.NOT_IN: + if value and value.string_values: + return column.notin_(value.string_values) + if value and value.uuid_values: + return column.notin_(value.uuid_values) + return None + + if operator == QueryOperator.CONTAINS: + s = _str_norm(value) if value else None + if s is None: + return None + return column.ilike(f"%{s}%") + if operator == QueryOperator.STARTS_WITH: + s = _str_norm(value) if value else None + if s is None: + return None + return column.ilike(f"{s}%") + if operator == QueryOperator.ENDS_WITH: + s = _str_norm(value) if value else None + if s is None: + return None + return column.ilike(f"%{s}") + + if operator == QueryOperator.ANY_EQ: + if value and value.string_values: + return or_(*[column == t for t in value.string_values]) + return None + + if operator in (QueryOperator.ANY_IN, QueryOperator.ALL_IN, QueryOperator.NONE_IN): + return _apply_multi_select_ops(column, operator, value) + + return None + + +def _cmp(column: Any, value: QueryFilterValueInput | None, pred) -> ColumnElement[bool] | None: + if value is None: + return None + if value.float_value is not None: + return pred(column, value.float_value) + if value.int_value is not None: + return pred(column, value.int_value) + if value.date_value is not None: + return pred(func.date(column), value.date_value) + if value.date_time_value is not None: + return pred(column, value.date_time_value) + return None + + +def _apply_date_ops( + column: Any, operator: QueryOperator, value: QueryFilterValueInput | None +) -> ColumnElement[bool] | None: + if value is None: + return None + dc = func.date(column) + if operator == QueryOperator.EQ and value.date_value is not None: + return dc == value.date_value + if operator == QueryOperator.NEQ and value.date_value is not None: + return dc != value.date_value + if operator == QueryOperator.GT and value.date_value is not None: + return dc > value.date_value + if operator == QueryOperator.GTE and value.date_value is not None: + return dc >= value.date_value + if operator == QueryOperator.LT and value.date_value is not None: + return dc < value.date_value + if operator == QueryOperator.LTE and value.date_value is not None: + return dc <= value.date_value + if ( + operator == QueryOperator.BETWEEN + and value.date_min is not None + and value.date_max is not None + ): + return dc.between(value.date_min, value.date_max) + if operator == QueryOperator.IN and value.string_values: + return dc.in_(value.string_values) + return None + + +def _apply_datetime_ops( + column: Any, operator: QueryOperator, value: QueryFilterValueInput | None +) -> ColumnElement[bool] | None: + if value is None: + return None + if operator == QueryOperator.EQ and value.date_time_value is not None: + return column == value.date_time_value + if operator == QueryOperator.NEQ and value.date_time_value is not None: + return column != value.date_time_value + if operator == QueryOperator.GT and value.date_time_value is not None: + return column > value.date_time_value + if operator == QueryOperator.GTE and value.date_time_value is not None: + return column >= value.date_time_value + if operator == QueryOperator.LT and value.date_time_value is not None: + return column < value.date_time_value + if operator == QueryOperator.LTE and value.date_time_value is not None: + return column <= value.date_time_value + if ( + operator == QueryOperator.BETWEEN + and value.date_min is not None + and value.date_max is not None + ): + return func.date(column).between(value.date_min, value.date_max) + return None + + +def _apply_multi_select_ops( + column: Any, operator: QueryOperator, value: QueryFilterValueInput | None +) -> ColumnElement[bool] | None: + if value is None or not value.string_values: + return None + tags = value.string_values + if operator == QueryOperator.ANY_IN: + return or_(*[column.contains(tag) for tag in tags]) + if operator == QueryOperator.ALL_IN: + return and_(*[column.contains(tag) for tag in tags]) + if operator == QueryOperator.NONE_IN: + return and_(*[not_(column.contains(tag)) for tag in tags]) + return None diff --git a/backend/api/query/graphql_types.py b/backend/api/query/graphql_types.py new file mode 100644 index 0000000..737766b --- /dev/null +++ b/backend/api/query/graphql_types.py @@ -0,0 +1,36 @@ +import strawberry + +from api.query.enums import ( + QueryOperator, + QueryableFieldKind, + QueryableValueType, + ReferenceFilterMode, +) + + +@strawberry.type +class QueryableRelationMeta: + target_entity: str + id_field_key: str + label_field_key: str + allowed_filter_modes: list[ReferenceFilterMode] + + +@strawberry.type +class QueryableChoiceMeta: + option_keys: list[str] + option_labels: list[str] + + +@strawberry.type +class QueryableField: + key: str + label: str + kind: QueryableFieldKind + value_type: QueryableValueType + allowed_operators: list[QueryOperator] + sortable: bool + searchable: bool + relation: QueryableRelationMeta | None = None + choice: QueryableChoiceMeta | None = None + property_definition_id: str | None = None diff --git a/backend/api/query/inputs.py b/backend/api/query/inputs.py new file mode 100644 index 0000000..2a0ad05 --- /dev/null +++ b/backend/api/query/inputs.py @@ -0,0 +1,42 @@ +from datetime import date, datetime + +import strawberry + +from api.inputs import SortDirection +from api.query.enums import QueryOperator + + +@strawberry.input +class QueryFilterValueInput: + string_value: str | None = None + string_values: list[str] | None = None + float_value: float | None = None + float_min: float | None = None + float_max: float | None = None + int_value: int | None = None + bool_value: bool | None = None + date_value: date | None = None + date_min: date | None = None + date_max: date | None = None + date_time_value: datetime | None = None + uuid_value: str | None = None + uuid_values: list[str] | None = None + + +@strawberry.input +class QueryFilterClauseInput: + field_key: str + operator: QueryOperator + value: QueryFilterValueInput | None = None + + +@strawberry.input +class QuerySortClauseInput: + field_key: str + direction: SortDirection + + +@strawberry.input +class QuerySearchInput: + search_text: str | None = None + include_properties: bool = False diff --git a/backend/api/query/metadata_service.py b/backend/api/query/metadata_service.py new file mode 100644 index 0000000..1a53d01 --- /dev/null +++ b/backend/api/query/metadata_service.py @@ -0,0 +1,265 @@ +from typing import Any + +from sqlalchemy import select + +from api.query.adapters import patient as patient_adapters +from api.query.adapters import task as task_adapters +from api.query.adapters import user as user_adapters +from api.query.enums import ( + QueryOperator, + QueryableFieldKind, + QueryableValueType, + ReferenceFilterMode, +) +from api.query.graphql_types import QueryableChoiceMeta, QueryableField, QueryableRelationMeta +from api.query.registry import PATIENT, TASK, USER +from database import models + + +def _str_ops() -> list[QueryOperator]: + return [ + QueryOperator.EQ, + QueryOperator.NEQ, + QueryOperator.CONTAINS, + QueryOperator.STARTS_WITH, + QueryOperator.ENDS_WITH, + QueryOperator.IN, + QueryOperator.NOT_IN, + QueryOperator.IS_NULL, + QueryOperator.IS_NOT_NULL, + ] + + +def _num_ops() -> list[QueryOperator]: + return [ + QueryOperator.EQ, + QueryOperator.NEQ, + QueryOperator.GT, + QueryOperator.GTE, + QueryOperator.LT, + QueryOperator.LTE, + QueryOperator.BETWEEN, + QueryOperator.IS_NULL, + QueryOperator.IS_NOT_NULL, + ] + + +def _date_ops() -> list[QueryOperator]: + return [ + QueryOperator.EQ, + QueryOperator.NEQ, + QueryOperator.GT, + QueryOperator.GTE, + QueryOperator.LT, + QueryOperator.LTE, + QueryOperator.BETWEEN, + QueryOperator.IS_NULL, + QueryOperator.IS_NOT_NULL, + ] + + +def _dt_ops() -> list[QueryOperator]: + return _date_ops() + + +def _bool_ops() -> list[QueryOperator]: + return [ + QueryOperator.EQ, + QueryOperator.IS_NULL, + QueryOperator.IS_NOT_NULL, + ] + + +def _choice_ops() -> list[QueryOperator]: + return [ + QueryOperator.EQ, + QueryOperator.NEQ, + QueryOperator.IN, + QueryOperator.NOT_IN, + QueryOperator.IS_NULL, + QueryOperator.IS_NOT_NULL, + ] + + +def _multi_choice_ops() -> list[QueryOperator]: + return [ + QueryOperator.ANY_IN, + QueryOperator.ALL_IN, + QueryOperator.NONE_IN, + QueryOperator.IS_EMPTY, + QueryOperator.IS_NOT_EMPTY, + QueryOperator.IS_NULL, + QueryOperator.IS_NOT_NULL, + ] + + +def _user_ref_ops() -> list[QueryOperator]: + return [ + QueryOperator.EQ, + QueryOperator.IN, + QueryOperator.IS_NULL, + QueryOperator.IS_NOT_NULL, + ] + + +def _property_definition_to_field(p: models.PropertyDefinition) -> QueryableField: + ft = p.field_type + key = f"property_{p.id}" + name = p.name + raw_opts = (p.options or "").strip() + option_labels = [x.strip() for x in raw_opts.split(",") if x.strip()] if raw_opts else [] + option_keys = list(option_labels) + + if ft == "FIELD_TYPE_TEXT": + return QueryableField( + key=key, + label=name, + kind=QueryableFieldKind.PROPERTY, + value_type=QueryableValueType.STRING, + allowed_operators=_str_ops(), + sortable=True, + searchable=True, + property_definition_id=str(p.id), + ) + if ft == "FIELD_TYPE_NUMBER": + return QueryableField( + key=key, + label=name, + kind=QueryableFieldKind.PROPERTY, + value_type=QueryableValueType.NUMBER, + allowed_operators=_num_ops(), + sortable=True, + searchable=False, + property_definition_id=str(p.id), + ) + if ft == "FIELD_TYPE_CHECKBOX": + return QueryableField( + key=key, + label=name, + kind=QueryableFieldKind.PROPERTY, + value_type=QueryableValueType.BOOLEAN, + allowed_operators=_bool_ops(), + sortable=True, + searchable=False, + property_definition_id=str(p.id), + ) + if ft == "FIELD_TYPE_DATE": + return QueryableField( + key=key, + label=name, + kind=QueryableFieldKind.PROPERTY, + value_type=QueryableValueType.DATE, + allowed_operators=_date_ops(), + sortable=True, + searchable=False, + property_definition_id=str(p.id), + ) + if ft == "FIELD_TYPE_DATE_TIME": + return QueryableField( + key=key, + label=name, + kind=QueryableFieldKind.PROPERTY, + value_type=QueryableValueType.DATETIME, + allowed_operators=_dt_ops(), + sortable=True, + searchable=False, + property_definition_id=str(p.id), + ) + if ft == "FIELD_TYPE_SELECT": + return QueryableField( + key=key, + label=name, + kind=QueryableFieldKind.CHOICE, + value_type=QueryableValueType.STRING, + allowed_operators=_choice_ops(), + sortable=True, + searchable=False, + property_definition_id=str(p.id), + choice=QueryableChoiceMeta( + option_keys=option_keys, + option_labels=option_labels, + ), + ) + if ft == "FIELD_TYPE_MULTI_SELECT": + return QueryableField( + key=key, + label=name, + kind=QueryableFieldKind.CHOICE_LIST, + value_type=QueryableValueType.STRING_LIST, + allowed_operators=_multi_choice_ops(), + sortable=False, + searchable=False, + property_definition_id=str(p.id), + choice=QueryableChoiceMeta( + option_keys=option_keys, + option_labels=option_labels, + ), + ) + if ft == "FIELD_TYPE_USER": + return QueryableField( + key=key, + label=name, + kind=QueryableFieldKind.REFERENCE, + value_type=QueryableValueType.UUID, + allowed_operators=_user_ref_ops(), + sortable=True, + searchable=False, + property_definition_id=str(p.id), + relation=QueryableRelationMeta( + target_entity="User", + id_field_key="id", + label_field_key="name", + allowed_filter_modes=[ReferenceFilterMode.ID], + ), + ) + return QueryableField( + key=key, + label=name, + kind=QueryableFieldKind.PROPERTY, + value_type=QueryableValueType.STRING, + allowed_operators=_str_ops(), + sortable=True, + searchable=True, + property_definition_id=str(p.id), + ) + + +async def load_queryable_fields( + db: Any, entity: str +) -> list[QueryableField]: + e = entity.strip() + if e == TASK: + base = task_adapters.build_task_queryable_fields_static() + prop_rows = ( + await db.execute( + select(models.PropertyDefinition).where( + models.PropertyDefinition.is_active.is_(True), + ) + ) + ).scalars().all() + extra = [] + for p in prop_rows: + ents = (p.allowed_entities or "").split(",") + if "TASK" not in [x.strip() for x in ents if x.strip()]: + continue + extra.append(_property_definition_to_field(p)) + return base + extra + if e == PATIENT: + base = patient_adapters.build_patient_queryable_fields_static() + prop_rows = ( + await db.execute( + select(models.PropertyDefinition).where( + models.PropertyDefinition.is_active.is_(True), + ) + ) + ).scalars().all() + extra = [] + for p in prop_rows: + ents = (p.allowed_entities or "").split(",") + if "PATIENT" not in [x.strip() for x in ents if x.strip()]: + continue + extra.append(_property_definition_to_field(p)) + return base + extra + if e == USER: + return user_adapters.build_user_queryable_fields_static() + return [] diff --git a/backend/api/query/property_sql.py b/backend/api/query/property_sql.py new file mode 100644 index 0000000..0be5e22 --- /dev/null +++ b/backend/api/query/property_sql.py @@ -0,0 +1,61 @@ +from typing import Any + +from sqlalchemy import Select, and_, select +from sqlalchemy.orm import aliased + +from database import models + + +def property_value_column_for_field_type(field_type: str) -> str: + mapping = { + "FIELD_TYPE_TEXT": "text_value", + "FIELD_TYPE_NUMBER": "number_value", + "FIELD_TYPE_CHECKBOX": "boolean_value", + "FIELD_TYPE_DATE": "date_value", + "FIELD_TYPE_DATE_TIME": "date_time_value", + "FIELD_TYPE_SELECT": "select_value", + "FIELD_TYPE_MULTI_SELECT": "multi_select_values", + "FIELD_TYPE_USER": "user_value", + } + return mapping.get(field_type, "text_value") + + +def join_property_value( + query: Select[Any], + root_model: type, + property_definition_id: str, + field_type: str, + entity: str, +) -> tuple[Select[Any], Any, Any]: + property_alias = aliased(models.PropertyValue) + value_column = getattr( + property_alias, property_value_column_for_field_type(field_type) + ) + + if entity == "patient": + join_condition = and_( + property_alias.patient_id == root_model.id, + property_alias.definition_id == property_definition_id, + ) + else: + join_condition = and_( + property_alias.task_id == root_model.id, + property_alias.definition_id == property_definition_id, + ) + + query = query.outerjoin(property_alias, join_condition) + return query, property_alias, value_column + + +async def load_property_field_types( + db: Any, definition_ids: set[str] +) -> dict[str, str]: + if not definition_ids: + return {} + result = await db.execute( + select(models.PropertyDefinition).where( + models.PropertyDefinition.id.in_(definition_ids) + ) + ) + rows = result.scalars().all() + return {str(p.id): p.field_type for p in rows} diff --git a/backend/api/query/registry.py b/backend/api/query/registry.py new file mode 100644 index 0000000..0108bb9 --- /dev/null +++ b/backend/api/query/registry.py @@ -0,0 +1,38 @@ +from typing import Any + +from api.query.adapters import patient as patient_adapters +from api.query.adapters import task as task_adapters +from api.query.adapters import user as user_adapters +from database import models + +EntityHandler = dict[str, Any] + +TASK = "Task" +PATIENT = "Patient" +USER = "User" + + +ENTITY_REGISTRY: dict[str, EntityHandler] = { + TASK: { + "root_model": models.Task, + "apply_filter": task_adapters.apply_task_filter_clause, + "apply_sorts": task_adapters.apply_task_sorts, + "apply_search": task_adapters.apply_task_search, + }, + PATIENT: { + "root_model": models.Patient, + "apply_filter": patient_adapters.apply_patient_filter_clause, + "apply_sorts": patient_adapters.apply_patient_sorts, + "apply_search": patient_adapters.apply_patient_search, + }, + USER: { + "root_model": models.User, + "apply_filter": user_adapters.apply_user_filter_clause, + "apply_sorts": user_adapters.apply_user_sorts, + "apply_search": user_adapters.apply_user_search, + }, +} + + +def get_entity_handler(entity: str) -> EntityHandler | None: + return ENTITY_REGISTRY.get(entity) diff --git a/backend/api/query/sql_expr.py b/backend/api/query/sql_expr.py new file mode 100644 index 0000000..9c0bf5f --- /dev/null +++ b/backend/api/query/sql_expr.py @@ -0,0 +1,33 @@ +from typing import Any + +from sqlalchemy import String, and_, case, cast, func + + +def user_display_label_expr(user_table: Any) -> Any: + return cast( + func.coalesce( + case( + ( + and_( + user_table.firstname.isnot(None), + user_table.lastname.isnot(None), + ), + user_table.firstname + " " + user_table.lastname, + ), + else_=None, + ), + user_table.username, + ), + String, + ) + + +def patient_display_name_expr(patient_table: Any) -> Any: + return cast( + func.trim(patient_table.firstname + " " + patient_table.lastname), + String, + ) + + +def location_title_expr(location_table: Any) -> Any: + return cast(location_table.title, String) diff --git a/backend/api/resolvers/__init__.py b/backend/api/resolvers/__init__.py index b053198..613428e 100644 --- a/backend/api/resolvers/__init__.py +++ b/backend/api/resolvers/__init__.py @@ -4,6 +4,7 @@ from .location import LocationMutation, LocationQuery, LocationSubscription from .patient import PatientMutation, PatientQuery, PatientSubscription from .property import PropertyDefinitionMutation, PropertyDefinitionQuery +from .query_metadata import QueryMetadataQuery from .task import TaskMutation, TaskQuery, TaskSubscription from .user import UserMutation, UserQuery @@ -16,6 +17,7 @@ class Query( PropertyDefinitionQuery, UserQuery, AuditQuery, + QueryMetadataQuery, ): pass diff --git a/backend/api/resolvers/patient.py b/backend/api/resolvers/patient.py index 946e77b..246307a 100644 --- a/backend/api/resolvers/patient.py +++ b/backend/api/resolvers/patient.py @@ -3,23 +3,15 @@ import strawberry from api.audit import audit_log from api.context import Info -from api.decorators.filter_sort import ( - apply_filtering, - apply_sorting, - filtered_and_sorted_query, - get_property_field_types, -) -from api.decorators.full_text_search import ( - apply_full_text_search, - full_text_search_query, -) -from api.inputs import ( - FilterInput, - FullTextSearchInput, - PaginationInput, - SortInput, -) from api.inputs import CreatePatientInput, PatientState, UpdatePatientInput +from api.inputs import PaginationInput +from api.query.execute import count_unified_query, is_unset, unified_list_query +from api.query.inputs import ( + QueryFilterClauseInput, + QuerySearchInput, + QuerySortClauseInput, +) +from api.query.registry import PATIENT from api.resolvers.base import BaseMutationResolver, BaseSubscriptionResolver from api.services.authorization import AuthorizationService from api.services.checksum import validate_checksum @@ -170,18 +162,17 @@ async def patient( return patient @strawberry.field - @filtered_and_sorted_query() - @full_text_search_query() + @unified_list_query(PATIENT) async def patients( self, info: Info, location_node_id: strawberry.ID | None = None, root_location_ids: list[strawberry.ID] | None = None, states: list[PatientState] | None = None, - filtering: list[FilterInput] | None = None, - sorting: list[SortInput] | None = None, + filters: list[QueryFilterClauseInput] | None = None, + sorts: list[QuerySortClauseInput] | None = None, pagination: PaginationInput | None = None, - search: FullTextSearchInput | None = None, + search: QuerySearchInput | None = None, ) -> list[PatientType]: query, _ = await PatientQuery._build_patients_base_query( info, location_node_id, root_location_ids, states @@ -195,45 +186,33 @@ async def patientsTotal( location_node_id: strawberry.ID | None = None, root_location_ids: list[strawberry.ID] | None = None, states: list[PatientState] | None = None, - filtering: list[FilterInput] | None = None, - sorting: list[SortInput] | None = None, - search: FullTextSearchInput | None = None, + filters: list[QueryFilterClauseInput] | None = None, + sorts: list[QuerySortClauseInput] | None = None, + search: QuerySearchInput | None = None, ) -> int: query, _ = await PatientQuery._build_patients_base_query( info, location_node_id, root_location_ids, states ) - if search and search is not strawberry.UNSET: - query = apply_full_text_search(query, search, models.Patient) - - property_field_types = await get_property_field_types( - info.context.db, filtering, sorting + return await count_unified_query( + query, + entity=PATIENT, + db=info.context.db, + filters=filters if filters is not None and not is_unset(filters) else None, + sorts=sorts if sorts is not None and not is_unset(sorts) else None, + search=search if search is not None and not is_unset(search) else None, ) - if filtering: - query = apply_filtering( - query, filtering, models.Patient, property_field_types - ) - if sorting: - query = apply_sorting( - query, sorting, models.Patient, property_field_types - ) - - subquery = query.subquery() - count_query = select(func.count(func.distinct(subquery.c.id))) - result = await info.context.db.execute(count_query) - return result.scalar() or 0 @strawberry.field - @filtered_and_sorted_query() - @full_text_search_query() + @unified_list_query(PATIENT) async def recent_patients( self, info: Info, root_location_ids: list[strawberry.ID] | None = None, - filtering: list[FilterInput] | None = None, - sorting: list[SortInput] | None = None, + filters: list[QueryFilterClauseInput] | None = None, + sorts: list[QuerySortClauseInput] | None = None, pagination: PaginationInput | None = None, - search: FullTextSearchInput | None = None, + search: QuerySearchInput | None = None, ) -> list[PatientType]: auth_service = AuthorizationService(info.context.db) accessible_location_ids = ( @@ -317,9 +296,9 @@ async def recentPatientsTotal( self, info: Info, root_location_ids: list[strawberry.ID] | None = None, - filtering: list[FilterInput] | None = None, - sorting: list[SortInput] | None = None, - search: FullTextSearchInput | None = None, + filters: list[QueryFilterClauseInput] | None = None, + sorts: list[QuerySortClauseInput] | None = None, + search: QuerySearchInput | None = None, ) -> int: auth_service = AuthorizationService(info.context.db) accessible_location_ids = ( @@ -391,25 +370,14 @@ async def recentPatientsTotal( .distinct() ) - if search and search is not strawberry.UNSET: - query = apply_full_text_search(query, search, models.Patient) - - property_field_types = await get_property_field_types( - info.context.db, filtering, sorting + return await count_unified_query( + query, + entity=PATIENT, + db=info.context.db, + filters=filters if filters is not None and not is_unset(filters) else None, + sorts=sorts if sorts is not None and not is_unset(sorts) else None, + search=search if search is not None and not is_unset(search) else None, ) - if filtering: - query = apply_filtering( - query, filtering, models.Patient, property_field_types - ) - if sorting: - query = apply_sorting( - query, sorting, models.Patient, property_field_types - ) - - subquery = query.subquery() - count_query = select(func.count(func.distinct(subquery.c.id))) - result = await info.context.db.execute(count_query) - return result.scalar() or 0 @strawberry.type diff --git a/backend/api/resolvers/query_metadata.py b/backend/api/resolvers/query_metadata.py new file mode 100644 index 0000000..0f825cf --- /dev/null +++ b/backend/api/resolvers/query_metadata.py @@ -0,0 +1,14 @@ +import strawberry + +from api.context import Info +from api.query.graphql_types import QueryableField +from api.query.metadata_service import load_queryable_fields + + +@strawberry.type +class QueryMetadataQuery: + @strawberry.field + async def queryable_fields( + self, info: Info, entity: str + ) -> list[QueryableField]: + return await load_queryable_fields(info.context.db, entity) diff --git a/backend/api/resolvers/task.py b/backend/api/resolvers/task.py index cc41c29..66ace14 100644 --- a/backend/api/resolvers/task.py +++ b/backend/api/resolvers/task.py @@ -3,26 +3,15 @@ import strawberry from api.audit import audit_log from api.context import Info -from api.decorators.filter_sort import ( - apply_filtering, - apply_sorting, - filtered_and_sorted_query, - get_property_field_types, -) -from api.decorators.full_text_search import ( - apply_full_text_search, - full_text_search_query, -) from api.errors import raise_forbidden -from api.inputs import ( - CreateTaskInput, - FilterInput, - FullTextSearchInput, - PaginationInput, - PatientState, - SortInput, - UpdateTaskInput, +from api.inputs import CreateTaskInput, PaginationInput, PatientState, SortDirection, UpdateTaskInput +from api.query.execute import count_unified_query, is_unset, unified_list_query +from api.query.inputs import ( + QueryFilterClauseInput, + QuerySearchInput, + QuerySortClauseInput, ) +from api.query.registry import TASK from api.resolvers.base import BaseMutationResolver, BaseSubscriptionResolver from api.services.authorization import AuthorizationService from api.services.checksum import validate_checksum @@ -31,7 +20,7 @@ from api.types.task import TaskType from database import models from graphql import GraphQLError -from sqlalchemy import desc, func, select +from sqlalchemy import select from sqlalchemy.orm import aliased, selectinload @@ -60,8 +49,7 @@ async def task(self, info: Info, id: strawberry.ID) -> TaskType | None: return task @strawberry.field - @filtered_and_sorted_query() - @full_text_search_query() + @unified_list_query(TASK) async def tasks( self, info: Info, @@ -69,10 +57,10 @@ async def tasks( assignee_id: strawberry.ID | None = None, assignee_team_id: strawberry.ID | None = None, root_location_ids: list[strawberry.ID] | None = None, - filtering: list[FilterInput] | None = None, - sorting: list[SortInput] | None = None, + filters: list[QueryFilterClauseInput] | None = None, + sorts: list[QuerySortClauseInput] | None = None, pagination: PaginationInput | None = None, - search: FullTextSearchInput | None = None, + search: QuerySearchInput | None = None, ) -> list[TaskType]: auth_service = AuthorizationService(info.context.db) @@ -222,9 +210,9 @@ async def tasksTotal( assignee_id: strawberry.ID | None = None, assignee_team_id: strawberry.ID | None = None, root_location_ids: list[strawberry.ID] | None = None, - filtering: list[FilterInput] | None = None, - sorting: list[SortInput] | None = None, - search: FullTextSearchInput | None = None, + filters: list[QueryFilterClauseInput] | None = None, + sorts: list[QuerySortClauseInput] | None = None, + search: QuerySearchInput | None = None, ) -> int: auth_service = AuthorizationService(info.context.db) @@ -358,45 +346,33 @@ async def tasksTotal( ), ) - if search and search is not strawberry.UNSET: - query = apply_full_text_search(query, search, models.Task) - - property_field_types = await get_property_field_types( - info.context.db, - filtering, - sorting, + return await count_unified_query( + query, + entity=TASK, + db=info.context.db, + filters=filters if filters is not None and not is_unset(filters) else None, + sorts=sorts if sorts is not None and not is_unset(sorts) else None, + search=search if search is not None and not is_unset(search) else None, ) - if filtering: - query = apply_filtering( - query, - filtering, - models.Task, - property_field_types, - ) - if sorting: - query = apply_sorting( - query, - sorting, - models.Task, - property_field_types, - ) - - subquery = query.subquery() - count_query = select(func.count(func.distinct(subquery.c.id))) - result = await info.context.db.execute(count_query) - return result.scalar() or 0 @strawberry.field - @filtered_and_sorted_query() - @full_text_search_query() + @unified_list_query( + TASK, + default_sorts_when_empty=[ + QuerySortClauseInput( + field_key="updateDate", + direction=SortDirection.DESC, + ) + ], + ) async def recent_tasks( self, info: Info, root_location_ids: list[strawberry.ID] | None = None, - filtering: list[FilterInput] | None = None, - sorting: list[SortInput] | None = None, + filters: list[QueryFilterClauseInput] | None = None, + sorts: list[QuerySortClauseInput] | None = None, pagination: PaginationInput | None = None, - search: FullTextSearchInput | None = None, + search: QuerySearchInput | None = None, ) -> list[TaskType]: auth_service = AuthorizationService(info.context.db) accessible_location_ids = ( @@ -489,10 +465,6 @@ async def recent_tasks( .distinct() ) - default_sorting = sorting is None or len(sorting) == 0 - if default_sorting: - query = query.order_by(desc(models.Task.update_date)) - return query @strawberry.field @@ -500,9 +472,9 @@ async def recentTasksTotal( self, info: Info, root_location_ids: list[strawberry.ID] | None = None, - filtering: list[FilterInput] | None = None, - sorting: list[SortInput] | None = None, - search: FullTextSearchInput | None = None, + filters: list[QueryFilterClauseInput] | None = None, + sorts: list[QuerySortClauseInput] | None = None, + search: QuerySearchInput | None = None, ) -> int: auth_service = AuthorizationService(info.context.db) accessible_location_ids = ( @@ -590,33 +562,14 @@ async def recentTasksTotal( .distinct() ) - if search and search is not strawberry.UNSET: - query = apply_full_text_search(query, search, models.Task) - - property_field_types = await get_property_field_types( - info.context.db, - filtering, - sorting, + return await count_unified_query( + query, + entity=TASK, + db=info.context.db, + filters=filters if filters is not None and not is_unset(filters) else None, + sorts=sorts if sorts is not None and not is_unset(sorts) else None, + search=search if search is not None and not is_unset(search) else None, ) - if filtering: - query = apply_filtering( - query, - filtering, - models.Task, - property_field_types, - ) - if sorting: - query = apply_sorting( - query, - sorting, - models.Task, - property_field_types, - ) - - subquery = query.subquery() - count_query = select(func.count(func.distinct(subquery.c.id))) - result = await info.context.db.execute(count_query) - return result.scalar() or 0 @strawberry.type diff --git a/backend/api/resolvers/user.py b/backend/api/resolvers/user.py index e4d9752..4b9ac92 100644 --- a/backend/api/resolvers/user.py +++ b/backend/api/resolvers/user.py @@ -1,14 +1,13 @@ import strawberry from api.context import Info -from api.decorators.filter_sort import filtered_and_sorted_query -from api.decorators.full_text_search import full_text_search_query -from api.inputs import ( - FilterInput, - FullTextSearchInput, - PaginationInput, - SortInput, - UpdateProfilePictureInput, +from api.inputs import PaginationInput, UpdateProfilePictureInput +from api.query.execute import unified_list_query +from api.query.inputs import ( + QueryFilterClauseInput, + QuerySearchInput, + QuerySortClauseInput, ) +from api.query.registry import USER from api.resolvers.base import BaseMutationResolver from api.types.user import UserType from database import models @@ -26,15 +25,14 @@ async def user(self, info: Info, id: strawberry.ID) -> UserType | None: return result.scalars().first() @strawberry.field - @filtered_and_sorted_query() - @full_text_search_query() + @unified_list_query(USER) async def users( self, info: Info, - filtering: list[FilterInput] | None = None, - sorting: list[SortInput] | None = None, + filters: list[QueryFilterClauseInput] | None = None, + sorts: list[QuerySortClauseInput] | None = None, pagination: PaginationInput | None = None, - search: FullTextSearchInput | None = None, + search: QuerySearchInput | None = None, ) -> list[UserType]: query = select(models.User) return query diff --git a/web/api/gql/generated.ts b/web/api/gql/generated.ts index c614ec5..71ebda2 100644 --- a/web/api/gql/generated.ts +++ b/web/api/gql/generated.ts @@ -13,7 +13,9 @@ export type Scalars = { Boolean: { input: boolean; output: boolean; } Int: { input: number; output: number; } Float: { input: number; output: number; } + /** Date (isoformat) */ Date: { input: any; output: any; } + /** Date with time (isoformat) */ DateTime: { input: any; output: any; } }; @@ -26,11 +28,6 @@ export type AuditLogType = { userId?: Maybe; }; -export enum ColumnType { - DirectAttribute = 'DIRECT_ATTRIBUTE', - Property = 'PROPERTY' -} - export type CreateLocationNodeInput = { kind: LocationType; parentId?: InputMaybe; @@ -86,83 +83,6 @@ export enum FieldType { FieldTypeUser = 'FIELD_TYPE_USER' } -export type FilterInput = { - column: Scalars['String']['input']; - columnType?: ColumnType; - operator: FilterOperator; - parameter: FilterParameter; - propertyDefinitionId?: InputMaybe; -}; - -export enum FilterOperator { - BooleanIsFalse = 'BOOLEAN_IS_FALSE', - BooleanIsTrue = 'BOOLEAN_IS_TRUE', - DatetimeBetween = 'DATETIME_BETWEEN', - DatetimeEquals = 'DATETIME_EQUALS', - DatetimeGreaterThan = 'DATETIME_GREATER_THAN', - DatetimeGreaterThanOrEqual = 'DATETIME_GREATER_THAN_OR_EQUAL', - DatetimeLessThan = 'DATETIME_LESS_THAN', - DatetimeLessThanOrEqual = 'DATETIME_LESS_THAN_OR_EQUAL', - DatetimeNotBetween = 'DATETIME_NOT_BETWEEN', - DatetimeNotEquals = 'DATETIME_NOT_EQUALS', - DateBetween = 'DATE_BETWEEN', - DateEquals = 'DATE_EQUALS', - DateGreaterThan = 'DATE_GREATER_THAN', - DateGreaterThanOrEqual = 'DATE_GREATER_THAN_OR_EQUAL', - DateLessThan = 'DATE_LESS_THAN', - DateLessThanOrEqual = 'DATE_LESS_THAN_OR_EQUAL', - DateNotBetween = 'DATE_NOT_BETWEEN', - DateNotEquals = 'DATE_NOT_EQUALS', - IsNotNull = 'IS_NOT_NULL', - IsNull = 'IS_NULL', - NumberBetween = 'NUMBER_BETWEEN', - NumberEquals = 'NUMBER_EQUALS', - NumberGreaterThan = 'NUMBER_GREATER_THAN', - NumberGreaterThanOrEqual = 'NUMBER_GREATER_THAN_OR_EQUAL', - NumberLessThan = 'NUMBER_LESS_THAN', - NumberLessThanOrEqual = 'NUMBER_LESS_THAN_OR_EQUAL', - NumberNotBetween = 'NUMBER_NOT_BETWEEN', - NumberNotEquals = 'NUMBER_NOT_EQUALS', - TagsContains = 'TAGS_CONTAINS', - TagsEquals = 'TAGS_EQUALS', - TagsNotContains = 'TAGS_NOT_CONTAINS', - TagsNotEquals = 'TAGS_NOT_EQUALS', - TagsSingleContains = 'TAGS_SINGLE_CONTAINS', - TagsSingleEquals = 'TAGS_SINGLE_EQUALS', - TagsSingleNotContains = 'TAGS_SINGLE_NOT_CONTAINS', - TagsSingleNotEquals = 'TAGS_SINGLE_NOT_EQUALS', - TextContains = 'TEXT_CONTAINS', - TextEndsWith = 'TEXT_ENDS_WITH', - TextEquals = 'TEXT_EQUALS', - TextNotContains = 'TEXT_NOT_CONTAINS', - TextNotEquals = 'TEXT_NOT_EQUALS', - TextNotWhitespace = 'TEXT_NOT_WHITESPACE', - TextStartsWith = 'TEXT_STARTS_WITH' -} - -export type FilterParameter = { - compareDate?: InputMaybe; - compareDateTime?: InputMaybe; - compareValue?: InputMaybe; - isCaseSensitive?: Scalars['Boolean']['input']; - max?: InputMaybe; - maxDate?: InputMaybe; - maxDateTime?: InputMaybe; - min?: InputMaybe; - minDate?: InputMaybe; - minDateTime?: InputMaybe; - propertyDefinitionId?: InputMaybe; - searchTags?: InputMaybe>; - searchText?: InputMaybe; -}; - -export type FullTextSearchInput = { - includeProperties?: Scalars['Boolean']['input']; - propertyDefinitionIds?: InputMaybe>; - searchColumns?: InputMaybe>; - searchText: Scalars['String']['input']; -}; - export type LocationNodeType = { __typename?: 'LocationNodeType'; children: Array; @@ -430,6 +350,7 @@ export type Query = { patients: Array; patientsTotal: Scalars['Int']['output']; propertyDefinitions: Array; + queryableFields: Array; recentPatients: Array; recentPatientsTotal: Scalars['Int']['output']; recentTasks: Array; @@ -471,53 +392,62 @@ export type QueryPatientArgs = { export type QueryPatientsArgs = { - filtering?: InputMaybe>; + filters?: InputMaybe>; locationNodeId?: InputMaybe; pagination?: InputMaybe; rootLocationIds?: InputMaybe>; - search?: InputMaybe; - sorting?: InputMaybe>; + search?: InputMaybe; + sorts?: InputMaybe>; states?: InputMaybe>; }; export type QueryPatientsTotalArgs = { - filtering?: InputMaybe>; + filters?: InputMaybe>; locationNodeId?: InputMaybe; rootLocationIds?: InputMaybe>; - search?: InputMaybe; - sorting?: InputMaybe>; + search?: InputMaybe; + sorts?: InputMaybe>; states?: InputMaybe>; }; +export type QueryQueryableFieldsArgs = { + entity: Scalars['String']['input']; +}; + + export type QueryRecentPatientsArgs = { - filtering?: InputMaybe>; + filters?: InputMaybe>; pagination?: InputMaybe; - search?: InputMaybe; - sorting?: InputMaybe>; + rootLocationIds?: InputMaybe>; + search?: InputMaybe; + sorts?: InputMaybe>; }; export type QueryRecentPatientsTotalArgs = { - filtering?: InputMaybe>; - search?: InputMaybe; - sorting?: InputMaybe>; + filters?: InputMaybe>; + rootLocationIds?: InputMaybe>; + search?: InputMaybe; + sorts?: InputMaybe>; }; export type QueryRecentTasksArgs = { - filtering?: InputMaybe>; + filters?: InputMaybe>; pagination?: InputMaybe; - search?: InputMaybe; - sorting?: InputMaybe>; + rootLocationIds?: InputMaybe>; + search?: InputMaybe; + sorts?: InputMaybe>; }; export type QueryRecentTasksTotalArgs = { - filtering?: InputMaybe>; - search?: InputMaybe; - sorting?: InputMaybe>; + filters?: InputMaybe>; + rootLocationIds?: InputMaybe>; + search?: InputMaybe; + sorts?: InputMaybe>; }; @@ -529,23 +459,23 @@ export type QueryTaskArgs = { export type QueryTasksArgs = { assigneeId?: InputMaybe; assigneeTeamId?: InputMaybe; - filtering?: InputMaybe>; + filters?: InputMaybe>; pagination?: InputMaybe; patientId?: InputMaybe; rootLocationIds?: InputMaybe>; - search?: InputMaybe; - sorting?: InputMaybe>; + search?: InputMaybe; + sorts?: InputMaybe>; }; export type QueryTasksTotalArgs = { assigneeId?: InputMaybe; assigneeTeamId?: InputMaybe; - filtering?: InputMaybe>; + filters?: InputMaybe>; patientId?: InputMaybe; rootLocationIds?: InputMaybe>; - search?: InputMaybe; - sorting?: InputMaybe>; + search?: InputMaybe; + sorts?: InputMaybe>; }; @@ -555,12 +485,120 @@ export type QueryUserArgs = { export type QueryUsersArgs = { - filtering?: InputMaybe>; + filters?: InputMaybe>; pagination?: InputMaybe; - search?: InputMaybe; - sorting?: InputMaybe>; + search?: InputMaybe; + sorts?: InputMaybe>; +}; + +export type QueryFilterClauseInput = { + fieldKey: Scalars['String']['input']; + operator: QueryOperator; + value?: InputMaybe; }; +export type QueryFilterValueInput = { + boolValue?: InputMaybe; + dateMax?: InputMaybe; + dateMin?: InputMaybe; + dateTimeValue?: InputMaybe; + dateValue?: InputMaybe; + floatMax?: InputMaybe; + floatMin?: InputMaybe; + floatValue?: InputMaybe; + intValue?: InputMaybe; + stringValue?: InputMaybe; + stringValues?: InputMaybe>; + uuidValue?: InputMaybe; + uuidValues?: InputMaybe>; +}; + +export enum QueryOperator { + AllIn = 'ALL_IN', + AnyEq = 'ANY_EQ', + AnyIn = 'ANY_IN', + Between = 'BETWEEN', + Contains = 'CONTAINS', + EndsWith = 'ENDS_WITH', + Eq = 'EQ', + Gt = 'GT', + Gte = 'GTE', + In = 'IN', + IsEmpty = 'IS_EMPTY', + IsNotEmpty = 'IS_NOT_EMPTY', + IsNotNull = 'IS_NOT_NULL', + IsNull = 'IS_NULL', + Lt = 'LT', + Lte = 'LTE', + Neq = 'NEQ', + NoneIn = 'NONE_IN', + NotIn = 'NOT_IN', + StartsWith = 'STARTS_WITH' +} + +export type QuerySearchInput = { + includeProperties?: Scalars['Boolean']['input']; + searchText?: InputMaybe; +}; + +export type QuerySortClauseInput = { + direction: SortDirection; + fieldKey: Scalars['String']['input']; +}; + +export type QueryableChoiceMeta = { + __typename?: 'QueryableChoiceMeta'; + optionKeys: Array; + optionLabels: Array; +}; + +export type QueryableField = { + __typename?: 'QueryableField'; + allowedOperators: Array; + choice?: Maybe; + key: Scalars['String']['output']; + kind: QueryableFieldKind; + label: Scalars['String']['output']; + propertyDefinitionId?: Maybe; + relation?: Maybe; + searchable: Scalars['Boolean']['output']; + sortable: Scalars['Boolean']['output']; + valueType: QueryableValueType; +}; + +export enum QueryableFieldKind { + Choice = 'CHOICE', + ChoiceList = 'CHOICE_LIST', + Property = 'PROPERTY', + Reference = 'REFERENCE', + ReferenceList = 'REFERENCE_LIST', + Scalar = 'SCALAR' +} + +export type QueryableRelationMeta = { + __typename?: 'QueryableRelationMeta'; + allowedFilterModes: Array; + idFieldKey: Scalars['String']['output']; + labelFieldKey: Scalars['String']['output']; + targetEntity: Scalars['String']['output']; +}; + +export enum QueryableValueType { + Boolean = 'BOOLEAN', + Date = 'DATE', + Datetime = 'DATETIME', + Number = 'NUMBER', + String = 'STRING', + StringList = 'STRING_LIST', + Uuid = 'UUID', + UuidList = 'UUID_LIST' +} + +export enum ReferenceFilterMode { + Id = 'ID', + Label = 'LABEL' +} + export enum Sex { Female = 'FEMALE', Male = 'MALE', @@ -572,13 +610,6 @@ export enum SortDirection { Desc = 'DESC' } -export type SortInput = { - column: Scalars['String']['input']; - columnType?: ColumnType; - direction: SortDirection; - propertyDefinitionId?: InputMaybe; -}; - export type Subscription = { __typename?: 'Subscription'; locationNodeCreated: Scalars['ID']['output']; @@ -764,14 +795,14 @@ export type GetMyTasksQuery = { __typename?: 'Query', me?: { __typename?: 'UserT export type GetOverviewDataQueryVariables = Exact<{ rootLocationIds?: InputMaybe | Scalars['ID']['input']>; - recentPatientsFiltering?: InputMaybe | FilterInput>; - recentPatientsSorting?: InputMaybe | SortInput>; + recentPatientsFilters?: InputMaybe | QueryFilterClauseInput>; + recentPatientsSorts?: InputMaybe | QuerySortClauseInput>; recentPatientsPagination?: InputMaybe; - recentPatientsSearch?: InputMaybe; - recentTasksFiltering?: InputMaybe | FilterInput>; - recentTasksSorting?: InputMaybe | SortInput>; + recentPatientsSearch?: InputMaybe; + recentTasksFilters?: InputMaybe | QueryFilterClauseInput>; + recentTasksSorts?: InputMaybe | QuerySortClauseInput>; recentTasksPagination?: InputMaybe; - recentTasksSearch?: InputMaybe; + recentTasksSearch?: InputMaybe; }>; @@ -788,10 +819,10 @@ export type GetPatientsQueryVariables = Exact<{ locationId?: InputMaybe; rootLocationIds?: InputMaybe | Scalars['ID']['input']>; states?: InputMaybe | PatientState>; - filtering?: InputMaybe | FilterInput>; - sorting?: InputMaybe | SortInput>; + filters?: InputMaybe | QueryFilterClauseInput>; + sorts?: InputMaybe | QuerySortClauseInput>; pagination?: InputMaybe; - search?: InputMaybe; + search?: InputMaybe; }>; @@ -808,10 +839,10 @@ export type GetTasksQueryVariables = Exact<{ rootLocationIds?: InputMaybe | Scalars['ID']['input']>; assigneeId?: InputMaybe; assigneeTeamId?: InputMaybe; - filtering?: InputMaybe | FilterInput>; - sorting?: InputMaybe | SortInput>; + filters?: InputMaybe | QueryFilterClauseInput>; + sorts?: InputMaybe | QuerySortClauseInput>; pagination?: InputMaybe; - search?: InputMaybe; + search?: InputMaybe; }>; @@ -921,6 +952,13 @@ export type GetPropertiesForSubjectQueryVariables = Exact<{ export type GetPropertiesForSubjectQuery = { __typename?: 'Query', propertyDefinitions: Array<{ __typename?: 'PropertyDefinitionType', id: string, name: string, description?: string | null, fieldType: FieldType, isActive: boolean, allowedEntities: Array, options: Array }> }; +export type QueryableFieldsQueryVariables = Exact<{ + entity: Scalars['String']['input']; +}>; + + +export type QueryableFieldsQuery = { __typename?: 'Query', queryableFields: Array<{ __typename?: 'QueryableField', key: string, label: string, kind: QueryableFieldKind, valueType: QueryableValueType, allowedOperators: Array, sortable: boolean, searchable: boolean, propertyDefinitionId?: string | null, relation?: { __typename?: 'QueryableRelationMeta', targetEntity: string, idFieldKey: string, labelFieldKey: string, allowedFilterModes: Array } | null, choice?: { __typename?: 'QueryableChoiceMeta', optionKeys: Array, optionLabels: Array } | null }> }; + export type PatientCreatedSubscriptionVariables = Exact<{ rootLocationIds?: InputMaybe | Scalars['ID']['input']>; }>; @@ -1061,11 +1099,11 @@ export const GetAuditLogsDocument = {"kind":"Document","definitions":[{"kind":"O export const GetLocationNodeDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"GetLocationNode"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"id"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"ID"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"locationNode"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"id"},"value":{"kind":"Variable","name":{"kind":"Name","value":"id"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"title"}},{"kind":"Field","name":{"kind":"Name","value":"kind"}},{"kind":"Field","name":{"kind":"Name","value":"parentId"}},{"kind":"Field","name":{"kind":"Name","value":"parent"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"title"}},{"kind":"Field","name":{"kind":"Name","value":"kind"}},{"kind":"Field","name":{"kind":"Name","value":"parentId"}},{"kind":"Field","name":{"kind":"Name","value":"parent"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"title"}},{"kind":"Field","name":{"kind":"Name","value":"kind"}},{"kind":"Field","name":{"kind":"Name","value":"parentId"}},{"kind":"Field","name":{"kind":"Name","value":"parent"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"title"}},{"kind":"Field","name":{"kind":"Name","value":"kind"}},{"kind":"Field","name":{"kind":"Name","value":"parentId"}},{"kind":"Field","name":{"kind":"Name","value":"parent"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"title"}},{"kind":"Field","name":{"kind":"Name","value":"kind"}},{"kind":"Field","name":{"kind":"Name","value":"parentId"}}]}}]}}]}}]}}]}}]}}]} as unknown as DocumentNode; export const GetLocationsDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"GetLocations"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"limit"}},"type":{"kind":"NamedType","name":{"kind":"Name","value":"Int"}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"offset"}},"type":{"kind":"NamedType","name":{"kind":"Name","value":"Int"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"locationNodes"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"limit"},"value":{"kind":"Variable","name":{"kind":"Name","value":"limit"}}},{"kind":"Argument","name":{"kind":"Name","value":"offset"},"value":{"kind":"Variable","name":{"kind":"Name","value":"offset"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"title"}},{"kind":"Field","name":{"kind":"Name","value":"kind"}},{"kind":"Field","name":{"kind":"Name","value":"parentId"}}]}}]}}]} as unknown as DocumentNode; export const GetMyTasksDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"GetMyTasks"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"me"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"tasks"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"title"}},{"kind":"Field","name":{"kind":"Name","value":"description"}},{"kind":"Field","name":{"kind":"Name","value":"done"}},{"kind":"Field","name":{"kind":"Name","value":"dueDate"}},{"kind":"Field","name":{"kind":"Name","value":"priority"}},{"kind":"Field","name":{"kind":"Name","value":"estimatedTime"}},{"kind":"Field","name":{"kind":"Name","value":"creationDate"}},{"kind":"Field","name":{"kind":"Name","value":"updateDate"}},{"kind":"Field","name":{"kind":"Name","value":"patient"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"assignedLocation"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"title"}},{"kind":"Field","name":{"kind":"Name","value":"parent"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"title"}}]}}]}},{"kind":"Field","name":{"kind":"Name","value":"assignedLocations"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"title"}},{"kind":"Field","name":{"kind":"Name","value":"kind"}},{"kind":"Field","name":{"kind":"Name","value":"parent"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"title"}},{"kind":"Field","name":{"kind":"Name","value":"parent"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"title"}}]}}]}}]}}]}},{"kind":"Field","name":{"kind":"Name","value":"assignee"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"avatarUrl"}},{"kind":"Field","name":{"kind":"Name","value":"lastOnline"}},{"kind":"Field","name":{"kind":"Name","value":"isOnline"}}]}}]}}]}}]}}]} as unknown as DocumentNode; -export const GetOverviewDataDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"GetOverviewData"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"rootLocationIds"}},"type":{"kind":"ListType","type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"ID"}}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"recentPatientsFiltering"}},"type":{"kind":"ListType","type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"FilterInput"}}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"recentPatientsSorting"}},"type":{"kind":"ListType","type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"SortInput"}}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"recentPatientsPagination"}},"type":{"kind":"NamedType","name":{"kind":"Name","value":"PaginationInput"}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"recentPatientsSearch"}},"type":{"kind":"NamedType","name":{"kind":"Name","value":"FullTextSearchInput"}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"recentTasksFiltering"}},"type":{"kind":"ListType","type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"FilterInput"}}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"recentTasksSorting"}},"type":{"kind":"ListType","type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"SortInput"}}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"recentTasksPagination"}},"type":{"kind":"NamedType","name":{"kind":"Name","value":"PaginationInput"}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"recentTasksSearch"}},"type":{"kind":"NamedType","name":{"kind":"Name","value":"FullTextSearchInput"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"recentPatients"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"filtering"},"value":{"kind":"Variable","name":{"kind":"Name","value":"recentPatientsFiltering"}}},{"kind":"Argument","name":{"kind":"Name","value":"sorting"},"value":{"kind":"Variable","name":{"kind":"Name","value":"recentPatientsSorting"}}},{"kind":"Argument","name":{"kind":"Name","value":"pagination"},"value":{"kind":"Variable","name":{"kind":"Name","value":"recentPatientsPagination"}}},{"kind":"Argument","name":{"kind":"Name","value":"search"},"value":{"kind":"Variable","name":{"kind":"Name","value":"recentPatientsSearch"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"sex"}},{"kind":"Field","name":{"kind":"Name","value":"birthdate"}},{"kind":"Field","name":{"kind":"Name","value":"position"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"title"}},{"kind":"Field","name":{"kind":"Name","value":"kind"}},{"kind":"Field","name":{"kind":"Name","value":"parent"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"title"}}]}}]}},{"kind":"Field","name":{"kind":"Name","value":"tasks"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"updateDate"}}]}},{"kind":"Field","name":{"kind":"Name","value":"properties"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"definition"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"description"}},{"kind":"Field","name":{"kind":"Name","value":"fieldType"}},{"kind":"Field","name":{"kind":"Name","value":"isActive"}},{"kind":"Field","name":{"kind":"Name","value":"allowedEntities"}},{"kind":"Field","name":{"kind":"Name","value":"options"}}]}},{"kind":"Field","name":{"kind":"Name","value":"textValue"}},{"kind":"Field","name":{"kind":"Name","value":"numberValue"}},{"kind":"Field","name":{"kind":"Name","value":"booleanValue"}},{"kind":"Field","name":{"kind":"Name","value":"dateValue"}},{"kind":"Field","name":{"kind":"Name","value":"dateTimeValue"}},{"kind":"Field","name":{"kind":"Name","value":"selectValue"}},{"kind":"Field","name":{"kind":"Name","value":"multiSelectValues"}},{"kind":"Field","name":{"kind":"Name","value":"userValue"}},{"kind":"Field","name":{"kind":"Name","value":"user"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"avatarUrl"}},{"kind":"Field","name":{"kind":"Name","value":"lastOnline"}},{"kind":"Field","name":{"kind":"Name","value":"isOnline"}}]}},{"kind":"Field","name":{"kind":"Name","value":"team"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"title"}},{"kind":"Field","name":{"kind":"Name","value":"kind"}}]}}]}}]}},{"kind":"Field","name":{"kind":"Name","value":"recentPatientsTotal"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"rootLocationIds"},"value":{"kind":"Variable","name":{"kind":"Name","value":"rootLocationIds"}}},{"kind":"Argument","name":{"kind":"Name","value":"filtering"},"value":{"kind":"Variable","name":{"kind":"Name","value":"recentPatientsFiltering"}}},{"kind":"Argument","name":{"kind":"Name","value":"sorting"},"value":{"kind":"Variable","name":{"kind":"Name","value":"recentPatientsSorting"}}},{"kind":"Argument","name":{"kind":"Name","value":"search"},"value":{"kind":"Variable","name":{"kind":"Name","value":"recentPatientsSearch"}}}]},{"kind":"Field","name":{"kind":"Name","value":"recentTasks"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"filtering"},"value":{"kind":"Variable","name":{"kind":"Name","value":"recentTasksFiltering"}}},{"kind":"Argument","name":{"kind":"Name","value":"sorting"},"value":{"kind":"Variable","name":{"kind":"Name","value":"recentTasksSorting"}}},{"kind":"Argument","name":{"kind":"Name","value":"pagination"},"value":{"kind":"Variable","name":{"kind":"Name","value":"recentTasksPagination"}}},{"kind":"Argument","name":{"kind":"Name","value":"search"},"value":{"kind":"Variable","name":{"kind":"Name","value":"recentTasksSearch"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"title"}},{"kind":"Field","name":{"kind":"Name","value":"description"}},{"kind":"Field","name":{"kind":"Name","value":"done"}},{"kind":"Field","name":{"kind":"Name","value":"dueDate"}},{"kind":"Field","name":{"kind":"Name","value":"updateDate"}},{"kind":"Field","name":{"kind":"Name","value":"priority"}},{"kind":"Field","name":{"kind":"Name","value":"assignee"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"avatarUrl"}},{"kind":"Field","name":{"kind":"Name","value":"lastOnline"}},{"kind":"Field","name":{"kind":"Name","value":"isOnline"}}]}},{"kind":"Field","name":{"kind":"Name","value":"patient"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"position"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"title"}},{"kind":"Field","name":{"kind":"Name","value":"kind"}},{"kind":"Field","name":{"kind":"Name","value":"parent"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"title"}}]}}]}}]}},{"kind":"Field","name":{"kind":"Name","value":"properties"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"definition"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"description"}},{"kind":"Field","name":{"kind":"Name","value":"fieldType"}},{"kind":"Field","name":{"kind":"Name","value":"isActive"}},{"kind":"Field","name":{"kind":"Name","value":"allowedEntities"}},{"kind":"Field","name":{"kind":"Name","value":"options"}}]}},{"kind":"Field","name":{"kind":"Name","value":"textValue"}},{"kind":"Field","name":{"kind":"Name","value":"numberValue"}},{"kind":"Field","name":{"kind":"Name","value":"booleanValue"}},{"kind":"Field","name":{"kind":"Name","value":"dateValue"}},{"kind":"Field","name":{"kind":"Name","value":"dateTimeValue"}},{"kind":"Field","name":{"kind":"Name","value":"selectValue"}},{"kind":"Field","name":{"kind":"Name","value":"multiSelectValues"}},{"kind":"Field","name":{"kind":"Name","value":"userValue"}},{"kind":"Field","name":{"kind":"Name","value":"user"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"avatarUrl"}},{"kind":"Field","name":{"kind":"Name","value":"lastOnline"}},{"kind":"Field","name":{"kind":"Name","value":"isOnline"}}]}},{"kind":"Field","name":{"kind":"Name","value":"team"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"title"}},{"kind":"Field","name":{"kind":"Name","value":"kind"}}]}}]}}]}},{"kind":"Field","name":{"kind":"Name","value":"recentTasksTotal"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"rootLocationIds"},"value":{"kind":"Variable","name":{"kind":"Name","value":"rootLocationIds"}}},{"kind":"Argument","name":{"kind":"Name","value":"filtering"},"value":{"kind":"Variable","name":{"kind":"Name","value":"recentTasksFiltering"}}},{"kind":"Argument","name":{"kind":"Name","value":"sorting"},"value":{"kind":"Variable","name":{"kind":"Name","value":"recentTasksSorting"}}},{"kind":"Argument","name":{"kind":"Name","value":"search"},"value":{"kind":"Variable","name":{"kind":"Name","value":"recentTasksSearch"}}}]}]}}]} as unknown as DocumentNode; +export const GetOverviewDataDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"GetOverviewData"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"rootLocationIds"}},"type":{"kind":"ListType","type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"ID"}}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"recentPatientsFilters"}},"type":{"kind":"ListType","type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"QueryFilterClauseInput"}}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"recentPatientsSorts"}},"type":{"kind":"ListType","type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"QuerySortClauseInput"}}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"recentPatientsPagination"}},"type":{"kind":"NamedType","name":{"kind":"Name","value":"PaginationInput"}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"recentPatientsSearch"}},"type":{"kind":"NamedType","name":{"kind":"Name","value":"QuerySearchInput"}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"recentTasksFilters"}},"type":{"kind":"ListType","type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"QueryFilterClauseInput"}}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"recentTasksSorts"}},"type":{"kind":"ListType","type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"QuerySortClauseInput"}}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"recentTasksPagination"}},"type":{"kind":"NamedType","name":{"kind":"Name","value":"PaginationInput"}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"recentTasksSearch"}},"type":{"kind":"NamedType","name":{"kind":"Name","value":"QuerySearchInput"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"recentPatients"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"rootLocationIds"},"value":{"kind":"Variable","name":{"kind":"Name","value":"rootLocationIds"}}},{"kind":"Argument","name":{"kind":"Name","value":"filters"},"value":{"kind":"Variable","name":{"kind":"Name","value":"recentPatientsFilters"}}},{"kind":"Argument","name":{"kind":"Name","value":"sorts"},"value":{"kind":"Variable","name":{"kind":"Name","value":"recentPatientsSorts"}}},{"kind":"Argument","name":{"kind":"Name","value":"pagination"},"value":{"kind":"Variable","name":{"kind":"Name","value":"recentPatientsPagination"}}},{"kind":"Argument","name":{"kind":"Name","value":"search"},"value":{"kind":"Variable","name":{"kind":"Name","value":"recentPatientsSearch"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"sex"}},{"kind":"Field","name":{"kind":"Name","value":"birthdate"}},{"kind":"Field","name":{"kind":"Name","value":"position"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"title"}},{"kind":"Field","name":{"kind":"Name","value":"kind"}},{"kind":"Field","name":{"kind":"Name","value":"parent"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"title"}}]}}]}},{"kind":"Field","name":{"kind":"Name","value":"tasks"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"updateDate"}}]}},{"kind":"Field","name":{"kind":"Name","value":"properties"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"definition"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"description"}},{"kind":"Field","name":{"kind":"Name","value":"fieldType"}},{"kind":"Field","name":{"kind":"Name","value":"isActive"}},{"kind":"Field","name":{"kind":"Name","value":"allowedEntities"}},{"kind":"Field","name":{"kind":"Name","value":"options"}}]}},{"kind":"Field","name":{"kind":"Name","value":"textValue"}},{"kind":"Field","name":{"kind":"Name","value":"numberValue"}},{"kind":"Field","name":{"kind":"Name","value":"booleanValue"}},{"kind":"Field","name":{"kind":"Name","value":"dateValue"}},{"kind":"Field","name":{"kind":"Name","value":"dateTimeValue"}},{"kind":"Field","name":{"kind":"Name","value":"selectValue"}},{"kind":"Field","name":{"kind":"Name","value":"multiSelectValues"}},{"kind":"Field","name":{"kind":"Name","value":"userValue"}},{"kind":"Field","name":{"kind":"Name","value":"user"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"avatarUrl"}},{"kind":"Field","name":{"kind":"Name","value":"lastOnline"}},{"kind":"Field","name":{"kind":"Name","value":"isOnline"}}]}},{"kind":"Field","name":{"kind":"Name","value":"team"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"title"}},{"kind":"Field","name":{"kind":"Name","value":"kind"}}]}}]}}]}},{"kind":"Field","name":{"kind":"Name","value":"recentPatientsTotal"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"rootLocationIds"},"value":{"kind":"Variable","name":{"kind":"Name","value":"rootLocationIds"}}},{"kind":"Argument","name":{"kind":"Name","value":"filters"},"value":{"kind":"Variable","name":{"kind":"Name","value":"recentPatientsFilters"}}},{"kind":"Argument","name":{"kind":"Name","value":"sorts"},"value":{"kind":"Variable","name":{"kind":"Name","value":"recentPatientsSorts"}}},{"kind":"Argument","name":{"kind":"Name","value":"search"},"value":{"kind":"Variable","name":{"kind":"Name","value":"recentPatientsSearch"}}}]},{"kind":"Field","name":{"kind":"Name","value":"recentTasks"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"rootLocationIds"},"value":{"kind":"Variable","name":{"kind":"Name","value":"rootLocationIds"}}},{"kind":"Argument","name":{"kind":"Name","value":"filters"},"value":{"kind":"Variable","name":{"kind":"Name","value":"recentTasksFilters"}}},{"kind":"Argument","name":{"kind":"Name","value":"sorts"},"value":{"kind":"Variable","name":{"kind":"Name","value":"recentTasksSorts"}}},{"kind":"Argument","name":{"kind":"Name","value":"pagination"},"value":{"kind":"Variable","name":{"kind":"Name","value":"recentTasksPagination"}}},{"kind":"Argument","name":{"kind":"Name","value":"search"},"value":{"kind":"Variable","name":{"kind":"Name","value":"recentTasksSearch"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"title"}},{"kind":"Field","name":{"kind":"Name","value":"description"}},{"kind":"Field","name":{"kind":"Name","value":"done"}},{"kind":"Field","name":{"kind":"Name","value":"dueDate"}},{"kind":"Field","name":{"kind":"Name","value":"updateDate"}},{"kind":"Field","name":{"kind":"Name","value":"priority"}},{"kind":"Field","name":{"kind":"Name","value":"assignee"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"avatarUrl"}},{"kind":"Field","name":{"kind":"Name","value":"lastOnline"}},{"kind":"Field","name":{"kind":"Name","value":"isOnline"}}]}},{"kind":"Field","name":{"kind":"Name","value":"patient"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"position"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"title"}},{"kind":"Field","name":{"kind":"Name","value":"kind"}},{"kind":"Field","name":{"kind":"Name","value":"parent"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"title"}}]}}]}}]}},{"kind":"Field","name":{"kind":"Name","value":"properties"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"definition"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"description"}},{"kind":"Field","name":{"kind":"Name","value":"fieldType"}},{"kind":"Field","name":{"kind":"Name","value":"isActive"}},{"kind":"Field","name":{"kind":"Name","value":"allowedEntities"}},{"kind":"Field","name":{"kind":"Name","value":"options"}}]}},{"kind":"Field","name":{"kind":"Name","value":"textValue"}},{"kind":"Field","name":{"kind":"Name","value":"numberValue"}},{"kind":"Field","name":{"kind":"Name","value":"booleanValue"}},{"kind":"Field","name":{"kind":"Name","value":"dateValue"}},{"kind":"Field","name":{"kind":"Name","value":"dateTimeValue"}},{"kind":"Field","name":{"kind":"Name","value":"selectValue"}},{"kind":"Field","name":{"kind":"Name","value":"multiSelectValues"}},{"kind":"Field","name":{"kind":"Name","value":"userValue"}},{"kind":"Field","name":{"kind":"Name","value":"user"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"avatarUrl"}},{"kind":"Field","name":{"kind":"Name","value":"lastOnline"}},{"kind":"Field","name":{"kind":"Name","value":"isOnline"}}]}},{"kind":"Field","name":{"kind":"Name","value":"team"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"title"}},{"kind":"Field","name":{"kind":"Name","value":"kind"}}]}}]}}]}},{"kind":"Field","name":{"kind":"Name","value":"recentTasksTotal"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"rootLocationIds"},"value":{"kind":"Variable","name":{"kind":"Name","value":"rootLocationIds"}}},{"kind":"Argument","name":{"kind":"Name","value":"filters"},"value":{"kind":"Variable","name":{"kind":"Name","value":"recentTasksFilters"}}},{"kind":"Argument","name":{"kind":"Name","value":"sorts"},"value":{"kind":"Variable","name":{"kind":"Name","value":"recentTasksSorts"}}},{"kind":"Argument","name":{"kind":"Name","value":"search"},"value":{"kind":"Variable","name":{"kind":"Name","value":"recentTasksSearch"}}}]}]}}]} as unknown as DocumentNode; export const GetPatientDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"GetPatient"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"id"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"ID"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"patient"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"id"},"value":{"kind":"Variable","name":{"kind":"Name","value":"id"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"firstname"}},{"kind":"Field","name":{"kind":"Name","value":"lastname"}},{"kind":"Field","name":{"kind":"Name","value":"birthdate"}},{"kind":"Field","name":{"kind":"Name","value":"sex"}},{"kind":"Field","name":{"kind":"Name","value":"state"}},{"kind":"Field","name":{"kind":"Name","value":"description"}},{"kind":"Field","name":{"kind":"Name","value":"checksum"}},{"kind":"Field","name":{"kind":"Name","value":"assignedLocation"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"title"}}]}},{"kind":"Field","name":{"kind":"Name","value":"assignedLocations"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"title"}}]}},{"kind":"Field","name":{"kind":"Name","value":"clinic"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"title"}},{"kind":"Field","name":{"kind":"Name","value":"kind"}},{"kind":"Field","name":{"kind":"Name","value":"parent"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"title"}},{"kind":"Field","name":{"kind":"Name","value":"parent"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"title"}},{"kind":"Field","name":{"kind":"Name","value":"parent"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"title"}},{"kind":"Field","name":{"kind":"Name","value":"parent"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"title"}}]}}]}}]}}]}}]}},{"kind":"Field","name":{"kind":"Name","value":"position"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"title"}},{"kind":"Field","name":{"kind":"Name","value":"kind"}},{"kind":"Field","name":{"kind":"Name","value":"parent"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"title"}},{"kind":"Field","name":{"kind":"Name","value":"parent"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"title"}},{"kind":"Field","name":{"kind":"Name","value":"parent"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"title"}},{"kind":"Field","name":{"kind":"Name","value":"parent"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"title"}}]}}]}}]}}]}}]}},{"kind":"Field","name":{"kind":"Name","value":"teams"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"title"}},{"kind":"Field","name":{"kind":"Name","value":"kind"}},{"kind":"Field","name":{"kind":"Name","value":"parent"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"title"}},{"kind":"Field","name":{"kind":"Name","value":"parent"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"title"}},{"kind":"Field","name":{"kind":"Name","value":"parent"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"title"}},{"kind":"Field","name":{"kind":"Name","value":"parent"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"title"}}]}}]}}]}}]}}]}},{"kind":"Field","name":{"kind":"Name","value":"tasks"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"title"}},{"kind":"Field","name":{"kind":"Name","value":"description"}},{"kind":"Field","name":{"kind":"Name","value":"done"}},{"kind":"Field","name":{"kind":"Name","value":"dueDate"}},{"kind":"Field","name":{"kind":"Name","value":"priority"}},{"kind":"Field","name":{"kind":"Name","value":"estimatedTime"}},{"kind":"Field","name":{"kind":"Name","value":"updateDate"}},{"kind":"Field","name":{"kind":"Name","value":"assignee"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"avatarUrl"}},{"kind":"Field","name":{"kind":"Name","value":"lastOnline"}},{"kind":"Field","name":{"kind":"Name","value":"isOnline"}}]}},{"kind":"Field","name":{"kind":"Name","value":"assigneeTeam"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"title"}},{"kind":"Field","name":{"kind":"Name","value":"kind"}}]}}]}},{"kind":"Field","name":{"kind":"Name","value":"properties"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"definition"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"description"}},{"kind":"Field","name":{"kind":"Name","value":"fieldType"}},{"kind":"Field","name":{"kind":"Name","value":"isActive"}},{"kind":"Field","name":{"kind":"Name","value":"allowedEntities"}},{"kind":"Field","name":{"kind":"Name","value":"options"}}]}},{"kind":"Field","name":{"kind":"Name","value":"textValue"}},{"kind":"Field","name":{"kind":"Name","value":"numberValue"}},{"kind":"Field","name":{"kind":"Name","value":"booleanValue"}},{"kind":"Field","name":{"kind":"Name","value":"dateValue"}},{"kind":"Field","name":{"kind":"Name","value":"dateTimeValue"}},{"kind":"Field","name":{"kind":"Name","value":"selectValue"}},{"kind":"Field","name":{"kind":"Name","value":"multiSelectValues"}},{"kind":"Field","name":{"kind":"Name","value":"userValue"}},{"kind":"Field","name":{"kind":"Name","value":"user"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"avatarUrl"}},{"kind":"Field","name":{"kind":"Name","value":"lastOnline"}},{"kind":"Field","name":{"kind":"Name","value":"isOnline"}}]}},{"kind":"Field","name":{"kind":"Name","value":"team"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"title"}},{"kind":"Field","name":{"kind":"Name","value":"kind"}}]}}]}}]}}]}}]} as unknown as DocumentNode; -export const GetPatientsDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"GetPatients"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"locationId"}},"type":{"kind":"NamedType","name":{"kind":"Name","value":"ID"}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"rootLocationIds"}},"type":{"kind":"ListType","type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"ID"}}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"states"}},"type":{"kind":"ListType","type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"PatientState"}}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"filtering"}},"type":{"kind":"ListType","type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"FilterInput"}}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"sorting"}},"type":{"kind":"ListType","type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"SortInput"}}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"pagination"}},"type":{"kind":"NamedType","name":{"kind":"Name","value":"PaginationInput"}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"search"}},"type":{"kind":"NamedType","name":{"kind":"Name","value":"FullTextSearchInput"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"patients"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"locationNodeId"},"value":{"kind":"Variable","name":{"kind":"Name","value":"locationId"}}},{"kind":"Argument","name":{"kind":"Name","value":"rootLocationIds"},"value":{"kind":"Variable","name":{"kind":"Name","value":"rootLocationIds"}}},{"kind":"Argument","name":{"kind":"Name","value":"states"},"value":{"kind":"Variable","name":{"kind":"Name","value":"states"}}},{"kind":"Argument","name":{"kind":"Name","value":"filtering"},"value":{"kind":"Variable","name":{"kind":"Name","value":"filtering"}}},{"kind":"Argument","name":{"kind":"Name","value":"sorting"},"value":{"kind":"Variable","name":{"kind":"Name","value":"sorting"}}},{"kind":"Argument","name":{"kind":"Name","value":"pagination"},"value":{"kind":"Variable","name":{"kind":"Name","value":"pagination"}}},{"kind":"Argument","name":{"kind":"Name","value":"search"},"value":{"kind":"Variable","name":{"kind":"Name","value":"search"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"firstname"}},{"kind":"Field","name":{"kind":"Name","value":"lastname"}},{"kind":"Field","name":{"kind":"Name","value":"birthdate"}},{"kind":"Field","name":{"kind":"Name","value":"sex"}},{"kind":"Field","name":{"kind":"Name","value":"state"}},{"kind":"Field","name":{"kind":"Name","value":"assignedLocation"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"title"}},{"kind":"Field","name":{"kind":"Name","value":"parent"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"title"}}]}}]}},{"kind":"Field","name":{"kind":"Name","value":"assignedLocations"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"title"}},{"kind":"Field","name":{"kind":"Name","value":"kind"}},{"kind":"Field","name":{"kind":"Name","value":"parent"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"title"}},{"kind":"Field","name":{"kind":"Name","value":"parent"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"title"}},{"kind":"Field","name":{"kind":"Name","value":"parent"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"title"}}]}}]}}]}}]}},{"kind":"Field","name":{"kind":"Name","value":"clinic"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"title"}},{"kind":"Field","name":{"kind":"Name","value":"kind"}},{"kind":"Field","name":{"kind":"Name","value":"parent"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"title"}},{"kind":"Field","name":{"kind":"Name","value":"parent"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"title"}},{"kind":"Field","name":{"kind":"Name","value":"parent"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"title"}},{"kind":"Field","name":{"kind":"Name","value":"parent"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"title"}}]}}]}}]}}]}}]}},{"kind":"Field","name":{"kind":"Name","value":"position"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"title"}},{"kind":"Field","name":{"kind":"Name","value":"kind"}},{"kind":"Field","name":{"kind":"Name","value":"parent"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"title"}},{"kind":"Field","name":{"kind":"Name","value":"kind"}},{"kind":"Field","name":{"kind":"Name","value":"parent"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"title"}},{"kind":"Field","name":{"kind":"Name","value":"kind"}},{"kind":"Field","name":{"kind":"Name","value":"parent"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"title"}},{"kind":"Field","name":{"kind":"Name","value":"kind"}},{"kind":"Field","name":{"kind":"Name","value":"parent"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"title"}},{"kind":"Field","name":{"kind":"Name","value":"kind"}}]}}]}}]}}]}}]}},{"kind":"Field","name":{"kind":"Name","value":"teams"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"title"}},{"kind":"Field","name":{"kind":"Name","value":"kind"}},{"kind":"Field","name":{"kind":"Name","value":"parent"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"title"}},{"kind":"Field","name":{"kind":"Name","value":"parent"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"title"}},{"kind":"Field","name":{"kind":"Name","value":"parent"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"title"}},{"kind":"Field","name":{"kind":"Name","value":"parent"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"title"}}]}}]}}]}}]}}]}},{"kind":"Field","name":{"kind":"Name","value":"tasks"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"title"}},{"kind":"Field","name":{"kind":"Name","value":"description"}},{"kind":"Field","name":{"kind":"Name","value":"done"}},{"kind":"Field","name":{"kind":"Name","value":"dueDate"}},{"kind":"Field","name":{"kind":"Name","value":"priority"}},{"kind":"Field","name":{"kind":"Name","value":"estimatedTime"}},{"kind":"Field","name":{"kind":"Name","value":"creationDate"}},{"kind":"Field","name":{"kind":"Name","value":"updateDate"}},{"kind":"Field","name":{"kind":"Name","value":"assignee"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"avatarUrl"}},{"kind":"Field","name":{"kind":"Name","value":"lastOnline"}},{"kind":"Field","name":{"kind":"Name","value":"isOnline"}}]}},{"kind":"Field","name":{"kind":"Name","value":"assigneeTeam"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"title"}},{"kind":"Field","name":{"kind":"Name","value":"kind"}}]}}]}},{"kind":"Field","name":{"kind":"Name","value":"properties"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"definition"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"description"}},{"kind":"Field","name":{"kind":"Name","value":"fieldType"}},{"kind":"Field","name":{"kind":"Name","value":"isActive"}},{"kind":"Field","name":{"kind":"Name","value":"allowedEntities"}},{"kind":"Field","name":{"kind":"Name","value":"options"}}]}},{"kind":"Field","name":{"kind":"Name","value":"textValue"}},{"kind":"Field","name":{"kind":"Name","value":"numberValue"}},{"kind":"Field","name":{"kind":"Name","value":"booleanValue"}},{"kind":"Field","name":{"kind":"Name","value":"dateValue"}},{"kind":"Field","name":{"kind":"Name","value":"dateTimeValue"}},{"kind":"Field","name":{"kind":"Name","value":"selectValue"}},{"kind":"Field","name":{"kind":"Name","value":"multiSelectValues"}},{"kind":"Field","name":{"kind":"Name","value":"userValue"}},{"kind":"Field","name":{"kind":"Name","value":"user"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"avatarUrl"}},{"kind":"Field","name":{"kind":"Name","value":"lastOnline"}},{"kind":"Field","name":{"kind":"Name","value":"isOnline"}}]}},{"kind":"Field","name":{"kind":"Name","value":"team"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"title"}},{"kind":"Field","name":{"kind":"Name","value":"kind"}}]}}]}}]}},{"kind":"Field","name":{"kind":"Name","value":"patientsTotal"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"locationNodeId"},"value":{"kind":"Variable","name":{"kind":"Name","value":"locationId"}}},{"kind":"Argument","name":{"kind":"Name","value":"rootLocationIds"},"value":{"kind":"Variable","name":{"kind":"Name","value":"rootLocationIds"}}},{"kind":"Argument","name":{"kind":"Name","value":"states"},"value":{"kind":"Variable","name":{"kind":"Name","value":"states"}}},{"kind":"Argument","name":{"kind":"Name","value":"filtering"},"value":{"kind":"Variable","name":{"kind":"Name","value":"filtering"}}},{"kind":"Argument","name":{"kind":"Name","value":"sorting"},"value":{"kind":"Variable","name":{"kind":"Name","value":"sorting"}}},{"kind":"Argument","name":{"kind":"Name","value":"search"},"value":{"kind":"Variable","name":{"kind":"Name","value":"search"}}}]}]}}]} as unknown as DocumentNode; +export const GetPatientsDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"GetPatients"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"locationId"}},"type":{"kind":"NamedType","name":{"kind":"Name","value":"ID"}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"rootLocationIds"}},"type":{"kind":"ListType","type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"ID"}}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"states"}},"type":{"kind":"ListType","type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"PatientState"}}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"filters"}},"type":{"kind":"ListType","type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"QueryFilterClauseInput"}}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"sorts"}},"type":{"kind":"ListType","type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"QuerySortClauseInput"}}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"pagination"}},"type":{"kind":"NamedType","name":{"kind":"Name","value":"PaginationInput"}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"search"}},"type":{"kind":"NamedType","name":{"kind":"Name","value":"QuerySearchInput"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"patients"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"locationNodeId"},"value":{"kind":"Variable","name":{"kind":"Name","value":"locationId"}}},{"kind":"Argument","name":{"kind":"Name","value":"rootLocationIds"},"value":{"kind":"Variable","name":{"kind":"Name","value":"rootLocationIds"}}},{"kind":"Argument","name":{"kind":"Name","value":"states"},"value":{"kind":"Variable","name":{"kind":"Name","value":"states"}}},{"kind":"Argument","name":{"kind":"Name","value":"filters"},"value":{"kind":"Variable","name":{"kind":"Name","value":"filters"}}},{"kind":"Argument","name":{"kind":"Name","value":"sorts"},"value":{"kind":"Variable","name":{"kind":"Name","value":"sorts"}}},{"kind":"Argument","name":{"kind":"Name","value":"pagination"},"value":{"kind":"Variable","name":{"kind":"Name","value":"pagination"}}},{"kind":"Argument","name":{"kind":"Name","value":"search"},"value":{"kind":"Variable","name":{"kind":"Name","value":"search"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"firstname"}},{"kind":"Field","name":{"kind":"Name","value":"lastname"}},{"kind":"Field","name":{"kind":"Name","value":"birthdate"}},{"kind":"Field","name":{"kind":"Name","value":"sex"}},{"kind":"Field","name":{"kind":"Name","value":"state"}},{"kind":"Field","name":{"kind":"Name","value":"assignedLocation"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"title"}},{"kind":"Field","name":{"kind":"Name","value":"parent"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"title"}}]}}]}},{"kind":"Field","name":{"kind":"Name","value":"assignedLocations"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"title"}},{"kind":"Field","name":{"kind":"Name","value":"kind"}},{"kind":"Field","name":{"kind":"Name","value":"parent"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"title"}},{"kind":"Field","name":{"kind":"Name","value":"parent"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"title"}},{"kind":"Field","name":{"kind":"Name","value":"parent"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"title"}}]}}]}}]}}]}},{"kind":"Field","name":{"kind":"Name","value":"clinic"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"title"}},{"kind":"Field","name":{"kind":"Name","value":"kind"}},{"kind":"Field","name":{"kind":"Name","value":"parent"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"title"}},{"kind":"Field","name":{"kind":"Name","value":"parent"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"title"}},{"kind":"Field","name":{"kind":"Name","value":"parent"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"title"}},{"kind":"Field","name":{"kind":"Name","value":"parent"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"title"}}]}}]}}]}}]}}]}},{"kind":"Field","name":{"kind":"Name","value":"position"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"title"}},{"kind":"Field","name":{"kind":"Name","value":"kind"}},{"kind":"Field","name":{"kind":"Name","value":"parent"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"title"}},{"kind":"Field","name":{"kind":"Name","value":"kind"}},{"kind":"Field","name":{"kind":"Name","value":"parent"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"title"}},{"kind":"Field","name":{"kind":"Name","value":"kind"}},{"kind":"Field","name":{"kind":"Name","value":"parent"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"title"}},{"kind":"Field","name":{"kind":"Name","value":"kind"}},{"kind":"Field","name":{"kind":"Name","value":"parent"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"title"}},{"kind":"Field","name":{"kind":"Name","value":"kind"}}]}}]}}]}}]}}]}},{"kind":"Field","name":{"kind":"Name","value":"teams"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"title"}},{"kind":"Field","name":{"kind":"Name","value":"kind"}},{"kind":"Field","name":{"kind":"Name","value":"parent"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"title"}},{"kind":"Field","name":{"kind":"Name","value":"parent"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"title"}},{"kind":"Field","name":{"kind":"Name","value":"parent"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"title"}},{"kind":"Field","name":{"kind":"Name","value":"parent"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"title"}}]}}]}}]}}]}}]}},{"kind":"Field","name":{"kind":"Name","value":"tasks"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"title"}},{"kind":"Field","name":{"kind":"Name","value":"description"}},{"kind":"Field","name":{"kind":"Name","value":"done"}},{"kind":"Field","name":{"kind":"Name","value":"dueDate"}},{"kind":"Field","name":{"kind":"Name","value":"priority"}},{"kind":"Field","name":{"kind":"Name","value":"estimatedTime"}},{"kind":"Field","name":{"kind":"Name","value":"creationDate"}},{"kind":"Field","name":{"kind":"Name","value":"updateDate"}},{"kind":"Field","name":{"kind":"Name","value":"assignee"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"avatarUrl"}},{"kind":"Field","name":{"kind":"Name","value":"lastOnline"}},{"kind":"Field","name":{"kind":"Name","value":"isOnline"}}]}},{"kind":"Field","name":{"kind":"Name","value":"assigneeTeam"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"title"}},{"kind":"Field","name":{"kind":"Name","value":"kind"}}]}}]}},{"kind":"Field","name":{"kind":"Name","value":"properties"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"definition"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"description"}},{"kind":"Field","name":{"kind":"Name","value":"fieldType"}},{"kind":"Field","name":{"kind":"Name","value":"isActive"}},{"kind":"Field","name":{"kind":"Name","value":"allowedEntities"}},{"kind":"Field","name":{"kind":"Name","value":"options"}}]}},{"kind":"Field","name":{"kind":"Name","value":"textValue"}},{"kind":"Field","name":{"kind":"Name","value":"numberValue"}},{"kind":"Field","name":{"kind":"Name","value":"booleanValue"}},{"kind":"Field","name":{"kind":"Name","value":"dateValue"}},{"kind":"Field","name":{"kind":"Name","value":"dateTimeValue"}},{"kind":"Field","name":{"kind":"Name","value":"selectValue"}},{"kind":"Field","name":{"kind":"Name","value":"multiSelectValues"}},{"kind":"Field","name":{"kind":"Name","value":"userValue"}},{"kind":"Field","name":{"kind":"Name","value":"user"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"avatarUrl"}},{"kind":"Field","name":{"kind":"Name","value":"lastOnline"}},{"kind":"Field","name":{"kind":"Name","value":"isOnline"}}]}},{"kind":"Field","name":{"kind":"Name","value":"team"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"title"}},{"kind":"Field","name":{"kind":"Name","value":"kind"}}]}}]}}]}},{"kind":"Field","name":{"kind":"Name","value":"patientsTotal"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"locationNodeId"},"value":{"kind":"Variable","name":{"kind":"Name","value":"locationId"}}},{"kind":"Argument","name":{"kind":"Name","value":"rootLocationIds"},"value":{"kind":"Variable","name":{"kind":"Name","value":"rootLocationIds"}}},{"kind":"Argument","name":{"kind":"Name","value":"states"},"value":{"kind":"Variable","name":{"kind":"Name","value":"states"}}},{"kind":"Argument","name":{"kind":"Name","value":"filters"},"value":{"kind":"Variable","name":{"kind":"Name","value":"filters"}}},{"kind":"Argument","name":{"kind":"Name","value":"sorts"},"value":{"kind":"Variable","name":{"kind":"Name","value":"sorts"}}},{"kind":"Argument","name":{"kind":"Name","value":"search"},"value":{"kind":"Variable","name":{"kind":"Name","value":"search"}}}]}]}}]} as unknown as DocumentNode; export const GetTaskDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"GetTask"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"id"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"ID"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"task"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"id"},"value":{"kind":"Variable","name":{"kind":"Name","value":"id"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"title"}},{"kind":"Field","name":{"kind":"Name","value":"description"}},{"kind":"Field","name":{"kind":"Name","value":"done"}},{"kind":"Field","name":{"kind":"Name","value":"dueDate"}},{"kind":"Field","name":{"kind":"Name","value":"priority"}},{"kind":"Field","name":{"kind":"Name","value":"estimatedTime"}},{"kind":"Field","name":{"kind":"Name","value":"checksum"}},{"kind":"Field","name":{"kind":"Name","value":"updateDate"}},{"kind":"Field","name":{"kind":"Name","value":"patient"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}}]}},{"kind":"Field","name":{"kind":"Name","value":"assignee"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"avatarUrl"}},{"kind":"Field","name":{"kind":"Name","value":"lastOnline"}},{"kind":"Field","name":{"kind":"Name","value":"isOnline"}}]}},{"kind":"Field","name":{"kind":"Name","value":"assigneeTeam"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"title"}},{"kind":"Field","name":{"kind":"Name","value":"kind"}}]}},{"kind":"Field","name":{"kind":"Name","value":"properties"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"definition"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"description"}},{"kind":"Field","name":{"kind":"Name","value":"fieldType"}},{"kind":"Field","name":{"kind":"Name","value":"isActive"}},{"kind":"Field","name":{"kind":"Name","value":"allowedEntities"}},{"kind":"Field","name":{"kind":"Name","value":"options"}}]}},{"kind":"Field","name":{"kind":"Name","value":"textValue"}},{"kind":"Field","name":{"kind":"Name","value":"numberValue"}},{"kind":"Field","name":{"kind":"Name","value":"booleanValue"}},{"kind":"Field","name":{"kind":"Name","value":"dateValue"}},{"kind":"Field","name":{"kind":"Name","value":"dateTimeValue"}},{"kind":"Field","name":{"kind":"Name","value":"selectValue"}},{"kind":"Field","name":{"kind":"Name","value":"multiSelectValues"}},{"kind":"Field","name":{"kind":"Name","value":"userValue"}},{"kind":"Field","name":{"kind":"Name","value":"user"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"avatarUrl"}},{"kind":"Field","name":{"kind":"Name","value":"lastOnline"}},{"kind":"Field","name":{"kind":"Name","value":"isOnline"}}]}},{"kind":"Field","name":{"kind":"Name","value":"team"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"title"}},{"kind":"Field","name":{"kind":"Name","value":"kind"}}]}}]}}]}}]}}]} as unknown as DocumentNode; -export const GetTasksDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"GetTasks"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"rootLocationIds"}},"type":{"kind":"ListType","type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"ID"}}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"assigneeId"}},"type":{"kind":"NamedType","name":{"kind":"Name","value":"ID"}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"assigneeTeamId"}},"type":{"kind":"NamedType","name":{"kind":"Name","value":"ID"}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"filtering"}},"type":{"kind":"ListType","type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"FilterInput"}}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"sorting"}},"type":{"kind":"ListType","type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"SortInput"}}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"pagination"}},"type":{"kind":"NamedType","name":{"kind":"Name","value":"PaginationInput"}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"search"}},"type":{"kind":"NamedType","name":{"kind":"Name","value":"FullTextSearchInput"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"tasks"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"rootLocationIds"},"value":{"kind":"Variable","name":{"kind":"Name","value":"rootLocationIds"}}},{"kind":"Argument","name":{"kind":"Name","value":"assigneeId"},"value":{"kind":"Variable","name":{"kind":"Name","value":"assigneeId"}}},{"kind":"Argument","name":{"kind":"Name","value":"assigneeTeamId"},"value":{"kind":"Variable","name":{"kind":"Name","value":"assigneeTeamId"}}},{"kind":"Argument","name":{"kind":"Name","value":"filtering"},"value":{"kind":"Variable","name":{"kind":"Name","value":"filtering"}}},{"kind":"Argument","name":{"kind":"Name","value":"sorting"},"value":{"kind":"Variable","name":{"kind":"Name","value":"sorting"}}},{"kind":"Argument","name":{"kind":"Name","value":"pagination"},"value":{"kind":"Variable","name":{"kind":"Name","value":"pagination"}}},{"kind":"Argument","name":{"kind":"Name","value":"search"},"value":{"kind":"Variable","name":{"kind":"Name","value":"search"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"title"}},{"kind":"Field","name":{"kind":"Name","value":"description"}},{"kind":"Field","name":{"kind":"Name","value":"done"}},{"kind":"Field","name":{"kind":"Name","value":"dueDate"}},{"kind":"Field","name":{"kind":"Name","value":"priority"}},{"kind":"Field","name":{"kind":"Name","value":"estimatedTime"}},{"kind":"Field","name":{"kind":"Name","value":"creationDate"}},{"kind":"Field","name":{"kind":"Name","value":"updateDate"}},{"kind":"Field","name":{"kind":"Name","value":"patient"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"assignedLocation"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"title"}},{"kind":"Field","name":{"kind":"Name","value":"parent"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"title"}}]}}]}},{"kind":"Field","name":{"kind":"Name","value":"assignedLocations"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"title"}},{"kind":"Field","name":{"kind":"Name","value":"kind"}},{"kind":"Field","name":{"kind":"Name","value":"parent"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"title"}},{"kind":"Field","name":{"kind":"Name","value":"kind"}},{"kind":"Field","name":{"kind":"Name","value":"parent"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"title"}},{"kind":"Field","name":{"kind":"Name","value":"kind"}},{"kind":"Field","name":{"kind":"Name","value":"parent"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"title"}},{"kind":"Field","name":{"kind":"Name","value":"kind"}}]}}]}}]}}]}}]}},{"kind":"Field","name":{"kind":"Name","value":"assignee"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"avatarUrl"}},{"kind":"Field","name":{"kind":"Name","value":"lastOnline"}},{"kind":"Field","name":{"kind":"Name","value":"isOnline"}}]}},{"kind":"Field","name":{"kind":"Name","value":"assigneeTeam"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"title"}},{"kind":"Field","name":{"kind":"Name","value":"kind"}}]}},{"kind":"Field","name":{"kind":"Name","value":"properties"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"definition"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"description"}},{"kind":"Field","name":{"kind":"Name","value":"fieldType"}},{"kind":"Field","name":{"kind":"Name","value":"isActive"}},{"kind":"Field","name":{"kind":"Name","value":"allowedEntities"}},{"kind":"Field","name":{"kind":"Name","value":"options"}}]}},{"kind":"Field","name":{"kind":"Name","value":"textValue"}},{"kind":"Field","name":{"kind":"Name","value":"numberValue"}},{"kind":"Field","name":{"kind":"Name","value":"booleanValue"}},{"kind":"Field","name":{"kind":"Name","value":"dateValue"}},{"kind":"Field","name":{"kind":"Name","value":"dateTimeValue"}},{"kind":"Field","name":{"kind":"Name","value":"selectValue"}},{"kind":"Field","name":{"kind":"Name","value":"multiSelectValues"}},{"kind":"Field","name":{"kind":"Name","value":"userValue"}},{"kind":"Field","name":{"kind":"Name","value":"user"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"avatarUrl"}},{"kind":"Field","name":{"kind":"Name","value":"lastOnline"}},{"kind":"Field","name":{"kind":"Name","value":"isOnline"}}]}},{"kind":"Field","name":{"kind":"Name","value":"team"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"title"}},{"kind":"Field","name":{"kind":"Name","value":"kind"}}]}}]}}]}},{"kind":"Field","name":{"kind":"Name","value":"tasksTotal"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"rootLocationIds"},"value":{"kind":"Variable","name":{"kind":"Name","value":"rootLocationIds"}}},{"kind":"Argument","name":{"kind":"Name","value":"assigneeId"},"value":{"kind":"Variable","name":{"kind":"Name","value":"assigneeId"}}},{"kind":"Argument","name":{"kind":"Name","value":"assigneeTeamId"},"value":{"kind":"Variable","name":{"kind":"Name","value":"assigneeTeamId"}}},{"kind":"Argument","name":{"kind":"Name","value":"filtering"},"value":{"kind":"Variable","name":{"kind":"Name","value":"filtering"}}},{"kind":"Argument","name":{"kind":"Name","value":"sorting"},"value":{"kind":"Variable","name":{"kind":"Name","value":"sorting"}}},{"kind":"Argument","name":{"kind":"Name","value":"search"},"value":{"kind":"Variable","name":{"kind":"Name","value":"search"}}}]}]}}]} as unknown as DocumentNode; +export const GetTasksDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"GetTasks"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"rootLocationIds"}},"type":{"kind":"ListType","type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"ID"}}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"assigneeId"}},"type":{"kind":"NamedType","name":{"kind":"Name","value":"ID"}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"assigneeTeamId"}},"type":{"kind":"NamedType","name":{"kind":"Name","value":"ID"}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"filters"}},"type":{"kind":"ListType","type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"QueryFilterClauseInput"}}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"sorts"}},"type":{"kind":"ListType","type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"QuerySortClauseInput"}}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"pagination"}},"type":{"kind":"NamedType","name":{"kind":"Name","value":"PaginationInput"}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"search"}},"type":{"kind":"NamedType","name":{"kind":"Name","value":"QuerySearchInput"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"tasks"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"rootLocationIds"},"value":{"kind":"Variable","name":{"kind":"Name","value":"rootLocationIds"}}},{"kind":"Argument","name":{"kind":"Name","value":"assigneeId"},"value":{"kind":"Variable","name":{"kind":"Name","value":"assigneeId"}}},{"kind":"Argument","name":{"kind":"Name","value":"assigneeTeamId"},"value":{"kind":"Variable","name":{"kind":"Name","value":"assigneeTeamId"}}},{"kind":"Argument","name":{"kind":"Name","value":"filters"},"value":{"kind":"Variable","name":{"kind":"Name","value":"filters"}}},{"kind":"Argument","name":{"kind":"Name","value":"sorts"},"value":{"kind":"Variable","name":{"kind":"Name","value":"sorts"}}},{"kind":"Argument","name":{"kind":"Name","value":"pagination"},"value":{"kind":"Variable","name":{"kind":"Name","value":"pagination"}}},{"kind":"Argument","name":{"kind":"Name","value":"search"},"value":{"kind":"Variable","name":{"kind":"Name","value":"search"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"title"}},{"kind":"Field","name":{"kind":"Name","value":"description"}},{"kind":"Field","name":{"kind":"Name","value":"done"}},{"kind":"Field","name":{"kind":"Name","value":"dueDate"}},{"kind":"Field","name":{"kind":"Name","value":"priority"}},{"kind":"Field","name":{"kind":"Name","value":"estimatedTime"}},{"kind":"Field","name":{"kind":"Name","value":"creationDate"}},{"kind":"Field","name":{"kind":"Name","value":"updateDate"}},{"kind":"Field","name":{"kind":"Name","value":"patient"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"assignedLocation"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"title"}},{"kind":"Field","name":{"kind":"Name","value":"parent"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"title"}}]}}]}},{"kind":"Field","name":{"kind":"Name","value":"assignedLocations"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"title"}},{"kind":"Field","name":{"kind":"Name","value":"kind"}},{"kind":"Field","name":{"kind":"Name","value":"parent"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"title"}},{"kind":"Field","name":{"kind":"Name","value":"kind"}},{"kind":"Field","name":{"kind":"Name","value":"parent"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"title"}},{"kind":"Field","name":{"kind":"Name","value":"kind"}},{"kind":"Field","name":{"kind":"Name","value":"parent"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"title"}},{"kind":"Field","name":{"kind":"Name","value":"kind"}}]}}]}}]}}]}}]}},{"kind":"Field","name":{"kind":"Name","value":"assignee"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"avatarUrl"}},{"kind":"Field","name":{"kind":"Name","value":"lastOnline"}},{"kind":"Field","name":{"kind":"Name","value":"isOnline"}}]}},{"kind":"Field","name":{"kind":"Name","value":"assigneeTeam"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"title"}},{"kind":"Field","name":{"kind":"Name","value":"kind"}}]}},{"kind":"Field","name":{"kind":"Name","value":"properties"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"definition"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"description"}},{"kind":"Field","name":{"kind":"Name","value":"fieldType"}},{"kind":"Field","name":{"kind":"Name","value":"isActive"}},{"kind":"Field","name":{"kind":"Name","value":"allowedEntities"}},{"kind":"Field","name":{"kind":"Name","value":"options"}}]}},{"kind":"Field","name":{"kind":"Name","value":"textValue"}},{"kind":"Field","name":{"kind":"Name","value":"numberValue"}},{"kind":"Field","name":{"kind":"Name","value":"booleanValue"}},{"kind":"Field","name":{"kind":"Name","value":"dateValue"}},{"kind":"Field","name":{"kind":"Name","value":"dateTimeValue"}},{"kind":"Field","name":{"kind":"Name","value":"selectValue"}},{"kind":"Field","name":{"kind":"Name","value":"multiSelectValues"}},{"kind":"Field","name":{"kind":"Name","value":"userValue"}},{"kind":"Field","name":{"kind":"Name","value":"user"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"avatarUrl"}},{"kind":"Field","name":{"kind":"Name","value":"lastOnline"}},{"kind":"Field","name":{"kind":"Name","value":"isOnline"}}]}},{"kind":"Field","name":{"kind":"Name","value":"team"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"title"}},{"kind":"Field","name":{"kind":"Name","value":"kind"}}]}}]}}]}},{"kind":"Field","name":{"kind":"Name","value":"tasksTotal"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"rootLocationIds"},"value":{"kind":"Variable","name":{"kind":"Name","value":"rootLocationIds"}}},{"kind":"Argument","name":{"kind":"Name","value":"assigneeId"},"value":{"kind":"Variable","name":{"kind":"Name","value":"assigneeId"}}},{"kind":"Argument","name":{"kind":"Name","value":"assigneeTeamId"},"value":{"kind":"Variable","name":{"kind":"Name","value":"assigneeTeamId"}}},{"kind":"Argument","name":{"kind":"Name","value":"filters"},"value":{"kind":"Variable","name":{"kind":"Name","value":"filters"}}},{"kind":"Argument","name":{"kind":"Name","value":"sorts"},"value":{"kind":"Variable","name":{"kind":"Name","value":"sorts"}}},{"kind":"Argument","name":{"kind":"Name","value":"search"},"value":{"kind":"Variable","name":{"kind":"Name","value":"search"}}}]}]}}]} as unknown as DocumentNode; export const GetUserDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"GetUser"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"id"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"ID"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"user"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"id"},"value":{"kind":"Variable","name":{"kind":"Name","value":"id"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"username"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"email"}},{"kind":"Field","name":{"kind":"Name","value":"firstname"}},{"kind":"Field","name":{"kind":"Name","value":"lastname"}},{"kind":"Field","name":{"kind":"Name","value":"title"}},{"kind":"Field","name":{"kind":"Name","value":"avatarUrl"}},{"kind":"Field","name":{"kind":"Name","value":"lastOnline"}},{"kind":"Field","name":{"kind":"Name","value":"isOnline"}}]}}]}}]} as unknown as DocumentNode; export const GetUsersDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"GetUsers"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"users"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"avatarUrl"}},{"kind":"Field","name":{"kind":"Name","value":"lastOnline"}},{"kind":"Field","name":{"kind":"Name","value":"isOnline"}}]}}]}}]} as unknown as DocumentNode; export const GetGlobalDataDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"GetGlobalData"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"rootLocationIds"}},"type":{"kind":"ListType","type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"ID"}}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"me"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"username"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"firstname"}},{"kind":"Field","name":{"kind":"Name","value":"lastname"}},{"kind":"Field","name":{"kind":"Name","value":"avatarUrl"}},{"kind":"Field","name":{"kind":"Name","value":"lastOnline"}},{"kind":"Field","name":{"kind":"Name","value":"isOnline"}},{"kind":"Field","name":{"kind":"Name","value":"organizations"}},{"kind":"Field","name":{"kind":"Name","value":"rootLocations"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"title"}},{"kind":"Field","name":{"kind":"Name","value":"kind"}}]}},{"kind":"Field","name":{"kind":"Name","value":"tasks"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"rootLocationIds"},"value":{"kind":"Variable","name":{"kind":"Name","value":"rootLocationIds"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"done"}}]}}]}},{"kind":"Field","alias":{"kind":"Name","value":"wards"},"name":{"kind":"Name","value":"locationNodes"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"kind"},"value":{"kind":"EnumValue","value":"WARD"}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"title"}},{"kind":"Field","name":{"kind":"Name","value":"parentId"}}]}},{"kind":"Field","alias":{"kind":"Name","value":"teams"},"name":{"kind":"Name","value":"locationNodes"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"kind"},"value":{"kind":"EnumValue","value":"TEAM"}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"title"}},{"kind":"Field","name":{"kind":"Name","value":"parentId"}}]}},{"kind":"Field","alias":{"kind":"Name","value":"clinics"},"name":{"kind":"Name","value":"locationNodes"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"kind"},"value":{"kind":"EnumValue","value":"CLINIC"}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"title"}},{"kind":"Field","name":{"kind":"Name","value":"parentId"}}]}},{"kind":"Field","name":{"kind":"Name","value":"patients"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"rootLocationIds"},"value":{"kind":"Variable","name":{"kind":"Name","value":"rootLocationIds"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"state"}},{"kind":"Field","name":{"kind":"Name","value":"assignedLocation"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}}]}}]}},{"kind":"Field","alias":{"kind":"Name","value":"waitingPatients"},"name":{"kind":"Name","value":"patients"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"states"},"value":{"kind":"ListValue","values":[{"kind":"EnumValue","value":"WAIT"}]}},{"kind":"Argument","name":{"kind":"Name","value":"rootLocationIds"},"value":{"kind":"Variable","name":{"kind":"Name","value":"rootLocationIds"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"state"}}]}}]}}]} as unknown as DocumentNode; @@ -1081,6 +1119,7 @@ export const UpdatePropertyDefinitionDocument = {"kind":"Document","definitions" export const DeletePropertyDefinitionDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"DeletePropertyDefinition"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"id"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"ID"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"deletePropertyDefinition"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"id"},"value":{"kind":"Variable","name":{"kind":"Name","value":"id"}}}]}]}}]} as unknown as DocumentNode; export const GetPropertyDefinitionsDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"GetPropertyDefinitions"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"propertyDefinitions"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"description"}},{"kind":"Field","name":{"kind":"Name","value":"fieldType"}},{"kind":"Field","name":{"kind":"Name","value":"isActive"}},{"kind":"Field","name":{"kind":"Name","value":"allowedEntities"}},{"kind":"Field","name":{"kind":"Name","value":"options"}}]}}]}}]} as unknown as DocumentNode; export const GetPropertiesForSubjectDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"GetPropertiesForSubject"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"subjectId"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"ID"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"subjectType"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"PropertyEntity"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"propertyDefinitions"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"description"}},{"kind":"Field","name":{"kind":"Name","value":"fieldType"}},{"kind":"Field","name":{"kind":"Name","value":"isActive"}},{"kind":"Field","name":{"kind":"Name","value":"allowedEntities"}},{"kind":"Field","name":{"kind":"Name","value":"options"}}]}}]}}]} as unknown as DocumentNode; +export const QueryableFieldsDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"QueryableFields"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"entity"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"queryableFields"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"entity"},"value":{"kind":"Variable","name":{"kind":"Name","value":"entity"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"key"}},{"kind":"Field","name":{"kind":"Name","value":"label"}},{"kind":"Field","name":{"kind":"Name","value":"kind"}},{"kind":"Field","name":{"kind":"Name","value":"valueType"}},{"kind":"Field","name":{"kind":"Name","value":"allowedOperators"}},{"kind":"Field","name":{"kind":"Name","value":"sortable"}},{"kind":"Field","name":{"kind":"Name","value":"searchable"}},{"kind":"Field","name":{"kind":"Name","value":"propertyDefinitionId"}},{"kind":"Field","name":{"kind":"Name","value":"relation"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"targetEntity"}},{"kind":"Field","name":{"kind":"Name","value":"idFieldKey"}},{"kind":"Field","name":{"kind":"Name","value":"labelFieldKey"}},{"kind":"Field","name":{"kind":"Name","value":"allowedFilterModes"}}]}},{"kind":"Field","name":{"kind":"Name","value":"choice"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"optionKeys"}},{"kind":"Field","name":{"kind":"Name","value":"optionLabels"}}]}}]}}]}}]} as unknown as DocumentNode; export const PatientCreatedDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"subscription","name":{"kind":"Name","value":"PatientCreated"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"rootLocationIds"}},"type":{"kind":"ListType","type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"ID"}}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"patientCreated"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"rootLocationIds"},"value":{"kind":"Variable","name":{"kind":"Name","value":"rootLocationIds"}}}]}]}}]} as unknown as DocumentNode; export const PatientUpdatedDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"subscription","name":{"kind":"Name","value":"PatientUpdated"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"patientId"}},"type":{"kind":"NamedType","name":{"kind":"Name","value":"ID"}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"rootLocationIds"}},"type":{"kind":"ListType","type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"ID"}}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"patientUpdated"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"patientId"},"value":{"kind":"Variable","name":{"kind":"Name","value":"patientId"}}},{"kind":"Argument","name":{"kind":"Name","value":"rootLocationIds"},"value":{"kind":"Variable","name":{"kind":"Name","value":"rootLocationIds"}}}]}]}}]} as unknown as DocumentNode; export const PatientStateChangedDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"subscription","name":{"kind":"Name","value":"PatientStateChanged"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"patientId"}},"type":{"kind":"NamedType","name":{"kind":"Name","value":"ID"}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"rootLocationIds"}},"type":{"kind":"ListType","type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"ID"}}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"patientStateChanged"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"patientId"},"value":{"kind":"Variable","name":{"kind":"Name","value":"patientId"}}},{"kind":"Argument","name":{"kind":"Name","value":"rootLocationIds"},"value":{"kind":"Variable","name":{"kind":"Name","value":"rootLocationIds"}}}]}]}}]} as unknown as DocumentNode; diff --git a/web/api/graphql/GetOverviewData.graphql b/web/api/graphql/GetOverviewData.graphql index cacdfb1..b599f07 100644 --- a/web/api/graphql/GetOverviewData.graphql +++ b/web/api/graphql/GetOverviewData.graphql @@ -1,5 +1,5 @@ -query GetOverviewData($rootLocationIds: [ID!], $recentPatientsFiltering: [FilterInput!], $recentPatientsSorting: [SortInput!], $recentPatientsPagination: PaginationInput, $recentPatientsSearch: FullTextSearchInput, $recentTasksFiltering: [FilterInput!], $recentTasksSorting: [SortInput!], $recentTasksPagination: PaginationInput, $recentTasksSearch: FullTextSearchInput) { - recentPatients(rootLocationIds: $rootLocationIds, filtering: $recentPatientsFiltering, sorting: $recentPatientsSorting, pagination: $recentPatientsPagination, search: $recentPatientsSearch) { +query GetOverviewData($rootLocationIds: [ID!], $recentPatientsFilters: [QueryFilterClauseInput!], $recentPatientsSorts: [QuerySortClauseInput!], $recentPatientsPagination: PaginationInput, $recentPatientsSearch: QuerySearchInput, $recentTasksFilters: [QueryFilterClauseInput!], $recentTasksSorts: [QuerySortClauseInput!], $recentTasksPagination: PaginationInput, $recentTasksSearch: QuerySearchInput) { + recentPatients(rootLocationIds: $rootLocationIds, filters: $recentPatientsFilters, sorts: $recentPatientsSorts, pagination: $recentPatientsPagination, search: $recentPatientsSearch) { id name sex @@ -49,8 +49,8 @@ query GetOverviewData($rootLocationIds: [ID!], $recentPatientsFiltering: [Filter } } } - recentPatientsTotal(rootLocationIds: $rootLocationIds, filtering: $recentPatientsFiltering, sorting: $recentPatientsSorting, search: $recentPatientsSearch) - recentTasks(rootLocationIds: $rootLocationIds, filtering: $recentTasksFiltering, sorting: $recentTasksSorting, pagination: $recentTasksPagination, search: $recentTasksSearch) { + recentPatientsTotal(rootLocationIds: $rootLocationIds, filters: $recentPatientsFilters, sorts: $recentPatientsSorts, search: $recentPatientsSearch) + recentTasks(rootLocationIds: $rootLocationIds, filters: $recentTasksFilters, sorts: $recentTasksSorts, pagination: $recentTasksPagination, search: $recentTasksSearch) { id title description @@ -111,5 +111,5 @@ query GetOverviewData($rootLocationIds: [ID!], $recentPatientsFiltering: [Filter } } } - recentTasksTotal(rootLocationIds: $rootLocationIds, filtering: $recentTasksFiltering, sorting: $recentTasksSorting, search: $recentTasksSearch) + recentTasksTotal(rootLocationIds: $rootLocationIds, filters: $recentTasksFilters, sorts: $recentTasksSorts, search: $recentTasksSearch) } diff --git a/web/api/graphql/GetPatients.graphql b/web/api/graphql/GetPatients.graphql index 1875fa4..14ec6eb 100644 --- a/web/api/graphql/GetPatients.graphql +++ b/web/api/graphql/GetPatients.graphql @@ -1,5 +1,5 @@ -query GetPatients($locationId: ID, $rootLocationIds: [ID!], $states: [PatientState!], $filtering: [FilterInput!], $sorting: [SortInput!], $pagination: PaginationInput, $search: FullTextSearchInput) { - patients(locationNodeId: $locationId, rootLocationIds: $rootLocationIds, states: $states, filtering: $filtering, sorting: $sorting, pagination: $pagination, search: $search) { +query GetPatients($locationId: ID, $rootLocationIds: [ID!], $states: [PatientState!], $filters: [QueryFilterClauseInput!], $sorts: [QuerySortClauseInput!], $pagination: PaginationInput, $search: QuerySearchInput) { + patients(locationNodeId: $locationId, rootLocationIds: $rootLocationIds, states: $states, filters: $filters, sorts: $sorts, pagination: $pagination, search: $search) { id name firstname @@ -155,5 +155,5 @@ query GetPatients($locationId: ID, $rootLocationIds: [ID!], $states: [PatientSta } } } - patientsTotal(locationNodeId: $locationId, rootLocationIds: $rootLocationIds, states: $states, filtering: $filtering, sorting: $sorting, search: $search) + patientsTotal(locationNodeId: $locationId, rootLocationIds: $rootLocationIds, states: $states, filters: $filters, sorts: $sorts, search: $search) } diff --git a/web/api/graphql/GetTasks.graphql b/web/api/graphql/GetTasks.graphql index 7864f97..4332390 100644 --- a/web/api/graphql/GetTasks.graphql +++ b/web/api/graphql/GetTasks.graphql @@ -1,5 +1,5 @@ -query GetTasks($rootLocationIds: [ID!], $assigneeId: ID, $assigneeTeamId: ID, $filtering: [FilterInput!], $sorting: [SortInput!], $pagination: PaginationInput, $search: FullTextSearchInput) { - tasks(rootLocationIds: $rootLocationIds, assigneeId: $assigneeId, assigneeTeamId: $assigneeTeamId, filtering: $filtering, sorting: $sorting, pagination: $pagination, search: $search) { +query GetTasks($rootLocationIds: [ID!], $assigneeId: ID, $assigneeTeamId: ID, $filters: [QueryFilterClauseInput!], $sorts: [QuerySortClauseInput!], $pagination: PaginationInput, $search: QuerySearchInput) { + tasks(rootLocationIds: $rootLocationIds, assigneeId: $assigneeId, assigneeTeamId: $assigneeTeamId, filters: $filters, sorts: $sorts, pagination: $pagination, search: $search) { id title description @@ -86,6 +86,5 @@ query GetTasks($rootLocationIds: [ID!], $assigneeId: ID, $assigneeTeamId: ID, $f } } } - tasksTotal(rootLocationIds: $rootLocationIds, assigneeId: $assigneeId, assigneeTeamId: $assigneeTeamId, filtering: $filtering, sorting: $sorting, search: $search) + tasksTotal(rootLocationIds: $rootLocationIds, assigneeId: $assigneeId, assigneeTeamId: $assigneeTeamId, filters: $filters, sorts: $sorts, search: $search) } - diff --git a/web/api/graphql/QueryableFields.graphql b/web/api/graphql/QueryableFields.graphql new file mode 100644 index 0000000..8e4fe72 --- /dev/null +++ b/web/api/graphql/QueryableFields.graphql @@ -0,0 +1,22 @@ +query QueryableFields($entity: String!) { + queryableFields(entity: $entity) { + key + label + kind + valueType + allowedOperators + sortable + searchable + propertyDefinitionId + relation { + targetEntity + idFieldKey + labelFieldKey + allowedFilterModes + } + choice { + optionKeys + optionLabels + } + } +} diff --git a/web/codegen.ts b/web/codegen.ts index 92774cf..806fdff 100644 --- a/web/codegen.ts +++ b/web/codegen.ts @@ -1,9 +1,8 @@ import type { CodegenConfig } from '@graphql-codegen/cli' import 'dotenv/config' -import { getConfig } from './utils/config' const config: CodegenConfig = { - schema: getConfig().graphqlEndpoint, + schema: './schema.graphql', documents: 'api/graphql/**/*.graphql', generates: { 'api/gql/generated.ts': { diff --git a/web/components/tables/PatientList.tsx b/web/components/tables/PatientList.tsx index 081b387..479e683 100644 --- a/web/components/tables/PatientList.tsx +++ b/web/components/tables/PatientList.tsx @@ -3,8 +3,8 @@ import type { IdentifierFilterValue, FilterListItem, FilterListPopUpBuilderProps import { Chip, FillerCell, HelpwaveLogo, LoadingContainer, SearchBar, ProgressIndicator, Tooltip, Drawer, TableProvider, TableDisplay, TableColumnSwitcher, TablePagination, IconButton, useLocale, FilterList } from '@helpwave/hightide' import { PlusIcon } from 'lucide-react' import type { LocationType } from '@/api/gql/generated' -import { Sex, PatientState, type GetPatientsQuery, type TaskType, PropertyEntity, type FullTextSearchInput, FieldType } from '@/api/gql/generated' -import { usePropertyDefinitions, usePatientsPaginated, useRefreshingEntityIds } from '@/data' +import { Sex, PatientState, type GetPatientsQuery, type TaskType, PropertyEntity, FieldType } from '@/api/gql/generated' +import { usePropertyDefinitions, usePatientsPaginated, useQueryableFields, useRefreshingEntityIds } from '@/data' import { PatientDetailView } from '@/components/patients/PatientDetailView' import { LocationChips } from '@/components/locations/LocationChips' import { LocationChipsBySetting } from '@/components/patients/LocationChipsBySetting' @@ -16,7 +16,8 @@ import type { ColumnDef, Row, TableState } from '@tanstack/table-core' import { getPropertyColumnsForEntity } from '@/utils/propertyColumn' import { useStorageSyncedTableState } from '@/hooks/useTableState' import { usePropertyColumnVisibility } from '@/hooks/usePropertyColumnVisibility' -import { columnFiltersToFilterInput, paginationStateToPaginationInput, sortingStateToSortInput } from '@/utils/tableStateToApi' +import { columnFiltersToQueryFilterClauses, paginationStateToPaginationInput, sortingStateToQuerySortClauses } from '@/utils/tableStateToApi' +import { queryableFieldsToFilterListItems } from '@/utils/queryableFilterList' import { getPropertyFilterFn as getPropertyDatatype } from '@/utils/propertyFilterMapping' import { UserSelectFilterPopUp } from './UserSelectFilterPopUp' @@ -63,6 +64,7 @@ export const PatientList = forwardRef(({ initi const { selectedRootLocationIds } = useTasksContext() const { refreshingPatientIds } = useRefreshingEntityIds() const { data: propertyDefinitionsData } = usePropertyDefinitions() + const { data: queryableFieldsData } = useQueryableFields('Patient') const effectiveRootLocationIds = rootLocationIds ?? selectedRootLocationIds const [isPanelOpen, setIsPanelOpen] = useState(false) const [selectedPatient, setSelectedPatient] = useState(undefined) @@ -94,27 +96,28 @@ export const PatientList = forwardRef(({ initi PatientState.Wait, ], []) - const apiFiltering = useMemo(() => columnFiltersToFilterInput(filters), [filters]) + const apiFilters = useMemo(() => columnFiltersToQueryFilterClauses(filters), [filters]) const patientStates = useMemo(() => { - const stateFilter = apiFiltering.find( - f => f.column === 'state' && - (f.operator === 'TAGS_SINGLE_EQUALS' || f.operator === 'TAGS_SINGLE_CONTAINS') && - f.parameter?.searchTags != null && - f.parameter.searchTags.length > 0 - ) - if (!stateFilter?.parameter?.searchTags) return allPatientStates + const stateFilter = apiFilters.find(f => f.fieldKey === 'state') + if (!stateFilter?.value) return allPatientStates + const raw = stateFilter.value.stringValues?.length + ? stateFilter.value.stringValues + : stateFilter.value.stringValue + ? [stateFilter.value.stringValue] + : [] + if (raw.length === 0) return allPatientStates const allowed = new Set(allPatientStates as unknown as string[]) - const filtered = (stateFilter.parameter.searchTags as string[]).filter(s => allowed.has(s)) + const filtered = raw.filter(s => allowed.has(s)) return filtered.length > 0 ? (filtered as PatientState[]) : allPatientStates - }, [apiFiltering, allPatientStates]) + }, [apiFilters, allPatientStates]) - const searchInput: FullTextSearchInput | undefined = searchQuery + const searchInput = searchQuery ? { searchText: searchQuery, includeProperties: true, } : undefined - const apiSorting = useMemo(() => sortingStateToSortInput(sorting), [sorting]) + const apiSorting = useMemo(() => sortingStateToQuerySortClauses(sorting), [sorting]) const apiPagination = useMemo(() => paginationStateToPaginationInput(pagination), [pagination]) const lastTotalCountRef = useRef(undefined) @@ -123,12 +126,12 @@ export const PatientList = forwardRef(({ initi locationId: locationId || undefined, rootLocationIds: !locationId && effectiveRootLocationIds && effectiveRootLocationIds.length > 0 ? effectiveRootLocationIds : undefined, states: patientStates, - search: searchInput, }, { pagination: apiPagination, - sorting: apiSorting.length > 0 ? apiSorting : undefined, - filtering: apiFiltering.length > 0 ? apiFiltering : undefined, + sorts: apiSorting.length > 0 ? apiSorting : undefined, + filters: apiFilters.length > 0 ? apiFilters : undefined, + search: searchInput, } ) if (totalCount != null) lastTotalCountRef.current = totalCount @@ -393,61 +396,72 @@ export const PatientList = forwardRef(({ initi })), ], [translation, allPatientStates, patientPropertyColumns, refreshingPatientIds, rowLoadingCell, dateFormat]) - const availableFilters: FilterListItem[] = useMemo(() => [ - { - id: 'name', - label: translation('name'), - dataType: 'text', - tags: [], - }, - { - id: 'state', - label: translation('status'), - dataType: 'singleTag', - tags: allPatientStates.map(state => ({ label: translation('patientState', { state: state as string }), tag: state })), - }, - { - id: 'sex', - label: translation('sex'), - dataType: 'singleTag', - tags: [ - { label: translation('male'), tag: Sex.Male }, - { label: translation('female'), tag: Sex.Female }, - { label: translation('diverse'), tag: Sex.Unknown }, - ], - }, - ...(['CLINIC', 'WARD', 'ROOM', 'BED'] as const).map((kind): FilterListItem => ({ - id: `location-${kind}`, - label: translation(LOCATION_KIND_HEADERS[kind] as 'locationClinic' | 'locationWard' | 'locationRoom' | 'locationBed'), - dataType: 'text', - tags: [], - })), - { - id: 'birthdate', - label: translation('birthdate'), - dataType: 'date', - tags: [], - }, - { - id: 'tasks', - label: translation('tasks'), - dataType: 'number', - tags: [], - }, - ...propertyDefinitionsData?.propertyDefinitions.map(def => { - const dataType = getPropertyDatatype(def.fieldType) - return { - id: `property_${def.id}`, - label: def.name, - dataType, - tags: def.options.map((opt, idx) => ({ - label: opt, - tag: `${def.id}-opt-${idx}`, - })), - popUpBuilder: def.fieldType === FieldType.FieldTypeUser ? (props: FilterListPopUpBuilderProps) => () : undefined, - } - }) ?? [], - ], [allPatientStates, propertyDefinitionsData?.propertyDefinitions, translation]) + const propertyFieldTypeByDefId = useMemo( + () => new Map(propertyDefinitionsData?.propertyDefinitions.map(d => [d.id, d.fieldType]) ?? []), + [propertyDefinitionsData] + ) + + const availableFilters: FilterListItem[] = useMemo(() => { + const raw = queryableFieldsData?.queryableFields + if (raw?.length) { + return queryableFieldsToFilterListItems(raw, propertyFieldTypeByDefId) + } + return [ + { + id: 'name', + label: translation('name'), + dataType: 'text', + tags: [], + }, + { + id: 'state', + label: translation('status'), + dataType: 'singleTag', + tags: allPatientStates.map(state => ({ label: translation('patientState', { state: state as string }), tag: state })), + }, + { + id: 'sex', + label: translation('sex'), + dataType: 'singleTag', + tags: [ + { label: translation('male'), tag: Sex.Male }, + { label: translation('female'), tag: Sex.Female }, + { label: translation('diverse'), tag: Sex.Unknown }, + ], + }, + ...(['CLINIC', 'WARD', 'ROOM', 'BED'] as const).map((kind): FilterListItem => ({ + id: `location-${kind}`, + label: translation(LOCATION_KIND_HEADERS[kind] as 'locationClinic' | 'locationWard' | 'locationRoom' | 'locationBed'), + dataType: 'text', + tags: [], + })), + { + id: 'birthdate', + label: translation('birthdate'), + dataType: 'date', + tags: [], + }, + { + id: 'tasks', + label: translation('tasks'), + dataType: 'number', + tags: [], + }, + ...propertyDefinitionsData?.propertyDefinitions.map(def => { + const dataType = getPropertyDatatype(def.fieldType) + return { + id: `property_${def.id}`, + label: def.name, + dataType, + tags: def.options.map((opt, idx) => ({ + label: opt, + tag: `${def.id}-opt-${idx}`, + })), + popUpBuilder: def.fieldType === FieldType.FieldTypeUser ? (props: FilterListPopUpBuilderProps) => () : undefined, + } + }) ?? [], + ] + }, [queryableFieldsData?.queryableFields, propertyFieldTypeByDefId, translation, allPatientStates, propertyDefinitionsData?.propertyDefinitions]) const onRowClick = useCallback((row: Row) => handleEdit(row.original), [handleEdit]) const fillerRowCell = useCallback(() => (), []) @@ -474,6 +488,7 @@ export const PatientList = forwardRef(({ initi onSortingChange={setSorting} onColumnFiltersChange={setFilters} enableMultiSort={true} + enablePinning={false} pageCount={stableTotalCount != null ? Math.ceil(stableTotalCount / pagination.pageSize) : -1} >
diff --git a/web/components/tables/TaskList.tsx b/web/components/tables/TaskList.tsx index d269ebf..dc46564 100644 --- a/web/components/tables/TaskList.tsx +++ b/web/components/tables/TaskList.tsx @@ -1,10 +1,12 @@ import { useMemo, useState, forwardRef, useImperativeHandle, useEffect, useRef, useCallback } from 'react' import { useQueryClient } from '@tanstack/react-query' -import { Button, Checkbox, ConfirmDialog, FillerCell, HelpwaveLogo, IconButton, LoadingContainer, SearchBar, Select, SelectOption, TableColumnSwitcher, TableDisplay, TablePagination, TableProvider } from '@helpwave/hightide' +import type { FilterListItem } from '@helpwave/hightide' +import { Button, Checkbox, ConfirmDialog, FilterList, FillerCell, HelpwaveLogo, IconButton, LoadingContainer, SearchBar, Select, SelectOption, TableColumnSwitcher, TableDisplay, TablePagination, TableProvider } from '@helpwave/hightide' import { PlusIcon, UserCheck, Users } from 'lucide-react' +import type { IdentifierFilterValue } from '@helpwave/hightide' import type { TaskPriority, GetTasksQuery } from '@/api/gql/generated' import { PropertyEntity } from '@/api/gql/generated' -import { useAssignTask, useAssignTaskToTeam, useCompleteTask, useReopenTask, useUsers, useLocations, usePropertyDefinitions, useRefreshingEntityIds } from '@/data' +import { useAssignTask, useAssignTaskToTeam, useCompleteTask, useReopenTask, useUsers, useLocations, usePropertyDefinitions, useQueryableFields, useRefreshingEntityIds } from '@/data' import { AssigneeSelectDialog } from '@/components/tasks/AssigneeSelectDialog' import clsx from 'clsx' import { DateDisplay } from '@/components/Date/DateDisplay' @@ -22,6 +24,7 @@ import { PriorityUtils } from '@/utils/priority' import { getPropertyColumnsForEntity } from '@/utils/propertyColumn' import { useStorageSyncedTableState } from '@/hooks/useTableState' import { usePropertyColumnVisibility } from '@/hooks/usePropertyColumnVisibility' +import { queryableFieldsToFilterListItems } from '@/utils/queryableFilterList' export type TaskViewModel = { id: string, @@ -78,11 +81,14 @@ type TaskListProps = { loading?: boolean, showAllTasksMode?: boolean, tableState?: TaskListTableState, + searchQuery?: string, + onSearchQueryChange?: (value: string) => void, } -export const TaskList = forwardRef(({ tasks: initialTasks, onRefetch, showAssignee = false, initialTaskId, onInitialTaskOpened, headerActions, totalCount, loading = false, showAllTasksMode = false, tableState: controlledTableState }, ref) => { +export const TaskList = forwardRef(({ tasks: initialTasks, onRefetch, showAssignee = false, initialTaskId, onInitialTaskOpened, headerActions, totalCount, loading = false, showAllTasksMode = false, tableState: controlledTableState, searchQuery: searchQueryProp, onSearchQueryChange }, ref) => { const translation = useTasksTranslation() const { data: propertyDefinitionsData } = usePropertyDefinitions() + const { data: queryableFieldsData } = useQueryableFields('Task') const internalState = useStorageSyncedTableState('task-list', { defaultSorting: useMemo(() => [ @@ -154,7 +160,9 @@ export const TaskList = forwardRef(({ tasks: initial const [selectedPatientId, setSelectedPatientId] = useState(null) const [selectedUserPopupId, setSelectedUserPopupId] = useState(null) const [taskDialogState, setTaskDialogState] = useState({ isOpen: false }) - const [searchQuery, setSearchQuery] = useState('') + const [internalSearchQuery, setInternalSearchQuery] = useState('') + const searchQuery = searchQueryProp !== undefined ? searchQueryProp : internalSearchQuery + const setSearchQuery = onSearchQueryChange ?? setInternalSearchQuery const [openedTaskId, setOpenedTaskId] = useState(null) const [isHandoverDialogOpen, setIsHandoverDialogOpen] = useState(false) const [selectedUserId, setSelectedUserId] = useState(null) @@ -201,6 +209,8 @@ export const TaskList = forwardRef(({ tasks: initial }) }, [initialTasks]) + const isServerDriven = totalCount != null + const tasks = useMemo(() => { let data = initialTasks.map(task => { const optimisticDone = optimisticUpdates.get(task.id) @@ -210,25 +220,28 @@ export const TaskList = forwardRef(({ tasks: initial return task }) - if (searchQuery) { + if (!isServerDriven && searchQuery) { const lowerQuery = searchQuery.toLowerCase() data = data.filter(t => t.name.toLowerCase().includes(lowerQuery) || - t.patient?.name.toLowerCase().includes(lowerQuery)) + (t.patient?.name.toLowerCase().includes(lowerQuery) ?? false)) } - return [...data].sort((a, b) => { - if (a.done !== b.done) { - return a.done ? 1 : -1 - } + if (!isServerDriven) { + return [...data].sort((a, b) => { + if (a.done !== b.done) { + return a.done ? 1 : -1 + } - if (!a.dueDate && !b.dueDate) return 0 - if (!a.dueDate) return 1 - if (!b.dueDate) return -1 + if (!a.dueDate && !b.dueDate) return 0 + if (!a.dueDate) return 1 + if (!b.dueDate) return -1 - return a.dueDate.getTime() - b.dueDate.getTime() - }) - }, [initialTasks, optimisticUpdates, searchQuery]) + return a.dueDate.getTime() - b.dueDate.getTime() + }) + } + return data + }, [initialTasks, optimisticUpdates, searchQuery, isServerDriven]) const openTasks = useMemo(() => { @@ -330,6 +343,32 @@ export const TaskList = forwardRef(({ tasks: initial [propertyDefinitionsData] ) + const propertyFieldTypeByDefId = useMemo( + () => new Map(propertyDefinitionsData?.propertyDefinitions.map(d => [d.id, d.fieldType]) ?? []), + [propertyDefinitionsData] + ) + + const availableFilters: FilterListItem[] = useMemo(() => { + const raw = queryableFieldsData?.queryableFields + if (raw?.length) { + return queryableFieldsToFilterListItems(raw, propertyFieldTypeByDefId) + } + return [ + { id: 'title', label: translation('title'), dataType: 'text', tags: [] }, + { id: 'description', label: translation('description'), dataType: 'text', tags: [] }, + { id: 'done', label: translation('done'), dataType: 'boolean', tags: [] }, + { id: 'dueDate', label: translation('dueDate'), dataType: 'date', tags: [] }, + { id: 'priority', label: translation('priority'), dataType: 'singleTag', tags: ['P1', 'P2', 'P3', 'P4'].map(p => ({ label: p, tag: p })) }, + { id: 'patient', label: translation('patient'), dataType: 'text', tags: [] }, + { id: 'assignee', label: translation('assignedTo'), dataType: 'text', tags: [] }, + ...propertyDefinitionsData?.propertyDefinitions.map(def => ({ + id: `property_${def.id}`, + label: def.name, + dataType: 'text' as const, + tags: def.options.map((opt, idx) => ({ label: opt, tag: `${def.id}-opt-${idx}` })), + })) ?? [], + ] + }, [queryableFieldsData?.queryableFields, propertyFieldTypeByDefId, translation, propertyDefinitionsData?.propertyDefinitions]) const rowLoadingCell = useMemo(() => , []) @@ -577,6 +616,11 @@ export const TaskList = forwardRef(({ tasks: initial onSearch={() => null} containerProps={{ className: 'max-w-80' }} /> + { setFiltersNormalized(value) }} + availableItems={availableFilters} + />
diff --git a/web/data/cache/policies.ts b/web/data/cache/policies.ts index b0025b9..7d17908 100644 --- a/web/data/cache/policies.ts +++ b/web/data/cache/policies.ts @@ -12,11 +12,11 @@ export function buildCacheConfig(): InMemoryCacheConfig { fields: { task: { keyArgs: ['id'] }, tasks: { - keyArgs: ['rootLocationIds', 'assigneeId', 'assigneeTeamId', 'filtering', 'sorting', 'search', 'pagination'], + keyArgs: ['rootLocationIds', 'assigneeId', 'assigneeTeamId', 'filters', 'sorts', 'search', 'pagination'], }, patient: { keyArgs: ['id'] }, patients: { - keyArgs: ['locationId', 'rootLocationIds', 'states', 'filtering', 'sorting', 'search', 'pagination'], + keyArgs: ['locationId', 'rootLocationIds', 'states', 'filters', 'sorts', 'search', 'pagination'], merge: (_existing, incoming) => incoming, }, locationNode: { keyArgs: ['id'] }, diff --git a/web/data/hooks/index.ts b/web/data/hooks/index.ts index c0db2b5..88e1e7f 100644 --- a/web/data/hooks/index.ts +++ b/web/data/hooks/index.ts @@ -16,6 +16,7 @@ export { usePropertiesForSubject } from './usePropertiesForSubject' export { useMyTasks } from './useMyTasks' export { useTasksPaginated } from './useTasksPaginated' export { usePatientsPaginated } from './usePatientsPaginated' +export { useQueryableFields } from './useQueryableFields' export { useCreateTask } from './useCreateTask' export { useUpdateTask } from './useUpdateTask' export { useDeleteTask } from './useDeleteTask' diff --git a/web/data/hooks/usePaginatedEntityQuery.ts b/web/data/hooks/usePaginatedEntityQuery.ts index 98bf669..bbdb148 100644 --- a/web/data/hooks/usePaginatedEntityQuery.ts +++ b/web/data/hooks/usePaginatedEntityQuery.ts @@ -1,11 +1,12 @@ import { useCallback, useMemo } from 'react' import { useQueryWhenReady } from './queryHelpers' -import type { FilterInput, SortInput } from '@/api/gql/generated' +import type { QueryFilterClauseInput, QuerySearchInput, QuerySortClauseInput } from '@/api/gql/generated' export type UsePaginatedEntityQueryOptions = { pagination: { pageIndex: number, pageSize: number }, - sorting?: SortInput[], - filtering?: FilterInput[], + sorts?: QuerySortClauseInput[], + filters?: QueryFilterClauseInput[], + search?: QuerySearchInput, getPageDataKey?: (data: TQueryData | undefined) => string, } @@ -19,8 +20,9 @@ export type UsePaginatedEntityQueryResult = { type VariablesWithPagination = { pagination: { pageIndex: number, pageSize: number }, - sorting?: SortInput[], - filtering?: FilterInput[], + sorts?: QuerySortClauseInput[], + filters?: QueryFilterClauseInput[], + search?: QuerySearchInput, } export function usePaginatedEntityQuery< @@ -34,13 +36,14 @@ export function usePaginatedEntityQuery< extractItems: (data: TQueryData | undefined) => TItem[], extractTotal: (data: TQueryData | undefined) => number | undefined ): UsePaginatedEntityQueryResult { - const { pagination, sorting, filtering } = options + const { pagination, sorts, filters, search } = options const variablesWithPagination = useMemo(() => ({ ...(variables ?? {}), pagination: { pageIndex: pagination.pageIndex, pageSize: pagination.pageSize }, - ...(sorting != null && sorting.length > 0 ? { sorting } : {}), - ...(filtering != null && filtering.length > 0 ? { filtering } : {}), - }), [variables, pagination.pageIndex, pagination.pageSize, sorting, filtering]) + ...(sorts != null && sorts.length > 0 ? { sorts } : {}), + ...(filters != null && filters.length > 0 ? { filters } : {}), + ...(search != null && search.searchText ? { search } : {}), + }), [variables, pagination.pageIndex, pagination.pageSize, sorts, filters, search]) const variablesTyped = variablesWithPagination as TVariables & VariablesWithPagination const result = useQueryWhenReady( document, diff --git a/web/data/hooks/usePatientsPaginated.ts b/web/data/hooks/usePatientsPaginated.ts index 210258d..2258866 100644 --- a/web/data/hooks/usePatientsPaginated.ts +++ b/web/data/hooks/usePatientsPaginated.ts @@ -3,13 +3,14 @@ import { type GetPatientsQuery, type GetPatientsQueryVariables } from '@/api/gql/generated' -import type { FilterInput, SortInput } from '@/api/gql/generated' +import type { QueryFilterClauseInput, QuerySortClauseInput, QuerySearchInput } from '@/api/gql/generated' import { usePaginatedEntityQuery } from './usePaginatedEntityQuery' export type UsePatientsPaginatedOptions = { pagination: { pageIndex: number, pageSize: number }, - sorting?: SortInput[], - filtering?: FilterInput[], + sorts?: QuerySortClauseInput[], + filters?: QueryFilterClauseInput[], + search?: QuerySearchInput, } export type UsePatientsPaginatedResult = { @@ -45,8 +46,9 @@ export function usePatientsPaginated( variables, { pagination: options.pagination, - sorting: options.sorting, - filtering: options.filtering, + sorts: options.sorts, + filters: options.filters, + search: options.search, getPageDataKey, }, (data) => data?.patients ?? [], diff --git a/web/data/hooks/useQueryableFields.ts b/web/data/hooks/useQueryableFields.ts new file mode 100644 index 0000000..7848408 --- /dev/null +++ b/web/data/hooks/useQueryableFields.ts @@ -0,0 +1,14 @@ +import { useQueryWhenReady } from './queryHelpers' +import { + QueryableFieldsDocument, + type QueryableFieldsQuery, + type QueryableFieldsQueryVariables +} from '@/api/gql/generated' + +export function useQueryableFields(entity: string) { + return useQueryWhenReady( + QueryableFieldsDocument, + { entity }, + { fetchPolicy: 'cache-first' } + ) +} diff --git a/web/data/hooks/useTasksPaginated.ts b/web/data/hooks/useTasksPaginated.ts index 2b5c853..5fa1db9 100644 --- a/web/data/hooks/useTasksPaginated.ts +++ b/web/data/hooks/useTasksPaginated.ts @@ -3,13 +3,14 @@ import { type GetTasksQuery, type GetTasksQueryVariables } from '@/api/gql/generated' -import type { FilterInput, SortInput } from '@/api/gql/generated' +import type { QueryFilterClauseInput, QuerySortClauseInput, QuerySearchInput } from '@/api/gql/generated' import { usePaginatedEntityQuery } from './usePaginatedEntityQuery' export type UseTasksPaginatedOptions = { pagination: { pageIndex: number, pageSize: number }, - sorting?: SortInput[], - filtering?: FilterInput[], + sorts?: QuerySortClauseInput[], + filters?: QueryFilterClauseInput[], + search?: QuerySearchInput, } export type UseTasksPaginatedResult = { @@ -33,8 +34,9 @@ export function useTasksPaginated( variables, { pagination: options.pagination, - sorting: options.sorting, - filtering: options.filtering, + sorts: options.sorts, + filters: options.filters, + search: options.search, }, (data) => data?.tasks ?? [], (data) => data?.tasksTotal diff --git a/web/data/index.ts b/web/data/index.ts index 6ce1ae9..de836f3 100644 --- a/web/data/index.ts +++ b/web/data/index.ts @@ -50,6 +50,7 @@ export { useMyTasks, useTasksPaginated, usePatientsPaginated, + useQueryableFields, useCreateTask, useUpdateTask, useDeleteTask, diff --git a/web/package-lock.json b/web/package-lock.json index a745f64..ef833b2 100644 --- a/web/package-lock.json +++ b/web/package-lock.json @@ -153,6 +153,7 @@ "integrity": "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@babel/code-frame": "^7.29.0", "@babel/generator": "^7.29.0", @@ -415,6 +416,7 @@ "resolved": "https://registry.npmjs.org/@dnd-kit/core/-/core-6.3.1.tgz", "integrity": "sha512-xkGBRQQab4RLwgXxoqETICr6S5JlogafbhNsidmrkVv2YRs5MLwpjoF2qpiGjQt8S9AoxtIV603s0GIUpY5eYQ==", "license": "MIT", + "peer": true, "dependencies": { "@dnd-kit/accessibility": "^3.1.1", "@dnd-kit/utilities": "^3.2.2", @@ -3608,6 +3610,7 @@ "integrity": "sha512-6TyEnHgd6SArQO8UO2OMTxshln3QMWBtPGrOCgs3wVEmQmwyuNtB10IZMfmYDE0riwNR1cu4q+pPcxMVtaG3TA==", "devOptional": true, "license": "Apache-2.0", + "peer": true, "dependencies": { "playwright": "1.57.0" }, @@ -4442,6 +4445,7 @@ "resolved": "https://registry.npmjs.org/@tanstack/react-query/-/react-query-5.90.16.tgz", "integrity": "sha512-bpMGOmV4OPmif7TNMteU/Ehf/hoC0Kf98PDc0F4BZkFrEapRMEqI/V6YS0lyzwSV6PQpY1y4xxArUIfBW5LVxQ==", "license": "MIT", + "peer": true, "dependencies": { "@tanstack/query-core": "5.90.16" }, @@ -4560,6 +4564,7 @@ "integrity": "sha512-MWtvHrGZLFttgeEj28VXHxpmwYbor/ATPYbBfSFZEIRK0ecCFLl2Qo55z52Hss+UV9CRN7trSeq1zbgx7YDWWg==", "devOptional": true, "license": "MIT", + "peer": true, "dependencies": { "csstype": "^3.2.2" } @@ -4629,6 +4634,7 @@ "integrity": "sha512-4z2nCSBfVIMnbuu8uinj+f0o4qOeggYJLbjpPHka3KH1om7e+H9yLKTYgksTaHcGco+NClhhY2vyO3HsMH1RGw==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@typescript-eslint/scope-manager": "8.55.0", "@typescript-eslint/types": "8.55.0", @@ -4938,6 +4944,7 @@ "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", "dev": true, "license": "MIT", + "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -5313,6 +5320,7 @@ } ], "license": "MIT", + "peer": true, "dependencies": { "baseline-browser-mapping": "^2.9.0", "caniuse-lite": "^1.0.30001759", @@ -6282,6 +6290,7 @@ "integrity": "sha512-BhHmn2yNOFA9H9JmmIVKJmd288g9hrVRDkdoIgRCRuSySRUHH7r/DI6aAXW9T1WwUuY3DFgrcaqB+deURBLR5g==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.1", @@ -7015,6 +7024,7 @@ "resolved": "https://registry.npmjs.org/graphql/-/graphql-16.12.0.tgz", "integrity": "sha512-DKKrynuQRne0PNpEbzuEdHlYOMksHSUI8Zc9Unei5gTsMNA2/vMpoMz/yKba50pejK56qj98qM0SjYxAKi13gQ==", "license": "MIT", + "peer": true, "engines": { "node": "^12.22.0 || ^14.16.0 || ^16.0.0 || >=17.0.0" } @@ -7325,6 +7335,7 @@ "resolved": "https://registry.npmjs.org/graphql-ws/-/graphql-ws-6.0.6.tgz", "integrity": "sha512-zgfER9s+ftkGKUZgc0xbx8T7/HMO4AV5/YuYiFc+AtgcO5T0v8AxYYNQ+ltzuzDZgNkYJaFspm5MMYLjQzrkmw==", "license": "MIT", + "peer": true, "engines": { "node": ">=20" }, @@ -9545,6 +9556,7 @@ } ], "license": "MIT", + "peer": true, "dependencies": { "nanoid": "^3.3.8", "picocolors": "^1.1.1", @@ -9619,6 +9631,7 @@ "resolved": "https://registry.npmjs.org/react/-/react-19.2.3.tgz", "integrity": "sha512-Ku/hhYbVjOQnXDZFv2+RibmLFGwFdeeKHFcOTlrt7xplBnya5OGn/hIRDsqDiSUcfORsDC7MPxwork8jBwsIWA==", "license": "MIT", + "peer": true, "engines": { "node": ">=0.10.0" } @@ -9628,6 +9641,7 @@ "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.3.tgz", "integrity": "sha512-yELu4WmLPw5Mr/lmeEpox5rw3RETacE++JgHqQzd2dg+YbJuat3jH4ingc+WPZhxaoFzdv9y33G+F7Nl5O0GBg==", "license": "MIT", + "peer": true, "dependencies": { "scheduler": "^0.27.0" }, @@ -10656,6 +10670,7 @@ "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.7.2.tgz", "integrity": "sha512-i5t66RHxDvVN40HfDd1PsEThGNnlMCMT3jMUuoh9/0TaqWevNontacunWyN02LA9/fIbEWlcHZcgTKb9QoaLfg==", "license": "Apache-2.0", + "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -11014,6 +11029,7 @@ "integrity": "sha512-blAT2mjOEIi0ZzruJfIhb3nps74PRWTCz1IjglWEEpQl5XS/UNama6u2/rjFkDDouqr4L67ry+1aGIALViWjDg==", "devOptional": true, "license": "MIT", + "peer": true, "engines": { "node": ">=10.0.0" }, diff --git a/web/pages/tasks/index.tsx b/web/pages/tasks/index.tsx index d7a1ab2..ca57144 100644 --- a/web/pages/tasks/index.tsx +++ b/web/pages/tasks/index.tsx @@ -5,12 +5,12 @@ import { useTasksTranslation } from '@/i18n/useTasksTranslation' import { ContentPanel } from '@/components/layout/ContentPanel' import type { TaskViewModel } from '@/components/tables/TaskList' import { TaskList } from '@/components/tables/TaskList' -import { useMemo } from 'react' +import { useMemo, useState } from 'react' import { useRouter } from 'next/router' import { useTasksContext } from '@/hooks/useTasksContext' import { useTasksPaginated } from '@/data' import { useStorageSyncedTableState } from '@/hooks/useTableState' -import { columnFiltersToFilterInput, paginationStateToPaginationInput, sortingStateToSortInput } from '@/utils/tableStateToApi' +import { columnFiltersToQueryFilterClauses, paginationStateToPaginationInput, sortingStateToQuerySortClauses } from '@/utils/tableStateToApi' const TasksPage: NextPage = () => { const translation = useTasksTranslation() @@ -32,9 +32,13 @@ const TasksPage: NextPage = () => { ], []), }) - const apiFiltering = useMemo(() => columnFiltersToFilterInput(filters, 'task'), [filters]) - const apiSorting = useMemo(() => sortingStateToSortInput(sorting, 'task'), [sorting]) + const [searchQuery, setSearchQuery] = useState('') + const apiFilters = useMemo(() => columnFiltersToQueryFilterClauses(filters), [filters]) + const apiSorting = useMemo(() => sortingStateToQuerySortClauses(sorting), [sorting]) const apiPagination = useMemo(() => paginationStateToPaginationInput(pagination), [pagination]) + const searchInput = searchQuery + ? { searchText: searchQuery, includeProperties: true } + : undefined const { data: tasksData, refetch, totalCount, loading: tasksLoading } = useTasksPaginated( !!selectedRootLocationIds && !!user @@ -42,8 +46,9 @@ const TasksPage: NextPage = () => { : undefined, { pagination: apiPagination, - sorting: apiSorting.length > 0 ? apiSorting : undefined, - filtering: apiFiltering.length > 0 ? apiFiltering : undefined, + sorts: apiSorting.length > 0 ? apiSorting : undefined, + filters: apiFilters.length > 0 ? apiFilters : undefined, + search: searchInput, } ) const taskId = router.query['taskId'] as string | undefined @@ -88,6 +93,8 @@ const TasksPage: NextPage = () => { onInitialTaskOpened={() => router.replace('/tasks', undefined, { shallow: true })} totalCount={totalCount} loading={tasksLoading} + searchQuery={searchQuery} + onSearchQueryChange={setSearchQuery} tableState={{ pagination, setPagination, diff --git a/web/schema.graphql b/web/schema.graphql new file mode 100644 index 0000000..aa8eae0 --- /dev/null +++ b/web/schema.graphql @@ -0,0 +1,434 @@ +type AuditLogType { + caseId: String! + activity: String! + userId: String + timestamp: DateTime! + context: String +} + +input CreateLocationNodeInput { + title: String! + kind: LocationType! + parentId: ID = null +} + +input CreatePatientInput { + firstname: String! + lastname: String! + birthdate: Date! + sex: Sex! + assignedLocationId: ID = null + assignedLocationIds: [ID!] = null + clinicId: ID! + positionId: ID = null + teamIds: [ID!] = null + properties: [PropertyValueInput!] = null + state: PatientState = null + description: String = null +} + +input CreatePropertyDefinitionInput { + name: String! + fieldType: FieldType! + allowedEntities: [PropertyEntity!]! + description: String = null + options: [String!] = null + isActive: Boolean! = true +} + +input CreateTaskInput { + title: String! + patientId: ID! + description: String = null + dueDate: DateTime = null + assigneeId: ID = null + assigneeTeamId: ID = null + previousTaskIds: [ID!] = null + properties: [PropertyValueInput!] = null + priority: TaskPriority = null + estimatedTime: Int = null +} + +"""Date (isoformat)""" +scalar Date + +"""Date with time (isoformat)""" +scalar DateTime + +enum FieldType { + FIELD_TYPE_UNSPECIFIED + FIELD_TYPE_TEXT + FIELD_TYPE_NUMBER + FIELD_TYPE_CHECKBOX + FIELD_TYPE_DATE + FIELD_TYPE_DATE_TIME + FIELD_TYPE_SELECT + FIELD_TYPE_MULTI_SELECT + FIELD_TYPE_USER +} + +type LocationNodeType { + id: ID! + title: String! + kind: LocationType! + parentId: ID + parent: LocationNodeType + children: [LocationNodeType!]! + patients: [PatientType!]! + organizationIds: [String!]! +} + +enum LocationType { + HOSPITAL + PRACTICE + CLINIC + TEAM + WARD + ROOM + BED + OTHER +} + +type Mutation { + createPatient(data: CreatePatientInput!): PatientType! + updatePatient(id: ID!, data: UpdatePatientInput!): PatientType! + deletePatient(id: ID!): Boolean! + admitPatient(id: ID!): PatientType! + dischargePatient(id: ID!): PatientType! + markPatientDead(id: ID!): PatientType! + waitPatient(id: ID!): PatientType! + createTask(data: CreateTaskInput!): TaskType! + updateTask(id: ID!, data: UpdateTaskInput!): TaskType! + assignTask(id: ID!, userId: ID!): TaskType! + unassignTask(id: ID!): TaskType! + assignTaskToTeam(id: ID!, teamId: ID!): TaskType! + unassignTaskFromTeam(id: ID!): TaskType! + completeTask(id: ID!): TaskType! + reopenTask(id: ID!): TaskType! + deleteTask(id: ID!): Boolean! + createPropertyDefinition(data: CreatePropertyDefinitionInput!): PropertyDefinitionType! + updatePropertyDefinition(id: ID!, data: UpdatePropertyDefinitionInput!): PropertyDefinitionType! + deletePropertyDefinition(id: ID!): Boolean! + createLocationNode(data: CreateLocationNodeInput!): LocationNodeType! + updateLocationNode(id: ID!, data: UpdateLocationNodeInput!): LocationNodeType! + deleteLocationNode(id: ID!): Boolean! + updateProfilePicture(data: UpdateProfilePictureInput!): UserType! +} + +input PaginationInput { + pageIndex: Int! = 0 + pageSize: Int = null +} + +enum PatientState { + WAIT + ADMITTED + DISCHARGED + DEAD +} + +type PatientType { + id: ID! + firstname: String! + lastname: String! + birthdate: Date! + sex: Sex! + state: PatientState! + assignedLocationId: ID + clinicId: ID! + positionId: ID + description: String + name: String! + age: Int! + assignedLocation: LocationNodeType + assignedLocations: [LocationNodeType!]! + clinic: LocationNodeType! + position: LocationNodeType + teams: [LocationNodeType!]! + tasks(done: Boolean = null): [TaskType!]! + properties: [PropertyValueType!]! + checksum: String! +} + +type PropertyDefinitionType { + id: ID! + name: String! + description: String + fieldType: FieldType! + isActive: Boolean! + options: [String!]! + allowedEntities: [PropertyEntity!]! +} + +enum PropertyEntity { + PATIENT + TASK +} + +input PropertyValueInput { + definitionId: ID! + textValue: String = null + numberValue: Float = null + booleanValue: Boolean = null + dateValue: Date = null + dateTimeValue: DateTime = null + selectValue: String = null + multiSelectValues: [String!] = null + userValue: String = null +} + +type PropertyValueType { + id: ID! + definition: PropertyDefinitionType! + textValue: String + numberValue: Float + booleanValue: Boolean + dateValue: Date + dateTimeValue: DateTime + selectValue: String + userValue: String + multiSelectValues: [String!] + user: UserType + team: LocationNodeType +} + +type Query { + patient(id: ID!): PatientType + patients(locationNodeId: ID = null, rootLocationIds: [ID!] = null, states: [PatientState!] = null, filters: [QueryFilterClauseInput!] = null, sorts: [QuerySortClauseInput!] = null, pagination: PaginationInput = null, search: QuerySearchInput = null): [PatientType!]! + patientsTotal(locationNodeId: ID = null, rootLocationIds: [ID!] = null, states: [PatientState!] = null, filters: [QueryFilterClauseInput!] = null, sorts: [QuerySortClauseInput!] = null, search: QuerySearchInput = null): Int! + recentPatients(rootLocationIds: [ID!] = null, filters: [QueryFilterClauseInput!] = null, sorts: [QuerySortClauseInput!] = null, pagination: PaginationInput = null, search: QuerySearchInput = null): [PatientType!]! + recentPatientsTotal(rootLocationIds: [ID!] = null, filters: [QueryFilterClauseInput!] = null, sorts: [QuerySortClauseInput!] = null, search: QuerySearchInput = null): Int! + task(id: ID!): TaskType + tasks(patientId: ID = null, assigneeId: ID = null, assigneeTeamId: ID = null, rootLocationIds: [ID!] = null, filters: [QueryFilterClauseInput!] = null, sorts: [QuerySortClauseInput!] = null, pagination: PaginationInput = null, search: QuerySearchInput = null): [TaskType!]! + tasksTotal(patientId: ID = null, assigneeId: ID = null, assigneeTeamId: ID = null, rootLocationIds: [ID!] = null, filters: [QueryFilterClauseInput!] = null, sorts: [QuerySortClauseInput!] = null, search: QuerySearchInput = null): Int! + recentTasks(rootLocationIds: [ID!] = null, filters: [QueryFilterClauseInput!] = null, sorts: [QuerySortClauseInput!] = null, pagination: PaginationInput = null, search: QuerySearchInput = null): [TaskType!]! + recentTasksTotal(rootLocationIds: [ID!] = null, filters: [QueryFilterClauseInput!] = null, sorts: [QuerySortClauseInput!] = null, search: QuerySearchInput = null): Int! + locationRoots: [LocationNodeType!]! + locationNode(id: ID!): LocationNodeType + locationNodes(kind: LocationType = null, search: String = null, parentId: ID = null, recursive: Boolean! = false, orderByName: Boolean! = false, limit: Int = null, offset: Int = null): [LocationNodeType!]! + propertyDefinitions: [PropertyDefinitionType!]! + user(id: ID!): UserType + users(filters: [QueryFilterClauseInput!] = null, sorts: [QuerySortClauseInput!] = null, pagination: PaginationInput = null, search: QuerySearchInput = null): [UserType!]! + me: UserType + auditLogs(caseId: ID!, limit: Int = null, offset: Int = null): [AuditLogType!]! + queryableFields(entity: String!): [QueryableField!]! +} + +input QueryFilterClauseInput { + fieldKey: String! + operator: QueryOperator! + value: QueryFilterValueInput = null +} + +input QueryFilterValueInput { + stringValue: String = null + stringValues: [String!] = null + floatValue: Float = null + floatMin: Float = null + floatMax: Float = null + intValue: Int = null + boolValue: Boolean = null + dateValue: Date = null + dateMin: Date = null + dateMax: Date = null + dateTimeValue: DateTime = null + uuidValue: String = null + uuidValues: [String!] = null +} + +enum QueryOperator { + EQ + NEQ + GT + GTE + LT + LTE + BETWEEN + IN + NOT_IN + CONTAINS + STARTS_WITH + ENDS_WITH + IS_NULL + IS_NOT_NULL + ANY_EQ + ANY_IN + ALL_IN + NONE_IN + IS_EMPTY + IS_NOT_EMPTY +} + +input QuerySearchInput { + searchText: String = null + includeProperties: Boolean! = false +} + +input QuerySortClauseInput { + fieldKey: String! + direction: SortDirection! +} + +type QueryableChoiceMeta { + optionKeys: [String!]! + optionLabels: [String!]! +} + +type QueryableField { + key: String! + label: String! + kind: QueryableFieldKind! + valueType: QueryableValueType! + allowedOperators: [QueryOperator!]! + sortable: Boolean! + searchable: Boolean! + relation: QueryableRelationMeta + choice: QueryableChoiceMeta + propertyDefinitionId: String +} + +enum QueryableFieldKind { + SCALAR + PROPERTY + REFERENCE + REFERENCE_LIST + CHOICE + CHOICE_LIST +} + +type QueryableRelationMeta { + targetEntity: String! + idFieldKey: String! + labelFieldKey: String! + allowedFilterModes: [ReferenceFilterMode!]! +} + +enum QueryableValueType { + STRING + NUMBER + BOOLEAN + DATE + DATETIME + UUID + STRING_LIST + UUID_LIST +} + +enum ReferenceFilterMode { + ID + LABEL +} + +enum Sex { + MALE + FEMALE + UNKNOWN +} + +enum SortDirection { + ASC + DESC +} + +type Subscription { + patientCreated(rootLocationIds: [ID!] = null): ID! + patientUpdated(patientId: ID = null, rootLocationIds: [ID!] = null): ID! + patientStateChanged(patientId: ID = null, rootLocationIds: [ID!] = null): ID! + patientDeleted(rootLocationIds: [ID!] = null): ID! + taskCreated(rootLocationIds: [ID!] = null): ID! + taskUpdated(taskId: ID = null, rootLocationIds: [ID!] = null): ID! + taskDeleted(rootLocationIds: [ID!] = null): ID! + locationNodeCreated: ID! + locationNodeUpdated(locationId: ID = null): ID! + locationNodeDeleted: ID! +} + +enum TaskPriority { + P1 + P2 + P3 + P4 +} + +type TaskType { + id: ID! + title: String! + description: String + done: Boolean! + dueDate: DateTime + creationDate: DateTime! + updateDate: DateTime + assigneeId: ID + assigneeTeamId: ID + patientId: ID! + priority: String + estimatedTime: Int + assignee: UserType + assigneeTeam: LocationNodeType + patient: PatientType! + properties: [PropertyValueType!]! + checksum: String! +} + +input UpdateLocationNodeInput { + title: String = null + kind: LocationType = null + parentId: ID = null +} + +input UpdatePatientInput { + firstname: String = null + lastname: String = null + birthdate: Date = null + sex: Sex = null + assignedLocationId: ID = null + assignedLocationIds: [ID!] = null + clinicId: ID = null + positionId: ID + teamIds: [ID!] + properties: [PropertyValueInput!] = null + checksum: String = null + description: String = null +} + +input UpdateProfilePictureInput { + avatarUrl: String! +} + +input UpdatePropertyDefinitionInput { + name: String = null + description: String = null + options: [String!] = null + isActive: Boolean = null + allowedEntities: [PropertyEntity!] = null +} + +input UpdateTaskInput { + title: String = null + description: String = null + done: Boolean = null + dueDate: DateTime + assigneeId: ID = null + assigneeTeamId: ID + previousTaskIds: [ID!] = null + properties: [PropertyValueInput!] = null + checksum: String = null + priority: TaskPriority + estimatedTime: Int +} + +type UserType { + id: ID! + username: String! + email: String + firstname: String + lastname: String + title: String + avatarUrl: String + lastOnline: DateTime + name: String! + isOnline: Boolean! + organizations: String + tasks(rootLocationIds: [ID!] = null): [TaskType!]! + rootLocations: [LocationNodeType!]! +} \ No newline at end of file diff --git a/web/utils/propertyColumn.tsx b/web/utils/propertyColumn.tsx index 9edd7f9..9cb0848 100644 --- a/web/utils/propertyColumn.tsx +++ b/web/utils/propertyColumn.tsx @@ -1,5 +1,5 @@ import type { ColumnDef } from '@tanstack/table-core' -import { ColumnType, FieldType, type LocationType, type PropertyDefinitionType, type PropertyValueType, type PropertyEntity } from '@/api/gql/generated' +import { FieldType, type LocationType, type PropertyDefinitionType, type PropertyValueType, type PropertyEntity } from '@/api/gql/generated' import { getPropertyFilterFn } from './propertyFilterMapping' import { PropertyCell } from '@/components/properties/PropertyCell' @@ -72,7 +72,7 @@ export function createPropertyColumn( return () }, meta: { - columnType: ColumnType.Property, + columnType: 'PROPERTY', propertyDefinitionId: prop.id, fieldType: prop.fieldType, ...(filterData && { filterData }), diff --git a/web/utils/queryableFilterList.tsx b/web/utils/queryableFilterList.tsx new file mode 100644 index 0000000..a15b79f --- /dev/null +++ b/web/utils/queryableFilterList.tsx @@ -0,0 +1,48 @@ +import type { ReactNode } from 'react' +import type { FilterListItem, FilterListPopUpBuilderProps } from '@helpwave/hightide' +import type { DataType } from '@helpwave/hightide' +import type { QueryableField } from '@/api/gql/generated' +import { FieldType, QueryableFieldKind, QueryableValueType } from '@/api/gql/generated' +import { UserSelectFilterPopUp } from '@/components/tables/UserSelectFilterPopUp' + +function valueKindToDataType(field: QueryableField): DataType { + const vt = field.valueType + const k = field.kind + if (k === QueryableFieldKind.Choice) return 'singleTag' + if (k === QueryableFieldKind.ChoiceList) return 'multiTags' + if (k === QueryableFieldKind.Reference) return 'text' + if (vt === QueryableValueType.Boolean) return 'boolean' + if (vt === QueryableValueType.Number) return 'number' + if (vt === QueryableValueType.Date) return 'date' + if (vt === QueryableValueType.Datetime) return 'dateTime' + return 'text' +} + +export function queryableFieldsToFilterListItems( + fields: QueryableField[], + propertyFieldTypeByDefId: Map +): FilterListItem[] { + return fields.map((field): FilterListItem => { + const dataType = valueKindToDataType(field) + const tags = field.choice + ? field.choice.optionLabels.map((label, idx) => ({ + label, + tag: field.choice!.optionKeys[idx] ?? label, + })) + : [] + + const ft = field.propertyDefinitionId + ? propertyFieldTypeByDefId.get(field.propertyDefinitionId) + : undefined + + return { + id: field.key, + label: field.label, + dataType, + tags, + popUpBuilder: ft === FieldType.FieldTypeUser + ? (props: FilterListPopUpBuilderProps): ReactNode => () + : undefined, + } + }) +} diff --git a/web/utils/tableStateToApi.ts b/web/utils/tableStateToApi.ts index d664792..777e9ae 100644 --- a/web/utils/tableStateToApi.ts +++ b/web/utils/tableStateToApi.ts @@ -1,89 +1,110 @@ import type { ColumnFiltersState, PaginationState, SortingState } from '@tanstack/react-table' -import type { FilterInput, FilterOperator, FilterParameter, SortInput } from '@/api/gql/generated' -import { ColumnType, SortDirection } from '@/api/gql/generated' +import type { QueryFilterClauseInput, QueryFilterValueInput, QuerySortClauseInput } from '@/api/gql/generated' +import { QueryOperator, SortDirection } from '@/api/gql/generated' import type { DataType, FilterValue, FilterOperator as HightideFilterOperator } from '@helpwave/hightide' -const TABLE_OPERATOR_TO_API: Record>> = { +const TABLE_OPERATOR_TO_QUERY: Record>> = { text: { - equals: 'TEXT_EQUALS' as FilterOperator, - notEquals: 'TEXT_NOT_EQUALS' as FilterOperator, - contains: 'TEXT_CONTAINS' as FilterOperator, - notContains: 'TEXT_NOT_CONTAINS' as FilterOperator, - startsWith: 'TEXT_STARTS_WITH' as FilterOperator, - endsWith: 'TEXT_ENDS_WITH' as FilterOperator, - // TODO consider what to do with TEXT_NOT_WHITESPACE - isNotUndefined: 'IS_NOT_NULL' as FilterOperator, - isUndefined: 'IS_NULL' as FilterOperator, + equals: QueryOperator.Eq, + notEquals: QueryOperator.Neq, + contains: QueryOperator.Contains, + notContains: QueryOperator.Neq, + startsWith: QueryOperator.StartsWith, + endsWith: QueryOperator.EndsWith, + isNotUndefined: QueryOperator.IsNotNull, + isUndefined: QueryOperator.IsNull, }, number: { - equals: 'NUMBER_EQUALS' as FilterOperator, - notEquals: 'NUMBER_NOT_EQUALS' as FilterOperator, - greaterThan: 'NUMBER_GREATER_THAN' as FilterOperator, - greaterThanOrEqual: 'NUMBER_GREATER_THAN_OR_EQUAL' as FilterOperator, - lessThan: 'NUMBER_LESS_THAN' as FilterOperator, - lessThanOrEqual: 'NUMBER_LESS_THAN_OR_EQUAL' as FilterOperator, - between: 'NUMBER_BETWEEN' as FilterOperator, - notBetween: 'NUMBER_NOT_BETWEEN' as FilterOperator, - isNotUndefined: 'IS_NOT_NULL' as FilterOperator, - isUndefined: 'IS_NULL' as FilterOperator, + equals: QueryOperator.Eq, + notEquals: QueryOperator.Neq, + greaterThan: QueryOperator.Gt, + greaterThanOrEqual: QueryOperator.Gte, + lessThan: QueryOperator.Lt, + lessThanOrEqual: QueryOperator.Lte, + between: QueryOperator.Between, + notBetween: QueryOperator.Neq, + isNotUndefined: QueryOperator.IsNotNull, + isUndefined: QueryOperator.IsNull, }, date: { - equals: 'DATE_EQUALS' as FilterOperator, - notEquals: 'DATE_NOT_EQUALS' as FilterOperator, - greaterThan: 'DATE_GREATER_THAN' as FilterOperator, - greaterThanOrEqual: 'DATE_GREATER_THAN_OR_EQUAL' as FilterOperator, - lessThan: 'DATE_LESS_THAN' as FilterOperator, - lessThanOrEqual: 'DATE_LESS_THAN_OR_EQUAL' as FilterOperator, - between: 'DATE_BETWEEN' as FilterOperator, - notBetween: 'DATE_NOT_BETWEEN' as FilterOperator, - isNotUndefined: 'IS_NOT_NULL' as FilterOperator, - isUndefined: 'IS_NULL' as FilterOperator, + equals: QueryOperator.Eq, + notEquals: QueryOperator.Neq, + greaterThan: QueryOperator.Gt, + greaterThanOrEqual: QueryOperator.Gte, + lessThan: QueryOperator.Lt, + lessThanOrEqual: QueryOperator.Lte, + between: QueryOperator.Between, + notBetween: QueryOperator.Neq, + isNotUndefined: QueryOperator.IsNotNull, + isUndefined: QueryOperator.IsNull, }, dateTime: { - equals: 'DATETIME_EQUALS' as FilterOperator, - notEquals: 'DATETIME_NOT_EQUALS' as FilterOperator, - greaterThan: 'DATETIME_GREATER_THAN' as FilterOperator, - greaterThanOrEqual: 'DATETIME_GREATER_THAN_OR_EQUAL' as FilterOperator, - lessThan: 'DATETIME_LESS_THAN' as FilterOperator, - lessThanOrEqual: 'DATETIME_LESS_THAN_OR_EQUAL' as FilterOperator, - between: 'DATETIME_BETWEEN' as FilterOperator, - notBetween: 'DATETIME_NOT_BETWEEN' as FilterOperator, - isNotUndefined: 'IS_NOT_NULL' as FilterOperator, - isUndefined: 'IS_NULL' as FilterOperator, + equals: QueryOperator.Eq, + notEquals: QueryOperator.Neq, + greaterThan: QueryOperator.Gt, + greaterThanOrEqual: QueryOperator.Gte, + lessThan: QueryOperator.Lt, + lessThanOrEqual: QueryOperator.Lte, + between: QueryOperator.Between, + notBetween: QueryOperator.Neq, + isNotUndefined: QueryOperator.IsNotNull, + isUndefined: QueryOperator.IsNull, }, boolean: { - isTrue: 'BOOLEAN_IS_TRUE' as FilterOperator, - isFalse: 'BOOLEAN_IS_FALSE' as FilterOperator, - isNotUndefined: 'IS_NOT_NULL' as FilterOperator, - isUndefined: 'IS_NULL' as FilterOperator, + isTrue: QueryOperator.Eq, + isFalse: QueryOperator.Eq, + isNotUndefined: QueryOperator.IsNotNull, + isUndefined: QueryOperator.IsNull, }, singleTag: { - equals: 'TAGS_SINGLE_EQUALS' as FilterOperator, - notEquals: 'TAGS_SINGLE_NOT_EQUALS' as FilterOperator, - contains: 'TAGS_SINGLE_CONTAINS' as FilterOperator, - notContains: 'TAGS_SINGLE_NOT_CONTAINS' as FilterOperator, - isNotUndefined: 'IS_NOT_NULL' as FilterOperator, - isUndefined: 'IS_NULL' as FilterOperator, + equals: QueryOperator.Eq, + notEquals: QueryOperator.Neq, + contains: QueryOperator.In, + notContains: QueryOperator.Neq, + isNotUndefined: QueryOperator.IsNotNull, + isUndefined: QueryOperator.IsNull, }, multiTags: { - equals: 'TAGS_EQUALS' as FilterOperator, - notEquals: 'TAGS_NOT_EQUALS' as FilterOperator, - contains: 'TAGS_CONTAINS' as FilterOperator, - notContains: 'TAGS_NOT_CONTAINS' as FilterOperator, - isNotUndefined: 'IS_NOT_NULL' as FilterOperator, - isUndefined: 'IS_NULL' as FilterOperator, + equals: QueryOperator.AllIn, + notEquals: QueryOperator.Neq, + contains: QueryOperator.AnyIn, + notContains: QueryOperator.NoneIn, + isNotUndefined: QueryOperator.IsNotNull, + isUndefined: QueryOperator.IsNull, }, unknownType: { - isNotUndefined: 'IS_NOT_NULL' as FilterOperator, - isUndefined: 'IS_NULL' as FilterOperator, + isNotUndefined: QueryOperator.IsNotNull, + isUndefined: QueryOperator.IsNull, }, } -function tableOperatorToApi(dataType: DataType, operator: HightideFilterOperator): FilterOperator | null { - return TABLE_OPERATOR_TO_API[dataType][operator] ?? null +function tableOperatorToQuery(dataType: DataType, operator: HightideFilterOperator): QueryOperator | null { + return TABLE_OPERATOR_TO_QUERY[dataType][operator] ?? null } -function toFilterParameter(value: FilterValue, propertyDefinitionId?: string): FilterParameter { +function formatLocalDateOnly(d: Date): string { + const y = d.getFullYear() + const m = String(d.getMonth() + 1).padStart(2, '0') + const day = String(d.getDate()).padStart(2, '0') + return `${y}-${m}-${day}` +} + +function toGraphqlDateInput(value: unknown): string | undefined { + if (value == null) return undefined + if (typeof value === 'string') { + if (/^\d{4}-\d{2}-\d{2}$/.test(value)) return value + const parsed = new Date(value) + if (Number.isNaN(parsed.getTime())) return undefined + return formatLocalDateOnly(parsed) + } + if (value instanceof Date) { + if (Number.isNaN(value.getTime())) return undefined + return formatLocalDateOnly(value) + } + return undefined +} + +function toQueryFilterValue(value: FilterValue): QueryFilterValueInput { const parameter = value.parameter const raw = parameter as Record const multi = parameter.multiOptionSearch @@ -102,81 +123,56 @@ function toFilterParameter(value: FilterValue, propertyDefinitionId?: string): F searchTagsUnknownType = [raw['searchTag']] } const searchTags: string[] = searchTagsUnknownType.map((t) => String(t)) - const param: FilterParameter = { - searchText: parameter.searchText, - isCaseSensitive: parameter.isCaseSensitive, - compareValue: parameter.compareValue, - min: parameter.minNumber, - max: parameter.maxNumber, - compareDate: parameter.compareDate?.toISOString().split('T')[0], - minDate: parameter.minDate?.toISOString().split('T')[0], - maxDate: parameter.maxDate?.toISOString().split('T')[0], - compareDateTime: parameter.compareDate?.toISOString().split('Z')[0], - minDateTime: parameter.minDate?.toISOString().split('Z')[0], - maxDateTime: parameter.maxDate?.toISOString().split('Z')[0], - searchTags, - propertyDefinitionId: propertyDefinitionId, + const base: QueryFilterValueInput = { + stringValue: parameter.searchText, + floatValue: parameter.compareValue, + floatMin: parameter.minNumber, + floatMax: parameter.maxNumber, + dateValue: toGraphqlDateInput(parameter.compareDate), + dateMin: toGraphqlDateInput(parameter.minDate), + dateMax: toGraphqlDateInput(parameter.maxDate), + dateTimeValue: parameter.compareDate?.toISOString(), + stringValues: searchTags.length > 0 ? searchTags : undefined, } - return param -} - -const TASK_COLUMN_TO_BACKEND: Record = { - dueDate: 'due_date', - updateDate: 'update_date', - creationDate: 'creation_date', - estimatedTime: 'estimated_time', - assigneeTeam: 'assignee_team_id', -} - -function isPropertyColumnId(id: string): boolean { - return id.startsWith('property_') -} - -function getPropertyDefinitionId(id: string): string | undefined { - if (!isPropertyColumnId(id)) return undefined - return id.replace(/^property_/, '') -} - -function columnIdToBackend(columnId: string, entity: 'task' | 'patient'): string { - if (entity === 'task' && TASK_COLUMN_TO_BACKEND[columnId]) { - return TASK_COLUMN_TO_BACKEND[columnId] + if (value.dataType === 'singleTag' && value.operator === 'equals' && searchTags.length === 1) { + base.stringValue = searchTags[0] + base.stringValues = undefined } - return columnId + if (value.dataType === 'boolean') { + if (value.operator === 'isTrue') { + base.boolValue = true + } else if (value.operator === 'isFalse') { + base.boolValue = false + } + } + return base } -export function columnFiltersToFilterInput( - filters: ColumnFiltersState, - entity: 'task' | 'patient' = 'patient' -): FilterInput[] { - const result: FilterInput[] = [] +export function columnFiltersToQueryFilterClauses( + filters: ColumnFiltersState +): QueryFilterClauseInput[] { + const result: QueryFilterClauseInput[] = [] for (const filter of filters) { const value = filter.value as FilterValue if (!value?.operator || !value?.parameter || !value?.dataType) continue - const apiOperator = tableOperatorToApi(value.dataType, value.operator) + const apiOperator = tableOperatorToQuery(value.dataType, value.operator) if (!apiOperator) continue - const isProperty = isPropertyColumnId(filter.id) - const propertyDefinitionId = getPropertyDefinitionId(filter.id) - const column = columnIdToBackend(filter.id, entity) + const fieldKey = filter.id result.push({ - column, + fieldKey, operator: apiOperator, - parameter: toFilterParameter(value), - columnType: isProperty ? ColumnType.Property : ColumnType.DirectAttribute, - propertyDefinitionId: propertyDefinitionId ?? undefined, + value: toQueryFilterValue(value), }) } return result } -export function sortingStateToSortInput( - sorting: SortingState, - entity: 'task' | 'patient' = 'patient' -): SortInput[] { +export function sortingStateToQuerySortClauses( + sorting: SortingState +): QuerySortClauseInput[] { return sorting.map((s) => ({ - column: columnIdToBackend(s.id, entity), - direction: s.desc ? SortDirection.Desc : SortDirection.Asc, - columnType: isPropertyColumnId(s.id) ? ColumnType.Property : ColumnType.DirectAttribute, - propertyDefinitionId: getPropertyDefinitionId(s.id) ?? undefined, + fieldKey: s.id, + direction: s.desc ? SortDirection.Desc : SortDirection.Asc })) } @@ -186,3 +182,6 @@ export function paginationStateToPaginationInput(pagination: PaginationState): { pageSize: pagination.pageSize ?? 10, } } + +export { columnFiltersToQueryFilterClauses as columnFiltersToFilterInput } +export { sortingStateToQuerySortClauses as sortingStateToSortInput } From 2997584ad3a520b4637d7f304bcb360b91cd9607 Mon Sep 17 00:00:00 2001 From: Felix Evers Date: Sun, 22 Mar 2026 21:02:09 +0100 Subject: [PATCH 10/20] integrate customer views with new filter logic --- web/components/layout/Page.tsx | 44 ++++++++++++++++----------------- web/data/hooks/queryHelpers.ts | 4 +-- web/data/hooks/useSavedViews.ts | 12 +++++++-- web/i18n/translations.ts | 14 +++++++++++ web/locales/de-DE.arb | 2 ++ web/locales/en-US.arb | 2 ++ web/locales/es-ES.arb | 2 ++ web/locales/fr-FR.arb | 2 ++ web/locales/nl-NL.arb | 2 ++ web/locales/pt-BR.arb | 2 ++ web/pages/settings/views.tsx | 6 ----- 11 files changed, 58 insertions(+), 34 deletions(-) diff --git a/web/components/layout/Page.tsx b/web/components/layout/Page.tsx index 7a62217..75704aa 100644 --- a/web/components/layout/Page.tsx +++ b/web/components/layout/Page.tsx @@ -554,30 +554,28 @@ export const Sidebar = ({ isOpen, onClose, ...props }: SidebarProps) => { {translation('patients')} {context?.totalPatientsCount !== undefined && ({context.totalPatientsCount})} - {savedViews.length > 0 && ( - - -
- - {translation('savedViews')} -
-
- - {savedViews.map((v: MySavedViewsQuery['mySavedViews'][number]) => ( - - {v.name} - - ))} - - {translation('viewSettings')} + + +
+ + {translation('savedViews')} +
+
+ + {savedViews.map((v: MySavedViewsQuery['mySavedViews'][number]) => ( + + {v.name} - -
- )} + ))} + + {translation('viewSettings')} + +
+
{(context?.teams?.length ?? 0) > 0 && ( () @@ -30,9 +29,8 @@ export function useQueryWhenReady { - const client = useApolloClientOptional() const doc = useMemo(() => getParsedDocument(document), [document]) - const skip = options?.skip ?? !client + const skip = options?.skip ?? false const result = useQuery(doc, { variables, skip, diff --git a/web/data/hooks/useSavedViews.ts b/web/data/hooks/useSavedViews.ts index 52514b7..58bda13 100644 --- a/web/data/hooks/useSavedViews.ts +++ b/web/data/hooks/useSavedViews.ts @@ -8,11 +8,19 @@ import { } from '@/api/gql/generated' import { useQueryWhenReady } from './queryHelpers' -export function useMySavedViews(options?: { skip?: boolean }) { +type MySavedViewsHookOptions = { + skip?: boolean, + fetchPolicy?: 'cache-first' | 'cache-and-network' | 'network-only', +} + +export function useMySavedViews(options?: MySavedViewsHookOptions) { return useQueryWhenReady( MySavedViewsDocument, {}, - options + { + skip: options?.skip, + fetchPolicy: options?.fetchPolicy ?? 'cache-and-network', + } ) } diff --git a/web/i18n/translations.ts b/web/i18n/translations.ts index 13eb60c..99c12bf 100644 --- a/web/i18n/translations.ts +++ b/web/i18n/translations.ts @@ -82,6 +82,7 @@ export type TasksTranslationEntries = { 'feedback': string, 'feedbackDescription': string, 'female': string, + 'filter': string, 'filterAll': string, 'filterUndone': string, 'firstName': string, @@ -195,6 +196,7 @@ export type TasksTranslationEntries = { 'shiftHandover': string, 'showAllTasks': string, 'showTeamTasks': string, + 'sorting': string, 'sPropertySubjectType': (values: { subject: string }) => string, 'sPropertyType': (values: { type: string }) => string, 'stagingModalDisclaimerMarkdown': string, @@ -326,6 +328,7 @@ export const tasksTranslation: Translation { return TranslationGen.resolveSelect(subject, { 'patient': `Patient`, @@ -657,6 +661,7 @@ export const tasksTranslation: Translation { return TranslationGen.resolveSelect(subject, { 'patient': `Patient`, @@ -988,6 +994,7 @@ export const tasksTranslation: Translation { return TranslationGen.resolveSelect(subject, { 'patient': `Paciente`, @@ -1318,6 +1326,7 @@ export const tasksTranslation: Translation { return TranslationGen.resolveSelect(subject, { 'patient': `Patient`, @@ -1648,6 +1658,7 @@ export const tasksTranslation: Translation { return TranslationGen.resolveSelect(subject, { 'patient': `Patiënt`, @@ -1981,6 +1993,7 @@ export const tasksTranslation: Translation { return TranslationGen.resolveSelect(subject, { 'patient': `Paciente`, diff --git a/web/locales/de-DE.arb b/web/locales/de-DE.arb index b348c52..7180dfa 100644 --- a/web/locales/de-DE.arb +++ b/web/locales/de-DE.arb @@ -97,6 +97,7 @@ "estimatedTime": "Geschätzte Zeit (Minuten)", "feedback": "Feedback", "female": "Weiblich", + "filter": "Filter", "filterAll": "Alle", "filterUndone": "Offen", "firstName": "Vorname", @@ -269,6 +270,7 @@ "shiftHandover": "Schichtübergabe", "showAllTasks": "Alle Aufgaben anzeigen", "showTeamTasks": "Team-Aufgaben anzeigen", + "sorting": "Sortierung", "stagingModalDisclaimerMarkdown": "Diese öffentliche Instanz von helpwave tasks ist für '\\b{Entwicklungs- und Vorschauzwecke}' gedacht. Bitte stellen Sie sicher, dass Sie '\\b{ausschließlich nicht-vertrauliche Testdaten}' eingeben. Diese Instanz kann '\\negative{\\b{jederzeit gelöscht}}' werden.", "status": "Status", "subjectType": "Subjekt Type", diff --git a/web/locales/en-US.arb b/web/locales/en-US.arb index 1fdad82..8702c9c 100644 --- a/web/locales/en-US.arb +++ b/web/locales/en-US.arb @@ -97,6 +97,7 @@ "estimatedTime": "Estimated Time (minutes)", "feedback": "Feedback", "female": "Female", + "filter": "Filter", "filterAll": "All", "filterUndone": "Undone", "firstName": "First Name", @@ -269,6 +270,7 @@ "shiftHandover": "Shift Handover", "showAllTasks": "Show All Tasks", "showTeamTasks": "Show Team Tasks", + "sorting": "Sorting", "stagingModalDisclaimerMarkdown": "This public instance of helpwave tasks is for '\\b{development and preview purposes}'. Please make sure to '\\b{only}' enter '\\b{non-confidential testing data}'. This instance can be '\\negative{\\b{deleted at any time}}'", "status": "Status", "subjectType": "Subject Type", diff --git a/web/locales/es-ES.arb b/web/locales/es-ES.arb index ba85555..1b66b06 100644 --- a/web/locales/es-ES.arb +++ b/web/locales/es-ES.arb @@ -63,6 +63,7 @@ "estimatedTime": "Tiempo estimado (minutos)", "feedback": "Comentarios", "female": "Femenino", + "filter": "Filtro", "filterAll": "Todos", "filterUndone": "Pendientes", "firstName": "Nombre", @@ -165,6 +166,7 @@ "shiftHandover": "Traspaso de turno", "showAllTasks": "Mostrar todas las tareas", "showTeamTasks": "Mostrar tareas del equipo", + "sorting": "Ordenación", "stagingModalDisclaimerMarkdown": "Esta instancia pública de helpwave tasks es para '\\b{desarrollo y vista previa}'. Asegúrese de '\\b{solo}' introducir '\\b{datos de prueba no confidenciales}'. Esta instancia puede '\\negative{\\b{eliminarse en cualquier momento}}'", "status": "Estado", "subjectType": "Tipo de sujeto", diff --git a/web/locales/fr-FR.arb b/web/locales/fr-FR.arb index 88b58e2..9b51e3a 100644 --- a/web/locales/fr-FR.arb +++ b/web/locales/fr-FR.arb @@ -63,6 +63,7 @@ "estimatedTime": "Temps estimé (minutes)", "feedback": "Commentaires", "female": "Féminin", + "filter": "Filtre", "filterAll": "Tous", "filterUndone": "En attente", "firstName": "Prénom", @@ -165,6 +166,7 @@ "shiftHandover": "Passation", "showAllTasks": "Afficher toutes les tâches", "showTeamTasks": "Afficher les tâches d''équipe", + "sorting": "Tri", "stagingModalDisclaimerMarkdown": "Cette instance publique de helpwave tasks est destinée au '\\b{développement et à l''aperçu}'. Veuillez '\\b{ne}' saisir '\\b{que des données de test non confidentielles}'. Cette instance peut '\\negative{\\b{être supprimée à tout moment}}'", "status": "Statut", "subjectType": "Type de sujet", diff --git a/web/locales/nl-NL.arb b/web/locales/nl-NL.arb index 0bf5450..d24eb2c 100644 --- a/web/locales/nl-NL.arb +++ b/web/locales/nl-NL.arb @@ -63,6 +63,7 @@ "estimatedTime": "Geschatte tijd (minuten)", "feedback": "Feedback", "female": "Vrouw", + "filter": "Filter", "filterAll": "Alle", "filterUndone": "Open", "firstName": "Voornaam", @@ -165,6 +166,7 @@ "shiftHandover": "Dienstwissel", "showAllTasks": "Alle taken tonen", "showTeamTasks": "Teamtaken tonen", + "sorting": "Sorteren", "stagingModalDisclaimerMarkdown": "Deze openbare instantie van helpwave tasks is voor '\\b{ontwikkeling en voorbeeld}'. Zorg ervoor dat u '\\b{alleen}' '\\b{niet-vertrouwelijke testgegevens}' invoert. Deze instantie kan '\\negative{\\b{op elk moment worden verwijderd}}'", "status": "Status", "subjectType": "Onderwerptype", diff --git a/web/locales/pt-BR.arb b/web/locales/pt-BR.arb index 5c486f4..ad40f1e 100644 --- a/web/locales/pt-BR.arb +++ b/web/locales/pt-BR.arb @@ -63,6 +63,7 @@ "estimatedTime": "Tempo estimado (minutos)", "feedback": "Feedback", "female": "Feminino", + "filter": "Filtro", "filterAll": "Todos", "filterUndone": "Pendentes", "firstName": "Nome", @@ -165,6 +166,7 @@ "shiftHandover": "Passagem de turno", "showAllTasks": "Mostrar todas as tarefas", "showTeamTasks": "Mostrar tarefas da equipe", + "sorting": "Ordenação", "stagingModalDisclaimerMarkdown": "Esta instância pública do helpwave tasks é para '\\b{desenvolvimento e pré-visualização}'. Certifique-se de '\\b{apenas}' inserir '\\b{dados de teste não confidenciais}'. Esta instância pode '\\negative{\\b{ser excluída a qualquer momento}}'", "status": "Status", "subjectType": "Tipo de sujeito", diff --git a/web/pages/settings/views.tsx b/web/pages/settings/views.tsx index 95748fe..0d5c0c1 100644 --- a/web/pages/settings/views.tsx +++ b/web/pages/settings/views.tsx @@ -3,7 +3,6 @@ import type { NextPage } from 'next' import { useCallback, useMemo, useState } from 'react' import { useMutation } from '@apollo/client/react' -import Link from 'next/link' import { useRouter } from 'next/router' import { Page } from '@/components/layout/Page' import titleWrapper from '@/utils/titleWrapper' @@ -203,11 +202,6 @@ const ViewsSettingsPage: NextPage = () => { titleElement={translation('viewSettings')} description={translation('viewSettingsDescription')} > -
- - ← {translation('settings')} - -
{loading ? ( ) : ( From d60fcc2b7990ea87daefcf74ef1c5847176a0309 Mon Sep 17 00:00:00 2001 From: Felix Evers Date: Sun, 22 Mar 2026 21:48:35 +0100 Subject: [PATCH 11/20] fix tasks list render --- backend/api/query/field_ops.py | 62 ++++----- backend/api/query/inputs.py | 4 +- web/api/gql/generated.ts | 4 +- web/components/tables/TaskList.tsx | 169 +++++++++++------------- web/components/views/SaveViewDialog.tsx | 2 + web/pages/location/[id].tsx | 1 - web/pages/settings/views.tsx | 22 +-- web/pages/tasks/index.tsx | 14 +- web/pages/view/[uid].tsx | 5 +- web/schema.graphql | 4 +- web/utils/tableStateToApi.ts | 25 +++- 11 files changed, 158 insertions(+), 154 deletions(-) diff --git a/backend/api/query/field_ops.py b/backend/api/query/field_ops.py index 3c7c32b..32c6494 100644 --- a/backend/api/query/field_ops.py +++ b/backend/api/query/field_ops.py @@ -52,14 +52,10 @@ def apply_ops_to_column( return column == value.string_value if value.float_value is not None: return column == value.float_value - if value.int_value is not None: - return column == value.int_value if value.bool_value is not None: return column == value.bool_value if value.date_value is not None: - return func.date(column) == value.date_value - if value.date_time_value is not None: - return column == value.date_time_value + return column == value.date_value return None if operator == QueryOperator.NEQ: @@ -139,12 +135,8 @@ def _cmp(column: Any, value: QueryFilterValueInput | None, pred) -> ColumnElemen return None if value.float_value is not None: return pred(column, value.float_value) - if value.int_value is not None: - return pred(column, value.int_value) if value.date_value is not None: - return pred(func.date(column), value.date_value) - if value.date_time_value is not None: - return pred(column, value.date_time_value) + return pred(column, value.date_value) return None @@ -154,18 +146,6 @@ def _apply_date_ops( if value is None: return None dc = func.date(column) - if operator == QueryOperator.EQ and value.date_value is not None: - return dc == value.date_value - if operator == QueryOperator.NEQ and value.date_value is not None: - return dc != value.date_value - if operator == QueryOperator.GT and value.date_value is not None: - return dc > value.date_value - if operator == QueryOperator.GTE and value.date_value is not None: - return dc >= value.date_value - if operator == QueryOperator.LT and value.date_value is not None: - return dc < value.date_value - if operator == QueryOperator.LTE and value.date_value is not None: - return dc <= value.date_value if ( operator == QueryOperator.BETWEEN and value.date_min is not None @@ -174,6 +154,20 @@ def _apply_date_ops( return dc.between(value.date_min, value.date_max) if operator == QueryOperator.IN and value.string_values: return dc.in_(value.string_values) + if value.date_value is not None: + d = value.date_value.date() + if operator == QueryOperator.EQ: + return dc == d + if operator == QueryOperator.NEQ: + return dc != d + if operator == QueryOperator.GT: + return dc > d + if operator == QueryOperator.GTE: + return dc >= d + if operator == QueryOperator.LT: + return dc < d + if operator == QueryOperator.LTE: + return dc <= d return None @@ -182,18 +176,18 @@ def _apply_datetime_ops( ) -> ColumnElement[bool] | None: if value is None: return None - if operator == QueryOperator.EQ and value.date_time_value is not None: - return column == value.date_time_value - if operator == QueryOperator.NEQ and value.date_time_value is not None: - return column != value.date_time_value - if operator == QueryOperator.GT and value.date_time_value is not None: - return column > value.date_time_value - if operator == QueryOperator.GTE and value.date_time_value is not None: - return column >= value.date_time_value - if operator == QueryOperator.LT and value.date_time_value is not None: - return column < value.date_time_value - if operator == QueryOperator.LTE and value.date_time_value is not None: - return column <= value.date_time_value + if operator == QueryOperator.EQ and value.date_value is not None: + return column == value.date_value + if operator == QueryOperator.NEQ and value.date_value is not None: + return column != value.date_value + if operator == QueryOperator.GT and value.date_value is not None: + return column > value.date_value + if operator == QueryOperator.GTE and value.date_value is not None: + return column >= value.date_value + if operator == QueryOperator.LT and value.date_value is not None: + return column < value.date_value + if operator == QueryOperator.LTE and value.date_value is not None: + return column <= value.date_value if ( operator == QueryOperator.BETWEEN and value.date_min is not None diff --git a/backend/api/query/inputs.py b/backend/api/query/inputs.py index 2a0ad05..2180ab6 100644 --- a/backend/api/query/inputs.py +++ b/backend/api/query/inputs.py @@ -13,12 +13,10 @@ class QueryFilterValueInput: float_value: float | None = None float_min: float | None = None float_max: float | None = None - int_value: int | None = None bool_value: bool | None = None - date_value: date | None = None + date_value: datetime | None = None date_min: date | None = None date_max: date | None = None - date_time_value: datetime | None = None uuid_value: str | None = None uuid_values: list[str] | None = None diff --git a/web/api/gql/generated.ts b/web/api/gql/generated.ts index 3eaa64b..e192f98 100644 --- a/web/api/gql/generated.ts +++ b/web/api/gql/generated.ts @@ -543,12 +543,10 @@ export type QueryFilterValueInput = { boolValue?: InputMaybe; dateMax?: InputMaybe; dateMin?: InputMaybe; - dateTimeValue?: InputMaybe; - dateValue?: InputMaybe; + dateValue?: InputMaybe; floatMax?: InputMaybe; floatMin?: InputMaybe; floatValue?: InputMaybe; - intValue?: InputMaybe; stringValue?: InputMaybe; stringValues?: InputMaybe>; uuidValue?: InputMaybe; diff --git a/web/components/tables/TaskList.tsx b/web/components/tables/TaskList.tsx index dc46564..14f7461 100644 --- a/web/components/tables/TaskList.tsx +++ b/web/components/tables/TaskList.tsx @@ -1,7 +1,7 @@ import { useMemo, useState, forwardRef, useImperativeHandle, useEffect, useRef, useCallback } from 'react' import { useQueryClient } from '@tanstack/react-query' import type { FilterListItem } from '@helpwave/hightide' -import { Button, Checkbox, ConfirmDialog, FilterList, FillerCell, HelpwaveLogo, IconButton, LoadingContainer, SearchBar, Select, SelectOption, TableColumnSwitcher, TableDisplay, TablePagination, TableProvider } from '@helpwave/hightide' +import { Button, Checkbox, ConfirmDialog, FilterList, FillerCell, HelpwaveLogo, IconButton, LoadingContainer, SearchBar, TableColumnSwitcher, TableDisplay, TablePagination, TableProvider, SortingList, ExpansionIcon } from '@helpwave/hightide' import { PlusIcon, UserCheck, Users } from 'lucide-react' import type { IdentifierFilterValue } from '@helpwave/hightide' import type { TaskPriority, GetTasksQuery } from '@/api/gql/generated' @@ -77,15 +77,15 @@ type TaskListProps = { initialTaskId?: string, onInitialTaskOpened?: () => void, headerActions?: React.ReactNode, + saveViewSlot?: React.ReactNode, totalCount?: number, loading?: boolean, - showAllTasksMode?: boolean, tableState?: TaskListTableState, searchQuery?: string, onSearchQueryChange?: (value: string) => void, } -export const TaskList = forwardRef(({ tasks: initialTasks, onRefetch, showAssignee = false, initialTaskId, onInitialTaskOpened, headerActions, totalCount, loading = false, showAllTasksMode = false, tableState: controlledTableState, searchQuery: searchQueryProp, onSearchQueryChange }, ref) => { +export const TaskList = forwardRef(({ tasks: initialTasks, onRefetch, showAssignee = false, initialTaskId, onInitialTaskOpened, headerActions, saveViewSlot, totalCount, loading = false, tableState: controlledTableState, searchQuery: searchQueryProp, onSearchQueryChange }, ref) => { const translation = useTasksTranslation() const { data: propertyDefinitionsData } = usePropertyDefinitions() const { data: queryableFieldsData } = useQueryableFields('Task') @@ -117,37 +117,6 @@ export const TaskList = forwardRef(({ tasks: initial setColumnVisibility ) - const normalizeDoneFilterValue = useCallback((value: unknown): boolean | undefined => { - if (value === true || value === 'true' || value === 'done') return true - if (value === false || value === 'false' || value === 'undone') return false - return undefined - }, []) - const rawDoneFilterValue = filters.find(f => f.id === 'done')?.value - const storedDoneFilterValue = rawDoneFilterValue === true || rawDoneFilterValue === 'true' || rawDoneFilterValue === 'done' - ? 'done' - : rawDoneFilterValue === false || rawDoneFilterValue === 'false' || rawDoneFilterValue === 'undone' - ? 'undone' - : 'all' - const doneFilterValue = showAllTasksMode ? 'all' : storedDoneFilterValue - const setDoneFilter = useCallback((value: boolean | 'all') => { - setFilters(prev => { - const rest = prev.filter(f => f.id !== 'done') - if (value === 'all') return rest - return [...rest, { id: 'done', value }] - }) - }, [setFilters]) - const setFiltersNormalized = useCallback((updater: ColumnFiltersState | ((prev: ColumnFiltersState) => ColumnFiltersState)) => { - setFilters(prev => { - const next = typeof updater === 'function' ? updater(prev) : updater - return next.flatMap(f => { - if (f.id !== 'done') return [f] - const normalized = normalizeDoneFilterValue(f.value) - if (normalized === undefined) return [] - return [{ ...f, value: normalized }] - }) - }) - }, [setFilters, normalizeDoneFilterValue]) - const queryClient = useQueryClient() const { totalPatientsCount, user } = useTasksContext() const { refreshingTaskIds } = useRefreshingEntityIds() @@ -168,6 +137,8 @@ export const TaskList = forwardRef(({ tasks: initial const [selectedUserId, setSelectedUserId] = useState(null) const [isConfirmDialogOpen, setIsConfirmDialogOpen] = useState(false) const isOpeningConfirmDialogRef = useRef(false) + const [isShowFilters, setIsShowFilters] = useState(false) + const [isShowSorting, setIsShowSorting] = useState(false) const hasPatients = (totalPatientsCount ?? 0) > 0 @@ -356,9 +327,13 @@ export const TaskList = forwardRef(({ tasks: initial return [ { id: 'title', label: translation('title'), dataType: 'text', tags: [] }, { id: 'description', label: translation('description'), dataType: 'text', tags: [] }, - { id: 'done', label: translation('done'), dataType: 'boolean', tags: [] }, { id: 'dueDate', label: translation('dueDate'), dataType: 'date', tags: [] }, - { id: 'priority', label: translation('priority'), dataType: 'singleTag', tags: ['P1', 'P2', 'P3', 'P4'].map(p => ({ label: p, tag: p })) }, + { + id: 'priority', + label: translation('priorityLabel'), + dataType: 'singleTag', + tags: ['P1', 'P2', 'P3', 'P4'].map(p => ({ label: translation('priority', { priority: p }), tag: p })), + }, { id: 'patient', label: translation('patient'), dataType: 'text', tags: [] }, { id: 'assignee', label: translation('assignedTo'), dataType: 'text', tags: [] }, ...propertyDefinitionsData?.propertyDefinitions.map(def => ({ @@ -378,14 +353,6 @@ export const TaskList = forwardRef(({ tasks: initial id: 'done', header: () => null, accessorKey: 'done', - enableColumnFilter: true, - filterFn: (row, _columnId, filterValue: boolean | string | undefined) => { - if (filterValue === undefined || filterValue === 'all') return true - const wantDone = filterValue === true || filterValue === 'done' || filterValue === 'true' - const wantUndone = filterValue === false || filterValue === 'undone' || filterValue === 'false' - if (!wantDone && !wantUndone) return true - return wantDone ? row.getValue('done') === true : row.getValue('done') === false - }, cell: ({ row }) => { if (refreshingTaskIds.has(row.original.id)) return rowLoadingCell const task = row.original @@ -586,7 +553,6 @@ export const TaskList = forwardRef(({ tasks: initial data={tasks} columns={columns} fillerRowCell={useCallback(() => (), [])} - manualPagination={true} initialState={{ pagination: { pageSize: 10, @@ -595,64 +561,87 @@ export const TaskList = forwardRef(({ tasks: initial state={{ columnVisibility, pagination, - sorting, - columnFilters: showAllTasksMode ? filters.filter(f => f.id !== 'done') : filters, } as Partial as TableState} onColumnVisibilityChange={setColumnVisibility} onPaginationChange={setPagination} onSortingChange={setSorting} - onColumnFiltersChange={setFiltersNormalized} + onColumnFiltersChange={setFilters} enableMultiSort={true} + enablePinning={false} onRowClick={row => setTaskDialogState({ isOpen: true, taskId: row.original.id })} pageCount={stableTotalCount != null ? Math.ceil(stableTotalCount / pagination.pageSize) : -1} + manualPagination={true} + manualSorting={true} + manualFiltering={true} + enableColumnFilters={false} + enableSorting={false} + enableColumnPinning={false} >
-
-
- setSearchQuery(e.target.value)} - onSearch={() => null} - containerProps={{ className: 'max-w-80' }} - /> - { setFiltersNormalized(value) }} - availableItems={availableFilters} - /> - -
-
- - {headerActions} - {canHandover && ( +
+
+
+ setSearchQuery(e.target.value)} + onSearch={() => null} + containerProps={{ className: 'max-w-80' }} + /> + - )} - setTaskDialogState({ isOpen: true })} - disabled={!hasPatients} - > - - + + {saveViewSlot} +
+
+ {headerActions} + {canHandover && ( + + )} + setTaskDialogState({ isOpen: true })} + disabled={!hasPatients} + > + + +
+ {isShowFilters && ( + + )} + {isShowSorting && ( + + )}
{loading && ( diff --git a/web/components/views/SaveViewDialog.tsx b/web/components/views/SaveViewDialog.tsx index a8d4f66..857d4c7 100644 --- a/web/components/views/SaveViewDialog.tsx +++ b/web/components/views/SaveViewDialog.tsx @@ -7,6 +7,7 @@ import type { SavedViewEntityType } from '@/api/gql/generated' import { CreateSavedViewDocument, + MySavedViewsDocument, type CreateSavedViewMutation, type CreateSavedViewMutationVariables, SavedViewVisibility @@ -47,6 +48,7 @@ export function SaveViewDialog({ CreateSavedViewMutation, CreateSavedViewMutationVariables >(getParsedDocument(CreateSavedViewDocument), { + refetchQueries: [{ query: getParsedDocument(MySavedViewsDocument) }], onCompleted(data) { onCreated?.(data?.createSavedView?.id) handleClose() diff --git a/web/pages/location/[id].tsx b/web/pages/location/[id].tsx index 6b7aad3..027e93e 100644 --- a/web/pages/location/[id].tsx +++ b/web/pages/location/[id].tsx @@ -210,7 +210,6 @@ const LocationPage: NextPage = () => { onRefetch={handleRefetch} showAssignee={true} loading={isTeamLocation ? isLoadingTasks : isLoadingPatients} - showAllTasksMode={isTeamLocation && showAllTasks} headerActions={ isTeamLocation ? ( -
- setIsSaveViewOpen(false)} @@ -131,6 +124,13 @@ const TasksPage: NextPage = () => { loading={tasksLoading} searchQuery={searchQuery} onSearchQueryChange={setSearchQuery} + saveViewSlot={( + + + + )} tableState={{ pagination, setPagination, diff --git a/web/pages/view/[uid].tsx b/web/pages/view/[uid].tsx index ff3a19e..8abd4b6 100644 --- a/web/pages/view/[uid].tsx +++ b/web/pages/view/[uid].tsx @@ -17,6 +17,7 @@ import { TaskViewPatientsPanel } from '@/components/views/TaskViewPatientsPanel' import { useSavedView } from '@/data' import { DuplicateSavedViewDocument, + MySavedViewsDocument, type DuplicateSavedViewMutation, type DuplicateSavedViewMutationVariables, SavedViewEntityType @@ -146,7 +147,9 @@ const ViewPage: NextPage = () => { const [duplicateSavedView] = useMutation< DuplicateSavedViewMutation, DuplicateSavedViewMutationVariables - >(getParsedDocument(DuplicateSavedViewDocument)) + >(getParsedDocument(DuplicateSavedViewDocument), { + refetchQueries: [{ query: getParsedDocument(MySavedViewsDocument) }], + }) const handleDuplicate = useCallback(async () => { if (!view?.id || duplicateName.trim().length < 2) return diff --git a/web/schema.graphql b/web/schema.graphql index b72c18a..76ec046 100644 --- a/web/schema.graphql +++ b/web/schema.graphql @@ -241,12 +241,10 @@ input QueryFilterValueInput { floatValue: Float = null floatMin: Float = null floatMax: Float = null - intValue: Int = null boolValue: Boolean = null - dateValue: Date = null + dateValue: DateTime = null dateMin: Date = null dateMax: Date = null - dateTimeValue: DateTime = null uuidValue: String = null uuidValues: [String!] = null } diff --git a/web/utils/tableStateToApi.ts b/web/utils/tableStateToApi.ts index 777e9ae..c914aaf 100644 --- a/web/utils/tableStateToApi.ts +++ b/web/utils/tableStateToApi.ts @@ -104,6 +104,28 @@ function toGraphqlDateInput(value: unknown): string | undefined { return undefined } +function localCalendarDateToIso(dateYmd: string): string | undefined { + const match = /^(\d{4})-(\d{2})-(\d{2})$/.exec(dateYmd) + if (!match?.[1] || !match[2] || !match[3]) return undefined + const y = Number(match[1]) + const m = Number(match[2]) + const d = Number(match[3]) + const dt = new Date(y, m - 1, d) + if (Number.isNaN(dt.getTime())) return undefined + return dt.toISOString() +} + +function filterDateValueForDataType(value: FilterValue): string | undefined { + const parameter = value.parameter + if (value.dataType === 'dateTime') { + if (parameter.compareDate == null) return undefined + return parameter.compareDate.toISOString() + } + const day = toGraphqlDateInput(parameter.compareDate) + if (!day) return undefined + return localCalendarDateToIso(day) +} + function toQueryFilterValue(value: FilterValue): QueryFilterValueInput { const parameter = value.parameter const raw = parameter as Record @@ -128,10 +150,9 @@ function toQueryFilterValue(value: FilterValue): QueryFilterValueInput { floatValue: parameter.compareValue, floatMin: parameter.minNumber, floatMax: parameter.maxNumber, - dateValue: toGraphqlDateInput(parameter.compareDate), + dateValue: filterDateValueForDataType(value), dateMin: toGraphqlDateInput(parameter.minDate), dateMax: toGraphqlDateInput(parameter.maxDate), - dateTimeValue: parameter.compareDate?.toISOString(), stringValues: searchTags.length > 0 ? searchTags : undefined, } if (value.dataType === 'singleTag' && value.operator === 'equals' && searchTags.length === 1) { From ea83ea710786720923120c5f4b15c81cb3c18d80 Mon Sep 17 00:00:00 2001 From: Felix Evers Date: Sun, 22 Mar 2026 22:03:25 +0100 Subject: [PATCH 12/20] add sorting list from backend --- backend/api/query/adapters/patient.py | 15 ++++++++++++++- backend/api/query/adapters/task.py | 18 +++++++++++++++++- backend/api/query/adapters/user.py | 5 ++++- backend/api/query/graphql_types.py | 10 ++++++++++ backend/api/query/metadata_service.py | 16 +++++++++++++++- web/api/gql/generated.ts | 6 ++++-- web/api/graphql/QueryableFields.graphql | 2 ++ web/components/tables/PatientList.tsx | 12 ++++++++++-- web/components/tables/TaskList.tsx | 12 ++++++++++-- web/components/tasks/TaskDataEditor.tsx | 21 +++++++++++---------- web/schema.graphql | 2 ++ web/utils/queryableFilterList.tsx | 16 +++++++++++++++- 12 files changed, 114 insertions(+), 21 deletions(-) diff --git a/backend/api/query/adapters/patient.py b/backend/api/query/adapters/patient.py index 4451751..3e4f745 100644 --- a/backend/api/query/adapters/patient.py +++ b/backend/api/query/adapters/patient.py @@ -11,7 +11,12 @@ ReferenceFilterMode, ) from api.query.field_ops import apply_ops_to_column -from api.query.graphql_types import QueryableChoiceMeta, QueryableField, QueryableRelationMeta +from api.query.graphql_types import ( + QueryableChoiceMeta, + QueryableField, + QueryableRelationMeta, + sort_directions_for, +) from api.query.inputs import QueryFilterClauseInput, QuerySearchInput, QuerySortClauseInput from api.query.property_sql import join_property_value from api.query.sql_expr import location_title_expr, patient_display_name_expr @@ -298,6 +303,7 @@ def build_patient_queryable_fields_static() -> list[QueryableField]: value_type=QueryableValueType.STRING, allowed_operators=str_ops, sortable=True, + sort_directions=sort_directions_for(True), searchable=True, ), QueryableField( @@ -307,6 +313,7 @@ def build_patient_queryable_fields_static() -> list[QueryableField]: value_type=QueryableValueType.STRING, allowed_operators=str_ops, sortable=True, + sort_directions=sort_directions_for(True), searchable=True, ), QueryableField( @@ -316,6 +323,7 @@ def build_patient_queryable_fields_static() -> list[QueryableField]: value_type=QueryableValueType.STRING, allowed_operators=str_ops, sortable=True, + sort_directions=sort_directions_for(True), searchable=True, ), QueryableField( @@ -325,6 +333,7 @@ def build_patient_queryable_fields_static() -> list[QueryableField]: value_type=QueryableValueType.STRING, allowed_operators=choice_ops, sortable=True, + sort_directions=sort_directions_for(True), searchable=False, choice=QueryableChoiceMeta( option_keys=state_keys, @@ -338,6 +347,7 @@ def build_patient_queryable_fields_static() -> list[QueryableField]: value_type=QueryableValueType.STRING, allowed_operators=choice_ops, sortable=True, + sort_directions=sort_directions_for(True), searchable=False, choice=QueryableChoiceMeta( option_keys=sex_keys, @@ -351,6 +361,7 @@ def build_patient_queryable_fields_static() -> list[QueryableField]: value_type=QueryableValueType.DATE, allowed_operators=date_ops, sortable=True, + sort_directions=sort_directions_for(True), searchable=False, ), QueryableField( @@ -360,6 +371,7 @@ def build_patient_queryable_fields_static() -> list[QueryableField]: value_type=QueryableValueType.STRING, allowed_operators=str_ops, sortable=True, + sort_directions=sort_directions_for(True), searchable=True, ), QueryableField( @@ -377,6 +389,7 @@ def build_patient_queryable_fields_static() -> list[QueryableField]: QueryOperator.IS_NOT_NULL, ], sortable=True, + sort_directions=sort_directions_for(True), searchable=False, relation=QueryableRelationMeta( target_entity="LocationNode", diff --git a/backend/api/query/adapters/task.py b/backend/api/query/adapters/task.py index 5899213..9802931 100644 --- a/backend/api/query/adapters/task.py +++ b/backend/api/query/adapters/task.py @@ -11,7 +11,12 @@ ReferenceFilterMode, ) from api.query.field_ops import apply_ops_to_column -from api.query.graphql_types import QueryableChoiceMeta, QueryableField, QueryableRelationMeta +from api.query.graphql_types import ( + QueryableChoiceMeta, + QueryableField, + QueryableRelationMeta, + sort_directions_for, +) from api.query.inputs import ( QueryFilterClauseInput, QuerySearchInput, @@ -400,6 +405,7 @@ def build_task_queryable_fields_static() -> list[QueryableField]: value_type=QueryableValueType.STRING, allowed_operators=str_ops, sortable=True, + sort_directions=sort_directions_for(True), searchable=True, ), QueryableField( @@ -409,6 +415,7 @@ def build_task_queryable_fields_static() -> list[QueryableField]: value_type=QueryableValueType.STRING, allowed_operators=str_ops, sortable=True, + sort_directions=sort_directions_for(True), searchable=True, ), QueryableField( @@ -418,6 +425,7 @@ def build_task_queryable_fields_static() -> list[QueryableField]: value_type=QueryableValueType.BOOLEAN, allowed_operators=bool_ops, sortable=True, + sort_directions=sort_directions_for(True), searchable=False, ), QueryableField( @@ -427,6 +435,7 @@ def build_task_queryable_fields_static() -> list[QueryableField]: value_type=QueryableValueType.DATETIME, allowed_operators=dt_ops, sortable=True, + sort_directions=sort_directions_for(True), searchable=False, ), QueryableField( @@ -436,6 +445,7 @@ def build_task_queryable_fields_static() -> list[QueryableField]: value_type=QueryableValueType.STRING, allowed_operators=prio_ops, sortable=True, + sort_directions=sort_directions_for(True), searchable=False, choice=QueryableChoiceMeta( option_keys=["P1", "P2", "P3", "P4"], @@ -449,6 +459,7 @@ def build_task_queryable_fields_static() -> list[QueryableField]: value_type=QueryableValueType.NUMBER, allowed_operators=num_ops, sortable=True, + sort_directions=sort_directions_for(True), searchable=False, ), QueryableField( @@ -458,6 +469,7 @@ def build_task_queryable_fields_static() -> list[QueryableField]: value_type=QueryableValueType.DATETIME, allowed_operators=dt_ops, sortable=True, + sort_directions=sort_directions_for(True), searchable=False, ), QueryableField( @@ -467,6 +479,7 @@ def build_task_queryable_fields_static() -> list[QueryableField]: value_type=QueryableValueType.DATETIME, allowed_operators=dt_ops, sortable=True, + sort_directions=sort_directions_for(True), searchable=False, ), QueryableField( @@ -476,6 +489,7 @@ def build_task_queryable_fields_static() -> list[QueryableField]: value_type=QueryableValueType.UUID, allowed_operators=ref_ops, sortable=True, + sort_directions=sort_directions_for(True), searchable=True, relation=QueryableRelationMeta( target_entity="Patient", @@ -494,6 +508,7 @@ def build_task_queryable_fields_static() -> list[QueryableField]: value_type=QueryableValueType.UUID, allowed_operators=ref_ops, sortable=True, + sort_directions=sort_directions_for(True), searchable=True, relation=QueryableRelationMeta( target_entity="User", @@ -512,6 +527,7 @@ def build_task_queryable_fields_static() -> list[QueryableField]: value_type=QueryableValueType.UUID, allowed_operators=ref_ops, sortable=True, + sort_directions=sort_directions_for(True), searchable=False, relation=QueryableRelationMeta( target_entity="LocationNode", diff --git a/backend/api/query/adapters/user.py b/backend/api/query/adapters/user.py index 750f1b0..c1fbebd 100644 --- a/backend/api/query/adapters/user.py +++ b/backend/api/query/adapters/user.py @@ -5,7 +5,7 @@ from api.inputs import SortDirection from api.query.enums import QueryOperator, QueryableFieldKind, QueryableValueType from api.query.field_ops import apply_ops_to_column -from api.query.graphql_types import QueryableField +from api.query.graphql_types import QueryableField, sort_directions_for from api.query.inputs import QueryFilterClauseInput, QuerySearchInput, QuerySortClauseInput from api.query.sql_expr import user_display_label_expr from database import models @@ -121,6 +121,7 @@ def build_user_queryable_fields_static() -> list[QueryableField]: value_type=QueryableValueType.STRING, allowed_operators=str_ops, sortable=True, + sort_directions=sort_directions_for(True), searchable=True, ), QueryableField( @@ -130,6 +131,7 @@ def build_user_queryable_fields_static() -> list[QueryableField]: value_type=QueryableValueType.STRING, allowed_operators=str_ops, sortable=True, + sort_directions=sort_directions_for(True), searchable=True, ), QueryableField( @@ -139,6 +141,7 @@ def build_user_queryable_fields_static() -> list[QueryableField]: value_type=QueryableValueType.STRING, allowed_operators=str_ops, sortable=True, + sort_directions=sort_directions_for(True), searchable=True, ), ] diff --git a/backend/api/query/graphql_types.py b/backend/api/query/graphql_types.py index 737766b..3464b19 100644 --- a/backend/api/query/graphql_types.py +++ b/backend/api/query/graphql_types.py @@ -1,5 +1,6 @@ import strawberry +from api.inputs import SortDirection from api.query.enums import ( QueryOperator, QueryableFieldKind, @@ -8,6 +9,10 @@ ) +def sort_directions_for(sortable: bool) -> list[SortDirection]: + return [SortDirection.ASC, SortDirection.DESC] if sortable else [] + + @strawberry.type class QueryableRelationMeta: target_entity: str @@ -30,7 +35,12 @@ class QueryableField: value_type: QueryableValueType allowed_operators: list[QueryOperator] sortable: bool + sort_directions: list[SortDirection] searchable: bool relation: QueryableRelationMeta | None = None choice: QueryableChoiceMeta | None = None property_definition_id: str | None = None + + @strawberry.field + def filterable(self) -> bool: + return len(self.allowed_operators) > 0 diff --git a/backend/api/query/metadata_service.py b/backend/api/query/metadata_service.py index 1a53d01..14375f3 100644 --- a/backend/api/query/metadata_service.py +++ b/backend/api/query/metadata_service.py @@ -11,7 +11,12 @@ QueryableValueType, ReferenceFilterMode, ) -from api.query.graphql_types import QueryableChoiceMeta, QueryableField, QueryableRelationMeta +from api.query.graphql_types import ( + QueryableChoiceMeta, + QueryableField, + QueryableRelationMeta, + sort_directions_for, +) from api.query.registry import PATIENT, TASK, USER from database import models @@ -118,6 +123,7 @@ def _property_definition_to_field(p: models.PropertyDefinition) -> QueryableFiel value_type=QueryableValueType.STRING, allowed_operators=_str_ops(), sortable=True, + sort_directions=sort_directions_for(True), searchable=True, property_definition_id=str(p.id), ) @@ -129,6 +135,7 @@ def _property_definition_to_field(p: models.PropertyDefinition) -> QueryableFiel value_type=QueryableValueType.NUMBER, allowed_operators=_num_ops(), sortable=True, + sort_directions=sort_directions_for(True), searchable=False, property_definition_id=str(p.id), ) @@ -140,6 +147,7 @@ def _property_definition_to_field(p: models.PropertyDefinition) -> QueryableFiel value_type=QueryableValueType.BOOLEAN, allowed_operators=_bool_ops(), sortable=True, + sort_directions=sort_directions_for(True), searchable=False, property_definition_id=str(p.id), ) @@ -151,6 +159,7 @@ def _property_definition_to_field(p: models.PropertyDefinition) -> QueryableFiel value_type=QueryableValueType.DATE, allowed_operators=_date_ops(), sortable=True, + sort_directions=sort_directions_for(True), searchable=False, property_definition_id=str(p.id), ) @@ -162,6 +171,7 @@ def _property_definition_to_field(p: models.PropertyDefinition) -> QueryableFiel value_type=QueryableValueType.DATETIME, allowed_operators=_dt_ops(), sortable=True, + sort_directions=sort_directions_for(True), searchable=False, property_definition_id=str(p.id), ) @@ -173,6 +183,7 @@ def _property_definition_to_field(p: models.PropertyDefinition) -> QueryableFiel value_type=QueryableValueType.STRING, allowed_operators=_choice_ops(), sortable=True, + sort_directions=sort_directions_for(True), searchable=False, property_definition_id=str(p.id), choice=QueryableChoiceMeta( @@ -188,6 +199,7 @@ def _property_definition_to_field(p: models.PropertyDefinition) -> QueryableFiel value_type=QueryableValueType.STRING_LIST, allowed_operators=_multi_choice_ops(), sortable=False, + sort_directions=sort_directions_for(False), searchable=False, property_definition_id=str(p.id), choice=QueryableChoiceMeta( @@ -203,6 +215,7 @@ def _property_definition_to_field(p: models.PropertyDefinition) -> QueryableFiel value_type=QueryableValueType.UUID, allowed_operators=_user_ref_ops(), sortable=True, + sort_directions=sort_directions_for(True), searchable=False, property_definition_id=str(p.id), relation=QueryableRelationMeta( @@ -219,6 +232,7 @@ def _property_definition_to_field(p: models.PropertyDefinition) -> QueryableFiel value_type=QueryableValueType.STRING, allowed_operators=_str_ops(), sortable=True, + sort_directions=sort_directions_for(True), searchable=True, property_definition_id=str(p.id), ) diff --git a/web/api/gql/generated.ts b/web/api/gql/generated.ts index e192f98..118ca9f 100644 --- a/web/api/gql/generated.ts +++ b/web/api/gql/generated.ts @@ -596,12 +596,14 @@ export type QueryableField = { __typename?: 'QueryableField'; allowedOperators: Array; choice?: Maybe; + filterable: Scalars['Boolean']['output']; key: Scalars['String']['output']; kind: QueryableFieldKind; label: Scalars['String']['output']; propertyDefinitionId?: Maybe; relation?: Maybe; searchable: Scalars['Boolean']['output']; + sortDirections: Array; sortable: Scalars['Boolean']['output']; valueType: QueryableValueType; }; @@ -1030,7 +1032,7 @@ export type QueryableFieldsQueryVariables = Exact<{ }>; -export type QueryableFieldsQuery = { __typename?: 'Query', queryableFields: Array<{ __typename?: 'QueryableField', key: string, label: string, kind: QueryableFieldKind, valueType: QueryableValueType, allowedOperators: Array, sortable: boolean, searchable: boolean, propertyDefinitionId?: string | null, relation?: { __typename?: 'QueryableRelationMeta', targetEntity: string, idFieldKey: string, labelFieldKey: string, allowedFilterModes: Array } | null, choice?: { __typename?: 'QueryableChoiceMeta', optionKeys: Array, optionLabels: Array } | null }> }; +export type QueryableFieldsQuery = { __typename?: 'Query', queryableFields: Array<{ __typename?: 'QueryableField', key: string, label: string, kind: QueryableFieldKind, valueType: QueryableValueType, allowedOperators: Array, sortable: boolean, sortDirections: Array, searchable: boolean, filterable: boolean, propertyDefinitionId?: string | null, relation?: { __typename?: 'QueryableRelationMeta', targetEntity: string, idFieldKey: string, labelFieldKey: string, allowedFilterModes: Array } | null, choice?: { __typename?: 'QueryableChoiceMeta', optionKeys: Array, optionLabels: Array } | null }> }; export type MySavedViewsQueryVariables = Exact<{ [key: string]: never; }>; @@ -1234,7 +1236,7 @@ export const UpdatePropertyDefinitionDocument = {"kind":"Document","definitions" export const DeletePropertyDefinitionDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"DeletePropertyDefinition"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"id"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"ID"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"deletePropertyDefinition"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"id"},"value":{"kind":"Variable","name":{"kind":"Name","value":"id"}}}]}]}}]} as unknown as DocumentNode; export const GetPropertyDefinitionsDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"GetPropertyDefinitions"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"propertyDefinitions"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"description"}},{"kind":"Field","name":{"kind":"Name","value":"fieldType"}},{"kind":"Field","name":{"kind":"Name","value":"isActive"}},{"kind":"Field","name":{"kind":"Name","value":"allowedEntities"}},{"kind":"Field","name":{"kind":"Name","value":"options"}}]}}]}}]} as unknown as DocumentNode; export const GetPropertiesForSubjectDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"GetPropertiesForSubject"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"subjectId"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"ID"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"subjectType"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"PropertyEntity"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"propertyDefinitions"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"description"}},{"kind":"Field","name":{"kind":"Name","value":"fieldType"}},{"kind":"Field","name":{"kind":"Name","value":"isActive"}},{"kind":"Field","name":{"kind":"Name","value":"allowedEntities"}},{"kind":"Field","name":{"kind":"Name","value":"options"}}]}}]}}]} as unknown as DocumentNode; -export const QueryableFieldsDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"QueryableFields"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"entity"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"queryableFields"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"entity"},"value":{"kind":"Variable","name":{"kind":"Name","value":"entity"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"key"}},{"kind":"Field","name":{"kind":"Name","value":"label"}},{"kind":"Field","name":{"kind":"Name","value":"kind"}},{"kind":"Field","name":{"kind":"Name","value":"valueType"}},{"kind":"Field","name":{"kind":"Name","value":"allowedOperators"}},{"kind":"Field","name":{"kind":"Name","value":"sortable"}},{"kind":"Field","name":{"kind":"Name","value":"searchable"}},{"kind":"Field","name":{"kind":"Name","value":"propertyDefinitionId"}},{"kind":"Field","name":{"kind":"Name","value":"relation"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"targetEntity"}},{"kind":"Field","name":{"kind":"Name","value":"idFieldKey"}},{"kind":"Field","name":{"kind":"Name","value":"labelFieldKey"}},{"kind":"Field","name":{"kind":"Name","value":"allowedFilterModes"}}]}},{"kind":"Field","name":{"kind":"Name","value":"choice"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"optionKeys"}},{"kind":"Field","name":{"kind":"Name","value":"optionLabels"}}]}}]}}]}}]} as unknown as DocumentNode; +export const QueryableFieldsDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"QueryableFields"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"entity"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"queryableFields"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"entity"},"value":{"kind":"Variable","name":{"kind":"Name","value":"entity"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"key"}},{"kind":"Field","name":{"kind":"Name","value":"label"}},{"kind":"Field","name":{"kind":"Name","value":"kind"}},{"kind":"Field","name":{"kind":"Name","value":"valueType"}},{"kind":"Field","name":{"kind":"Name","value":"allowedOperators"}},{"kind":"Field","name":{"kind":"Name","value":"sortable"}},{"kind":"Field","name":{"kind":"Name","value":"sortDirections"}},{"kind":"Field","name":{"kind":"Name","value":"searchable"}},{"kind":"Field","name":{"kind":"Name","value":"filterable"}},{"kind":"Field","name":{"kind":"Name","value":"propertyDefinitionId"}},{"kind":"Field","name":{"kind":"Name","value":"relation"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"targetEntity"}},{"kind":"Field","name":{"kind":"Name","value":"idFieldKey"}},{"kind":"Field","name":{"kind":"Name","value":"labelFieldKey"}},{"kind":"Field","name":{"kind":"Name","value":"allowedFilterModes"}}]}},{"kind":"Field","name":{"kind":"Name","value":"choice"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"optionKeys"}},{"kind":"Field","name":{"kind":"Name","value":"optionLabels"}}]}}]}}]}}]} as unknown as DocumentNode; export const MySavedViewsDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"MySavedViews"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"mySavedViews"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"baseEntityType"}},{"kind":"Field","name":{"kind":"Name","value":"filterDefinition"}},{"kind":"Field","name":{"kind":"Name","value":"sortDefinition"}},{"kind":"Field","name":{"kind":"Name","value":"parameters"}},{"kind":"Field","name":{"kind":"Name","value":"ownerUserId"}},{"kind":"Field","name":{"kind":"Name","value":"visibility"}},{"kind":"Field","name":{"kind":"Name","value":"createdAt"}},{"kind":"Field","name":{"kind":"Name","value":"updatedAt"}},{"kind":"Field","name":{"kind":"Name","value":"isOwner"}}]}}]}}]} as unknown as DocumentNode; export const SavedViewDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"SavedView"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"id"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"ID"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"savedView"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"id"},"value":{"kind":"Variable","name":{"kind":"Name","value":"id"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"baseEntityType"}},{"kind":"Field","name":{"kind":"Name","value":"filterDefinition"}},{"kind":"Field","name":{"kind":"Name","value":"sortDefinition"}},{"kind":"Field","name":{"kind":"Name","value":"parameters"}},{"kind":"Field","name":{"kind":"Name","value":"ownerUserId"}},{"kind":"Field","name":{"kind":"Name","value":"visibility"}},{"kind":"Field","name":{"kind":"Name","value":"createdAt"}},{"kind":"Field","name":{"kind":"Name","value":"updatedAt"}},{"kind":"Field","name":{"kind":"Name","value":"isOwner"}}]}}]}}]} as unknown as DocumentNode; export const CreateSavedViewDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"CreateSavedView"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"data"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"CreateSavedViewInput"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"createSavedView"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"data"},"value":{"kind":"Variable","name":{"kind":"Name","value":"data"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"baseEntityType"}},{"kind":"Field","name":{"kind":"Name","value":"filterDefinition"}},{"kind":"Field","name":{"kind":"Name","value":"sortDefinition"}},{"kind":"Field","name":{"kind":"Name","value":"parameters"}},{"kind":"Field","name":{"kind":"Name","value":"ownerUserId"}},{"kind":"Field","name":{"kind":"Name","value":"visibility"}},{"kind":"Field","name":{"kind":"Name","value":"createdAt"}},{"kind":"Field","name":{"kind":"Name","value":"updatedAt"}},{"kind":"Field","name":{"kind":"Name","value":"isOwner"}}]}}]}}]} as unknown as DocumentNode; diff --git a/web/api/graphql/QueryableFields.graphql b/web/api/graphql/QueryableFields.graphql index 8e4fe72..b5b78db 100644 --- a/web/api/graphql/QueryableFields.graphql +++ b/web/api/graphql/QueryableFields.graphql @@ -6,7 +6,9 @@ query QueryableFields($entity: String!) { valueType allowedOperators sortable + sortDirections searchable + filterable propertyDefinitionId relation { targetEntity diff --git a/web/components/tables/PatientList.tsx b/web/components/tables/PatientList.tsx index 5fb3d38..5961c87 100644 --- a/web/components/tables/PatientList.tsx +++ b/web/components/tables/PatientList.tsx @@ -17,7 +17,7 @@ import { getPropertyColumnsForEntity } from '@/utils/propertyColumn' import { useStorageSyncedTableState } from '@/hooks/useTableState' import { usePropertyColumnVisibility } from '@/hooks/usePropertyColumnVisibility' import { columnFiltersToQueryFilterClauses, paginationStateToPaginationInput, sortingStateToQuerySortClauses } from '@/utils/tableStateToApi' -import { queryableFieldsToFilterListItems } from '@/utils/queryableFilterList' +import { queryableFieldsToFilterListItems, queryableFieldsToSortingListItems } from '@/utils/queryableFilterList' import { getPropertyFilterFn as getPropertyDatatype } from '@/utils/propertyFilterMapping' import { UserSelectFilterPopUp } from './UserSelectFilterPopUp' import { SaveViewDialog } from '@/components/views/SaveViewDialog' @@ -482,6 +482,14 @@ export const PatientList = forwardRef(({ initi ] }, [queryableFieldsData?.queryableFields, propertyFieldTypeByDefId, translation, allPatientStates, propertyDefinitionsData?.propertyDefinitions]) + const availableSortItems = useMemo(() => { + const raw = queryableFieldsData?.queryableFields + if (raw?.length) { + return queryableFieldsToSortingListItems(raw) + } + return availableFilters.map(({ id, label, dataType }) => ({ id, label, dataType })) + }, [queryableFieldsData?.queryableFields, availableFilters]) + const onRowClick = useCallback((row: Row) => handleEdit(row.original), [handleEdit]) const fillerRowCell = useCallback(() => (), []) @@ -574,7 +582,7 @@ export const PatientList = forwardRef(({ initi )}
diff --git a/web/components/tables/TaskList.tsx b/web/components/tables/TaskList.tsx index 14f7461..b52b318 100644 --- a/web/components/tables/TaskList.tsx +++ b/web/components/tables/TaskList.tsx @@ -24,7 +24,7 @@ import { PriorityUtils } from '@/utils/priority' import { getPropertyColumnsForEntity } from '@/utils/propertyColumn' import { useStorageSyncedTableState } from '@/hooks/useTableState' import { usePropertyColumnVisibility } from '@/hooks/usePropertyColumnVisibility' -import { queryableFieldsToFilterListItems } from '@/utils/queryableFilterList' +import { queryableFieldsToFilterListItems, queryableFieldsToSortingListItems } from '@/utils/queryableFilterList' export type TaskViewModel = { id: string, @@ -345,6 +345,14 @@ export const TaskList = forwardRef(({ tasks: initial ] }, [queryableFieldsData?.queryableFields, propertyFieldTypeByDefId, translation, propertyDefinitionsData?.propertyDefinitions]) + const availableSortItems = useMemo(() => { + const raw = queryableFieldsData?.queryableFields + if (raw?.length) { + return queryableFieldsToSortingListItems(raw) + } + return availableFilters.map(({ id, label, dataType }) => ({ id, label, dataType })) + }, [queryableFieldsData?.queryableFields, availableFilters]) + const rowLoadingCell = useMemo(() => , []) const columns = useMemo[]>(() => { @@ -639,7 +647,7 @@ export const TaskList = forwardRef(({ tasks: initial )}
diff --git a/web/components/tasks/TaskDataEditor.tsx b/web/components/tasks/TaskDataEditor.tsx index ae96d02..b058efd 100644 --- a/web/components/tasks/TaskDataEditor.tsx +++ b/web/components/tasks/TaskDataEditor.tsx @@ -19,7 +19,8 @@ import { Drawer, useFormObserverKey, Visibility, - FormObserver + FormObserver, + FlexibleDateTimeInput } from '@helpwave/hightide' import { CenteredLoadingLogo } from '@/components/CenteredLoadingLogo' import { useTasksContext } from '@/hooks/useTasksContext' @@ -86,7 +87,7 @@ export const TaskDataEditor = ({ message: err instanceof Error ? err.message : 'Update failed', }) }, - }).catch(() => {}) + }).catch(() => { }) } const [deleteTask, { loading: isDeleting }] = useDeleteTask() @@ -284,8 +285,8 @@ export const TaskDataEditor = ({ onClick={() => setIsShowingPatientDialog(true)} className="w-fit" > - - { taskData?.patient?.name} + + {taskData?.patient?.name}
) @@ -311,17 +312,17 @@ export const TaskDataEditor = ({ unassignTask({ variables: { id: taskId }, onCompleted: () => onSuccess?.(), - }).catch(() => {}) + }).catch(() => { }) } else if (value.startsWith('team:')) { assignTaskToTeam({ variables: { id: taskId, teamId: value.replace('team:', '') }, onCompleted: () => onSuccess?.(), - }).catch(() => {}) + }).catch(() => { }) } else { assignTask({ variables: { id: taskId, userId: value }, onCompleted: () => onSuccess?.(), - }).catch(() => {}) + }).catch(() => { }) } } }} @@ -336,10 +337,10 @@ export const TaskDataEditor = ({ label={translation('dueDate')} > {({ dataProps, focusableElementProps, interactionStates }) => ( - )} @@ -469,7 +470,7 @@ export const TaskDataEditor = ({ setIsShowingPatientDialog(false)} - onSuccess={() => {}} + onSuccess={() => { }} /> diff --git a/web/schema.graphql b/web/schema.graphql index 76ec046..0ee985a 100644 --- a/web/schema.graphql +++ b/web/schema.graphql @@ -294,7 +294,9 @@ type QueryableField { valueType: QueryableValueType! allowedOperators: [QueryOperator!]! sortable: Boolean! + sortDirections: [SortDirection!]! searchable: Boolean! + filterable: Boolean! relation: QueryableRelationMeta choice: QueryableChoiceMeta propertyDefinitionId: String diff --git a/web/utils/queryableFilterList.tsx b/web/utils/queryableFilterList.tsx index a15b79f..f546846 100644 --- a/web/utils/queryableFilterList.tsx +++ b/web/utils/queryableFilterList.tsx @@ -18,11 +18,13 @@ function valueKindToDataType(field: QueryableField): DataType { return 'text' } +export type QueryableSortListItem = Pick + export function queryableFieldsToFilterListItems( fields: QueryableField[], propertyFieldTypeByDefId: Map ): FilterListItem[] { - return fields.map((field): FilterListItem => { + return fields.filter(field => field.filterable).map((field): FilterListItem => { const dataType = valueKindToDataType(field) const tags = field.choice ? field.choice.optionLabels.map((label, idx) => ({ @@ -46,3 +48,15 @@ export function queryableFieldsToFilterListItems( } }) } + +export function queryableFieldsToSortingListItems( + fields: QueryableField[] +): QueryableSortListItem[] { + return fields + .filter(field => field.sortable && field.sortDirections.length > 0) + .map((field): QueryableSortListItem => ({ + id: field.key, + label: field.label, + dataType: valueKindToDataType(field), + })) +} From 7fe261eda2dd690da67c8d5c50ab3f8ceaf7e72c Mon Sep 17 00:00:00 2001 From: Felix Evers Date: Sun, 22 Mar 2026 23:46:14 +0100 Subject: [PATCH 13/20] expand saved views --- backend/api/query/adapters/patient.py | 2 +- backend/api/query/adapters/task.py | 2 +- backend/api/query/field_ops.py | 1 - ...ba_merge_location_type_enum_and_remove_.py | 2 - ...438_merge_patient_description_and_task_.py | 2 - web/components/layout/Page.tsx | 7 +- web/components/tables/PatientList.tsx | 199 ++++++++++--- web/components/tables/RecentPatientsTable.tsx | 28 +- web/components/tables/RecentTasksTable.tsx | 28 +- web/components/tables/TaskList.tsx | 50 ++-- .../tables/UserSelectFilterPopUp.tsx | 8 +- web/components/tasks/TaskDataEditor.tsx | 1 - web/components/views/SaveViewActionsMenu.tsx | 74 +++++ web/components/views/SaveViewDialog.tsx | 6 +- .../views/SavedViewEntityTypeChip.tsx | 31 ++ web/globals.css | 2 +- web/hooks/usePropertyColumnVisibility.ts | 64 +++-- web/hooks/useTableState.ts | 114 +++----- web/i18n/translations.ts | 163 +++++++---- web/locales/de-DE.arb | 27 +- web/locales/en-US.arb | 27 +- web/locales/es-ES.arb | 27 +- web/locales/fr-FR.arb | 27 +- web/locales/nl-NL.arb | 23 +- web/locales/pt-BR.arb | 23 +- web/package-lock.json | 8 +- web/package.json | 2 +- web/pages/settings/index.tsx | 16 +- web/pages/settings/views.tsx | 52 ++-- web/pages/tasks/index.tsx | 104 +++++-- web/pages/view/[uid].tsx | 270 +++++++++++++++--- web/utils/tableStateToApi.ts | 24 +- web/utils/viewDefinition.ts | 142 +++++++-- 33 files changed, 1122 insertions(+), 434 deletions(-) create mode 100644 web/components/views/SaveViewActionsMenu.tsx create mode 100644 web/components/views/SavedViewEntityTypeChip.tsx diff --git a/backend/api/query/adapters/patient.py b/backend/api/query/adapters/patient.py index 3e4f745..232d2d4 100644 --- a/backend/api/query/adapters/patient.py +++ b/backend/api/query/adapters/patient.py @@ -1,6 +1,6 @@ from typing import Any -from sqlalchemy import Select, and_, case, func, or_, select +from sqlalchemy import Select, and_, case, or_ from sqlalchemy.orm import aliased from api.inputs import PatientState, Sex, SortDirection diff --git a/backend/api/query/adapters/task.py b/backend/api/query/adapters/task.py index 9802931..35805e5 100644 --- a/backend/api/query/adapters/task.py +++ b/backend/api/query/adapters/task.py @@ -1,6 +1,6 @@ from typing import Any -from sqlalchemy import Select, and_, case, func, or_, select +from sqlalchemy import Select, and_, case, or_ from sqlalchemy.orm import aliased from api.inputs import SortDirection diff --git a/backend/api/query/field_ops.py b/backend/api/query/field_ops.py index 32c6494..e6a63d1 100644 --- a/backend/api/query/field_ops.py +++ b/backend/api/query/field_ops.py @@ -1,4 +1,3 @@ -from datetime import date, datetime from typing import Any from sqlalchemy import String, and_, cast, func, not_, or_ diff --git a/backend/database/migrations/versions/0de3078888ba_merge_location_type_enum_and_remove_.py b/backend/database/migrations/versions/0de3078888ba_merge_location_type_enum_and_remove_.py index 1b31f78..df72dde 100644 --- a/backend/database/migrations/versions/0de3078888ba_merge_location_type_enum_and_remove_.py +++ b/backend/database/migrations/versions/0de3078888ba_merge_location_type_enum_and_remove_.py @@ -7,8 +7,6 @@ """ from typing import Sequence, Union -from alembic import op -import sqlalchemy as sa # revision identifiers, used by Alembic. diff --git a/backend/database/migrations/versions/81ffadeee438_merge_patient_description_and_task_.py b/backend/database/migrations/versions/81ffadeee438_merge_patient_description_and_task_.py index 6b44a7a..f84853b 100644 --- a/backend/database/migrations/versions/81ffadeee438_merge_patient_description_and_task_.py +++ b/backend/database/migrations/versions/81ffadeee438_merge_patient_description_and_task_.py @@ -7,8 +7,6 @@ """ from typing import Sequence, Union -from alembic import op -import sqlalchemy as sa # revision identifiers, used by Alembic. diff --git a/web/components/layout/Page.tsx b/web/components/layout/Page.tsx index 75704aa..7afa047 100644 --- a/web/components/layout/Page.tsx +++ b/web/components/layout/Page.tsx @@ -32,7 +32,7 @@ import { Menu as MenuIcon, X, MessageSquare, - LayoutList + Rabbit } from 'lucide-react' import { TasksLogo } from '@/components/TasksLogo' import { useRouter } from 'next/router' @@ -561,7 +561,7 @@ export const Sidebar = ({ isOpen, onClose, ...props }: SidebarProps) => { >
- + {translation('savedViews')}
@@ -571,9 +571,6 @@ export const Sidebar = ({ isOpen, onClose, ...props }: SidebarProps) => { {v.name} ))} - - {translation('viewSettings')} -
{(context?.teams?.length ?? 0) > 0 && ( diff --git a/web/components/tables/PatientList.tsx b/web/components/tables/PatientList.tsx index 5961c87..02f26d0 100644 --- a/web/components/tables/PatientList.tsx +++ b/web/components/tables/PatientList.tsx @@ -1,4 +1,5 @@ import { useMemo, useState, forwardRef, useImperativeHandle, useEffect, useCallback, useRef } from 'react' +import { useMutation } from '@apollo/client/react' import type { IdentifierFilterValue, FilterListItem, FilterListPopUpBuilderProps } from '@helpwave/hightide' import { Chip, FillerCell, HelpwaveLogo, LoadingContainer, SearchBar, ProgressIndicator, Tooltip, Drawer, TableProvider, TableDisplay, TableColumnSwitcher, TablePagination, IconButton, useLocale, FilterList, SortingList, Button, ExpansionIcon, Visibility } from '@helpwave/hightide' import { PlusIcon } from 'lucide-react' @@ -12,17 +13,32 @@ import { PatientStateChip } from '@/components/patients/PatientStateChip' import { getLocationNodesByKind, type LocationKindColumn } from '@/utils/location' import { useTasksTranslation } from '@/i18n/useTasksTranslation' import { useTasksContext } from '@/hooks/useTasksContext' -import type { ColumnDef, ColumnFiltersState, Row, SortingState, TableState } from '@tanstack/table-core' +import type { ColumnDef, ColumnFiltersState, ColumnOrderState, PaginationState, Row, SortingState, TableState, VisibilityState } from '@tanstack/table-core' import { getPropertyColumnsForEntity } from '@/utils/propertyColumn' -import { useStorageSyncedTableState } from '@/hooks/useTableState' -import { usePropertyColumnVisibility } from '@/hooks/usePropertyColumnVisibility' +import { getPropertyColumnIds, useColumnVisibilityWithPropertyDefaults } from '@/hooks/usePropertyColumnVisibility' import { columnFiltersToQueryFilterClauses, paginationStateToPaginationInput, sortingStateToQuerySortClauses } from '@/utils/tableStateToApi' import { queryableFieldsToFilterListItems, queryableFieldsToSortingListItems } from '@/utils/queryableFilterList' import { getPropertyFilterFn as getPropertyDatatype } from '@/utils/propertyFilterMapping' import { UserSelectFilterPopUp } from './UserSelectFilterPopUp' import { SaveViewDialog } from '@/components/views/SaveViewDialog' -import { SavedViewEntityType } from '@/api/gql/generated' -import { serializeColumnFiltersForView, serializeSortingForView, stringifyViewParameters } from '@/utils/viewDefinition' +import { SaveViewActionsMenu } from '@/components/views/SaveViewActionsMenu' +import { + MySavedViewsDocument, + SavedViewDocument, + SavedViewEntityType, + UpdateSavedViewDocument, + type UpdateSavedViewMutation, + type UpdateSavedViewMutationVariables +} from '@/api/gql/generated' +import { getParsedDocument } from '@/data/hooks/queryHelpers' +import { + normalizedColumnOrderForViewCompare, + normalizedVisibilityForViewCompare, + serializeColumnFiltersForView, + serializeSortingForView, + stringifyViewParameters, + tableViewStateMatchesBaseline +} from '@/utils/viewDefinition' import type { ViewParameters } from '@/utils/viewDefinition' export type PatientViewModel = { @@ -60,17 +76,19 @@ type PatientListProps = { acceptedStates?: PatientState[], rootLocationIds?: string[], locationId?: string, - /** Isolated storage namespace (e.g. per saved view). */ - storageKeyPrefix?: string, viewDefaultFilters?: ColumnFiltersState, viewDefaultSorting?: SortingState, viewDefaultSearchQuery?: string, + viewDefaultColumnVisibility?: VisibilityState, + viewDefaultColumnOrder?: ColumnOrderState, readOnly?: boolean, hideSaveView?: boolean, + /** When set (e.g. on `/view/:id`), overwrite updates this saved view. */ + savedViewId?: string, onSavedViewCreated?: (id: string) => void, } -export const PatientList = forwardRef(({ initialPatientId, onInitialPatientOpened, acceptedStates: _acceptedStates, rootLocationIds, locationId, storageKeyPrefix, viewDefaultFilters, viewDefaultSorting, viewDefaultSearchQuery, readOnly: _readOnly, hideSaveView, onSavedViewCreated }, ref) => { +export const PatientList = forwardRef(({ initialPatientId, onInitialPatientOpened, acceptedStates: _acceptedStates, rootLocationIds, locationId, viewDefaultFilters, viewDefaultSorting, viewDefaultSearchQuery, viewDefaultColumnVisibility, viewDefaultColumnOrder, readOnly: _readOnly, hideSaveView, savedViewId, onSavedViewCreated }, ref) => { const translation = useTasksTranslation() const { locale } = useLocale() const { selectedRootLocationIds } = useTasksContext() @@ -85,42 +103,139 @@ export const PatientList = forwardRef(({ initi const [isShowFilters, setIsShowFilters] = useState(false) const [isShowSorting, setIsShowSorting] = useState(false) - const { - pagination, - setPagination, - sorting, - setSorting, - filters, - setFilters, - columnVisibility, - setColumnVisibility, - } = useStorageSyncedTableState(storageKeyPrefix ?? 'patient-list', { - defaultFilters: viewDefaultFilters, - defaultSorting: viewDefaultSorting, - }) + const [pagination, setPagination] = useState({ pageSize: 10, pageIndex: 0 }) + const [sorting, setSorting] = useState(() => viewDefaultSorting ?? []) + const [filters, setFilters] = useState(() => viewDefaultFilters ?? []) + const [columnVisibility, setColumnVisibilityRaw] = useState(() => viewDefaultColumnVisibility ?? {}) + const [columnOrder, setColumnOrder] = useState(() => viewDefaultColumnOrder ?? []) + + const setColumnVisibility = useColumnVisibilityWithPropertyDefaults( + propertyDefinitionsData, + PropertyEntity.Patient, + setColumnVisibilityRaw + ) const baselineFilters = useMemo(() => viewDefaultFilters ?? [], [viewDefaultFilters]) const baselineSorting = useMemo(() => viewDefaultSorting ?? [], [viewDefaultSorting]) const baselineSearch = useMemo(() => viewDefaultSearchQuery ?? '', [viewDefaultSearchQuery]) + const baselineColumnVisibility = useMemo(() => viewDefaultColumnVisibility ?? {}, [viewDefaultColumnVisibility]) + const baselineColumnOrder = useMemo(() => viewDefaultColumnOrder ?? [], [viewDefaultColumnOrder]) - const filtersChanged = useMemo( - () => serializeColumnFiltersForView(filters as ColumnFiltersState) !== serializeColumnFiltersForView(baselineFilters), - [filters, baselineFilters] + const propertyColumnIds = useMemo( + () => getPropertyColumnIds(propertyDefinitionsData, PropertyEntity.Patient), + [propertyDefinitionsData] ) - const sortingChanged = useMemo( - () => serializeSortingForView(sorting) !== serializeSortingForView(baselineSorting), - [sorting, baselineSorting] + + const persistedSavedViewContentKey = useMemo( + () => + `${serializeColumnFiltersForView(baselineFilters)}|${serializeSortingForView(baselineSorting)}|${baselineSearch}|${normalizedVisibilityForViewCompare(baselineColumnVisibility)}|${normalizedColumnOrderForViewCompare(baselineColumnOrder)}`, + [baselineFilters, baselineSorting, baselineSearch, baselineColumnVisibility, baselineColumnOrder] ) - const searchChanged = useMemo(() => searchQuery !== baselineSearch, [searchQuery, baselineSearch]) + + useEffect(() => { + if (!savedViewId) { + return + } + setFilters(baselineFilters) + setSorting(baselineSorting) + setSearchQuery(baselineSearch) + setColumnVisibility(baselineColumnVisibility) + setColumnOrder(baselineColumnOrder) + setPagination({ pageSize: 10, pageIndex: 0 }) + }, [savedViewId, persistedSavedViewContentKey]) + + const viewMatchesBaseline = useMemo( + () => tableViewStateMatchesBaseline({ + filters: filters as ColumnFiltersState, + baselineFilters, + sorting, + baselineSorting, + searchQuery, + baselineSearch, + columnVisibility, + baselineColumnVisibility, + columnOrder, + baselineColumnOrder, + propertyColumnIds, + }), + [ + filters, + baselineFilters, + sorting, + baselineSorting, + searchQuery, + baselineSearch, + columnVisibility, + baselineColumnVisibility, + columnOrder, + baselineColumnOrder, + propertyColumnIds, + ] + ) + const hasUnsavedViewChanges = !viewMatchesBaseline const [isSaveViewDialogOpen, setIsSaveViewDialogOpen] = useState(false) - usePropertyColumnVisibility( - propertyDefinitionsData, - PropertyEntity.Patient, + const [updateSavedView, { loading: overwriteLoading }] = useMutation< + UpdateSavedViewMutation, + UpdateSavedViewMutationVariables + >(getParsedDocument(UpdateSavedViewDocument), { + refetchQueries: savedViewId + ? [ + { query: getParsedDocument(SavedViewDocument), variables: { id: savedViewId } }, + { query: getParsedDocument(MySavedViewsDocument) }, + ] + : [{ query: getParsedDocument(MySavedViewsDocument) }], + }) + + const handleDiscardViewChanges = useCallback(() => { + setFilters(baselineFilters) + setSorting(baselineSorting) + setSearchQuery(baselineSearch) + setColumnVisibility(baselineColumnVisibility) + setColumnOrder(baselineColumnOrder) + }, [ + baselineFilters, + baselineSorting, + baselineSearch, + baselineColumnVisibility, + baselineColumnOrder, + setFilters, + setSorting, + setSearchQuery, + setColumnVisibility, + setColumnOrder, + ]) + + const handleOverwriteSavedView = useCallback(async () => { + if (!savedViewId) return + await updateSavedView({ + variables: { + id: savedViewId, + data: { + filterDefinition: serializeColumnFiltersForView(filters as ColumnFiltersState), + sortDefinition: serializeSortingForView(sorting), + parameters: stringifyViewParameters({ + rootLocationIds: effectiveRootLocationIds ?? undefined, + locationId: locationId ?? undefined, + searchQuery: searchQuery || undefined, + columnVisibility, + columnOrder, + } satisfies ViewParameters), + }, + }, + }) + }, [ + savedViewId, + updateSavedView, + filters, + sorting, + effectiveRootLocationIds, + locationId, + searchQuery, columnVisibility, - setColumnVisibility - ) + columnOrder, + ]) const allPatientStates: PatientState[] = useMemo(() => [ PatientState.Admitted, @@ -507,9 +622,11 @@ export const PatientList = forwardRef(({ initi }} state={{ columnVisibility, + columnOrder, pagination, } as Partial as TableState} onColumnVisibilityChange={setColumnVisibility} + onColumnOrderChange={setColumnOrder} onPaginationChange={setPagination} onSortingChange={setSorting} onColumnFiltersChange={setFilters} @@ -553,12 +670,15 @@ export const PatientList = forwardRef(({ initi {translation('sorting') + ` (${sorting.length})`} - - + + setIsSaveViewDialogOpen(true)} + onDiscard={handleDiscardViewChanges} + /> - {/* TODO Offer undo in case this is already a fast access and add a update button */}
(({ initi rootLocationIds: effectiveRootLocationIds ?? undefined, locationId: locationId ?? undefined, searchQuery: searchQuery || undefined, + columnVisibility, + columnOrder, } satisfies ViewParameters)} + presentation={savedViewId ? 'default' : 'fromSystemList'} onCreated={onSavedViewCreated} /> diff --git a/web/components/tables/RecentPatientsTable.tsx b/web/components/tables/RecentPatientsTable.tsx index 53140c1..d55ba24 100644 --- a/web/components/tables/RecentPatientsTable.tsx +++ b/web/components/tables/RecentPatientsTable.tsx @@ -1,7 +1,7 @@ import { useTasksTranslation } from '@/i18n/useTasksTranslation' -import type { ColumnDef, Row, TableState } from '@tanstack/react-table' +import type { ColumnDef, ColumnFiltersState, PaginationState, Row, SortingState, TableState, VisibilityState } from '@tanstack/react-table' import type { GetOverviewDataQuery } from '@/api/gql/generated' -import { useCallback, useMemo } from 'react' +import { useCallback, useMemo, useState } from 'react' import type { TableProps } from '@helpwave/hightide' import { FillerCell, TableDisplay, TableProvider, Tooltip } from '@helpwave/hightide' import { DateDisplay } from '@/components/Date/DateDisplay' @@ -9,8 +9,7 @@ import { LocationChipsBySetting } from '@/components/patients/LocationChipsBySet import { PropertyEntity } from '@/api/gql/generated' import { usePropertyDefinitions } from '@/data' import { getPropertyColumnsForEntity } from '@/utils/propertyColumn' -import { useStorageSyncedTableState } from '@/hooks/useTableState' -import { usePropertyColumnVisibility } from '@/hooks/usePropertyColumnVisibility' +import { useColumnVisibilityWithPropertyDefaults } from '@/hooks/usePropertyColumnVisibility' type PatientViewModel = GetOverviewDataQuery['recentPatients'][0] @@ -27,22 +26,15 @@ export const RecentPatientsTable = ({ const translation = useTasksTranslation() const { data: propertyDefinitionsData } = usePropertyDefinitions() - const { - pagination, - setPagination, - sorting, - setSorting, - filters, - setFilters, - columnVisibility, - setColumnVisibility, - } = useStorageSyncedTableState('recent-patients') + const [pagination, setPagination] = useState({ pageSize: 10, pageIndex: 0 }) + const [sorting, setSorting] = useState([]) + const [filters, setFilters] = useState([]) + const [columnVisibility, setColumnVisibilityRaw] = useState({}) - usePropertyColumnVisibility( + const setColumnVisibility = useColumnVisibilityWithPropertyDefaults( propertyDefinitionsData, PropertyEntity.Patient, - columnVisibility, - setColumnVisibility + setColumnVisibilityRaw ) const patientPropertyColumns = useMemo[]>( @@ -139,6 +131,8 @@ export const RecentPatientsTable = ({ onSortingChange={setSorting} onColumnFiltersChange={setFilters} enableMultiSort={true} + enableSorting={false} + enableColumnFilters={false} >
diff --git a/web/components/tables/RecentTasksTable.tsx b/web/components/tables/RecentTasksTable.tsx index cab0816..2a11043 100644 --- a/web/components/tables/RecentTasksTable.tsx +++ b/web/components/tables/RecentTasksTable.tsx @@ -1,7 +1,7 @@ import { useTasksTranslation } from '@/i18n/useTasksTranslation' -import type { ColumnDef, Row, TableState } from '@tanstack/react-table' +import type { ColumnDef, ColumnFiltersState, PaginationState, Row, SortingState, TableState, VisibilityState } from '@tanstack/react-table' import type { GetOverviewDataQuery, TaskPriority } from '@/api/gql/generated' -import { useCallback, useMemo } from 'react' +import { useCallback, useMemo, useState } from 'react' import clsx from 'clsx' import type { TableProps } from '@helpwave/hightide' import { Button, Checkbox, FillerCell, TableDisplay, TableProvider, Tooltip } from '@helpwave/hightide' @@ -11,8 +11,7 @@ import { PriorityUtils } from '@/utils/priority' import { PropertyEntity } from '@/api/gql/generated' import { usePropertyDefinitions } from '@/data' import { getPropertyColumnsForEntity } from '@/utils/propertyColumn' -import { useStorageSyncedTableState } from '@/hooks/useTableState' -import { usePropertyColumnVisibility } from '@/hooks/usePropertyColumnVisibility' +import { useColumnVisibilityWithPropertyDefaults } from '@/hooks/usePropertyColumnVisibility' type TaskViewModel = GetOverviewDataQuery['recentTasks'][0] @@ -36,22 +35,15 @@ export const RecentTasksTable = ({ const translation = useTasksTranslation() const { data: propertyDefinitionsData } = usePropertyDefinitions() - const { - pagination, - setPagination, - sorting, - setSorting, - filters, - setFilters, - columnVisibility, - setColumnVisibility, - } = useStorageSyncedTableState('recent-tasks') + const [pagination, setPagination] = useState({ pageSize: 10, pageIndex: 0 }) + const [sorting, setSorting] = useState([]) + const [filters, setFilters] = useState([]) + const [columnVisibility, setColumnVisibilityRaw] = useState({}) - usePropertyColumnVisibility( + const setColumnVisibility = useColumnVisibilityWithPropertyDefaults( propertyDefinitionsData, PropertyEntity.Task, - columnVisibility, - setColumnVisibility + setColumnVisibilityRaw ) const taskPropertyColumns = useMemo[]>( @@ -212,6 +204,8 @@ export const RecentTasksTable = ({ onColumnFiltersChange={setFilters} enableMultiSort={true} isUsingFillerRows={true} + enableSorting={false} + enableColumnFilters={false} >
diff --git a/web/components/tables/TaskList.tsx b/web/components/tables/TaskList.tsx index b52b318..23d9314 100644 --- a/web/components/tables/TaskList.tsx +++ b/web/components/tables/TaskList.tsx @@ -17,13 +17,12 @@ import { PatientDetailView } from '@/components/patients/PatientDetailView' import { useTasksTranslation } from '@/i18n/useTasksTranslation' import { useTasksContext } from '@/hooks/useTasksContext' import { UserInfoPopup } from '@/components/UserInfoPopup' -import type { ColumnDef, ColumnFiltersState, PaginationState, SortingState, TableState, VisibilityState } from '@tanstack/table-core' +import type { ColumnDef, ColumnFiltersState, ColumnOrderState, PaginationState, SortingState, TableState, VisibilityState } from '@tanstack/table-core' import type { Dispatch, SetStateAction } from 'react' import { DueDateUtils } from '@/utils/dueDate' import { PriorityUtils } from '@/utils/priority' import { getPropertyColumnsForEntity } from '@/utils/propertyColumn' -import { useStorageSyncedTableState } from '@/hooks/useTableState' -import { usePropertyColumnVisibility } from '@/hooks/usePropertyColumnVisibility' +import { useColumnVisibilityWithPropertyDefaults } from '@/hooks/usePropertyColumnVisibility' import { queryableFieldsToFilterListItems, queryableFieldsToSortingListItems } from '@/utils/queryableFilterList' export type TaskViewModel = { @@ -68,6 +67,8 @@ type TaskListTableState = { setFilters: Dispatch>, columnVisibility: VisibilityState, setColumnVisibility: Dispatch>, + columnOrder: ColumnOrderState, + setColumnOrder: Dispatch>, } type TaskListProps = { @@ -90,31 +91,34 @@ export const TaskList = forwardRef(({ tasks: initial const { data: propertyDefinitionsData } = usePropertyDefinitions() const { data: queryableFieldsData } = useQueryableFields('Task') - const internalState = useStorageSyncedTableState('task-list', { - defaultSorting: useMemo(() => [ - { id: 'done', desc: false }, - { id: 'dueDate', desc: false }, - ], []), - }) + const [internalPagination, setInternalPagination] = useState({ pageSize: 10, pageIndex: 0 }) + const [internalSorting, setInternalSorting] = useState(() => [ + { id: 'done', desc: false }, + { id: 'dueDate', desc: false }, + ]) + const [internalFilters, setInternalFilters] = useState([]) + const [internalColumnVisibility, setInternalColumnVisibility] = useState({}) + const [internalColumnOrder, setInternalColumnOrder] = useState([]) const lastTotalCountRef = useRef(undefined) if (totalCount != null) lastTotalCountRef.current = totalCount const stableTotalCount = totalCount ?? lastTotalCountRef.current - const pagination = controlledTableState?.pagination ?? internalState.pagination - const setPagination = controlledTableState?.setPagination ?? internalState.setPagination - const sorting = controlledTableState?.sorting ?? internalState.sorting - const setSorting = controlledTableState?.setSorting ?? internalState.setSorting - const filters = controlledTableState?.filters ?? internalState.filters - const setFilters = controlledTableState?.setFilters ?? internalState.setFilters - const columnVisibility = controlledTableState?.columnVisibility ?? internalState.columnVisibility - const setColumnVisibility = controlledTableState?.setColumnVisibility ?? internalState.setColumnVisibility - - usePropertyColumnVisibility( + const pagination = controlledTableState?.pagination ?? internalPagination + const setPagination = controlledTableState?.setPagination ?? setInternalPagination + const sorting = controlledTableState?.sorting ?? internalSorting + const setSorting = controlledTableState?.setSorting ?? setInternalSorting + const filters = controlledTableState?.filters ?? internalFilters + const setFilters = controlledTableState?.setFilters ?? setInternalFilters + const columnVisibility = controlledTableState?.columnVisibility ?? internalColumnVisibility + const setColumnVisibilityRaw = controlledTableState?.setColumnVisibility ?? setInternalColumnVisibility + const columnOrder = controlledTableState?.columnOrder ?? internalColumnOrder + const setColumnOrder = controlledTableState?.setColumnOrder ?? setInternalColumnOrder + + const setColumnVisibilityMerged = useColumnVisibilityWithPropertyDefaults( propertyDefinitionsData, PropertyEntity.Task, - columnVisibility, - setColumnVisibility + setColumnVisibilityRaw ) const queryClient = useQueryClient() @@ -568,9 +572,11 @@ export const TaskList = forwardRef(({ tasks: initial }} state={{ columnVisibility, + columnOrder, pagination, } as Partial as TableState} - onColumnVisibilityChange={setColumnVisibility} + onColumnVisibilityChange={setColumnVisibilityMerged} + onColumnOrderChange={setColumnOrder} onPaginationChange={setPagination} onSortingChange={setSorting} onColumnFiltersChange={setFilters} diff --git a/web/components/tables/UserSelectFilterPopUp.tsx b/web/components/tables/UserSelectFilterPopUp.tsx index 82bff82..c033491 100644 --- a/web/components/tables/UserSelectFilterPopUp.tsx +++ b/web/components/tables/UserSelectFilterPopUp.tsx @@ -36,10 +36,10 @@ export const UserSelectFilterPopUp = ({ value, onValueChange, onRemove, name }:
onValueChange({ ...value, parameter: { ...parameter, singleOptionSearch: newUserValue } })} - onDialogClose={(newUserValue) => onValueChange({ ...value, parameter: { ...parameter, singleOptionSearch: newUserValue } })} - onValueClear={() => onValueChange({ ...value, parameter: { ...parameter, singleOptionSearch: undefined } })} + value={parameter.uuidValue != null ? String(parameter.uuidValue) : ''} + onValueChanged={(newUserValue) => onValueChange({ ...value, parameter: { ...parameter, uuidValue: newUserValue } })} + onDialogClose={(newUserValue) => onValueChange({ ...value, parameter: { ...parameter, uuidValue: newUserValue } })} + onValueClear={() => onValueChange({ ...value, parameter: { ...parameter, uuidValue: undefined } })} />
diff --git a/web/components/tasks/TaskDataEditor.tsx b/web/components/tasks/TaskDataEditor.tsx index b058efd..6a6ec2b 100644 --- a/web/components/tasks/TaskDataEditor.tsx +++ b/web/components/tasks/TaskDataEditor.tsx @@ -7,7 +7,6 @@ import type { FormFieldDataHandling } from '@helpwave/hightide' import { Button, Checkbox, - DateTimeInput, FormProvider, FormField, Input, diff --git a/web/components/views/SaveViewActionsMenu.tsx b/web/components/views/SaveViewActionsMenu.tsx new file mode 100644 index 0000000..124c70a --- /dev/null +++ b/web/components/views/SaveViewActionsMenu.tsx @@ -0,0 +1,74 @@ +'use client' + +import { Button, Menu, MenuItem } from '@helpwave/hightide' +import { Rabbit } from 'lucide-react' +import { useTasksTranslation } from '@/i18n/useTasksTranslation' + +export type SaveViewActionsMenuProps = { + canOverwrite: boolean, + overwriteLoading?: boolean, + onOverwrite: () => void | Promise, + onOpenSaveAsNew: () => void, + onDiscard: () => void, +} + +export function SaveViewActionsMenu({ + canOverwrite, + overwriteLoading = false, + onOverwrite, + onOpenSaveAsNew, + onDiscard, +}: SaveViewActionsMenuProps) { + const translation = useTasksTranslation() + + return ( +
+ + {canOverwrite ? ( + ( + + )} + className="min-w-56 p-2" + options={{ + verticalAlignment: 'beforeStart', + }} + > + {({ close }) => ( + <> + { + void onOverwrite() + close() + }} + isDisabled={overwriteLoading} + className="rounded-md cursor-pointer" + > + {translation('saveViewOverwriteCurrent')} + + { + onOpenSaveAsNew() + close() + }} + className="rounded-md cursor-pointer" + > + {translation('saveViewAsNew')} + + + )} + + ) : ( + + )} +
+ ) +} diff --git a/web/components/views/SaveViewDialog.tsx b/web/components/views/SaveViewDialog.tsx index 857d4c7..381b3f2 100644 --- a/web/components/views/SaveViewDialog.tsx +++ b/web/components/views/SaveViewDialog.tsx @@ -23,6 +23,7 @@ type SaveViewDialogProps = { filterDefinition: string, sortDefinition: string, parameters: string, + presentation?: 'default' | 'fromSystemList', /** Optional: navigate or toast after save */ onCreated?: (id: string) => void, } @@ -34,6 +35,7 @@ export function SaveViewDialog({ filterDefinition, sortDefinition, parameters, + presentation = 'default', onCreated, }: SaveViewDialogProps) { const translation = useTasksTranslation() @@ -59,8 +61,8 @@ export function SaveViewDialog({
diff --git a/web/components/views/SavedViewEntityTypeChip.tsx b/web/components/views/SavedViewEntityTypeChip.tsx new file mode 100644 index 0000000..8b6354f --- /dev/null +++ b/web/components/views/SavedViewEntityTypeChip.tsx @@ -0,0 +1,31 @@ +import type { ChipProps } from '@helpwave/hightide' +import { Chip } from '@helpwave/hightide' +import { SavedViewEntityType } from '@/api/gql/generated' +import { useTasksTranslation } from '@/i18n/useTasksTranslation' +import clsx from 'clsx' + +export type SavedViewEntityTypeChipProps = Omit & { + entityType: SavedViewEntityType, +} + +export function SavedViewEntityTypeChip({ + entityType, + className, + size = 'sm', + ...props +}: SavedViewEntityTypeChipProps) { + const translation = useTasksTranslation() + const isPatient = entityType === SavedViewEntityType.Patient + + return ( + + {isPatient ? translation('viewsEntityPatient') : translation('viewsEntityTask')} + + ) +} diff --git a/web/globals.css b/web/globals.css index f12e4b4..edd35c0 100644 --- a/web/globals.css +++ b/web/globals.css @@ -1,6 +1,6 @@ @import 'tailwindcss'; -@import "@helpwave/hightide/style/uncompiled/globals.css"; +@import "./node_modules/@helpwave/hightide/dist/style/uncompiled/globals.css"; @import "./style/index.css"; @source "./node_modules/@helpwave/hightide"; diff --git a/web/hooks/usePropertyColumnVisibility.ts b/web/hooks/usePropertyColumnVisibility.ts index 09d80f5..27a3555 100644 --- a/web/hooks/usePropertyColumnVisibility.ts +++ b/web/hooks/usePropertyColumnVisibility.ts @@ -1,7 +1,8 @@ -import { useEffect } from 'react' +import { useCallback, useMemo } from 'react' import type { Dispatch, SetStateAction } from 'react' import type { VisibilityState } from '@tanstack/react-table' import type { PropertyEntity } from '@/api/gql/generated' +import { normalizedVisibilityForViewCompare } from '@/utils/viewDefinition' type PropertyDefinitionsData = { propertyDefinitions?: Array<{ @@ -11,30 +12,49 @@ type PropertyDefinitionsData = { }>, } | null | undefined -export function usePropertyColumnVisibility( +export function getPropertyColumnIds( + propertyDefinitionsData: PropertyDefinitionsData, + entity: PropertyEntity +): string[] { + if (!propertyDefinitionsData?.propertyDefinitions) return [] + const entityValue = entity as string + return propertyDefinitionsData.propertyDefinitions + .filter(def => def.isActive && def.allowedEntities.includes(entityValue)) + .map(prop => `property_${prop.id}`) +} + +export function useColumnVisibilityWithPropertyDefaults( propertyDefinitionsData: PropertyDefinitionsData, entity: PropertyEntity, - columnVisibility: VisibilityState, setColumnVisibility: Dispatch> -): void { - useEffect(() => { - if (!propertyDefinitionsData?.propertyDefinitions) return - - const entityValue = entity as string - const properties = propertyDefinitionsData.propertyDefinitions.filter( - def => def.isActive && def.allowedEntities.includes(entityValue) - ) - const propertyColumnIds = properties.map(prop => `property_${prop.id}`) - const hasPropertyColumnsInVisibility = propertyColumnIds.some( - id => id in columnVisibility - ) +): Dispatch> { + const propertyColumnIds = useMemo( + () => getPropertyColumnIds(propertyDefinitionsData, entity), + [propertyDefinitionsData, entity] + ) - if (!hasPropertyColumnsInVisibility && propertyColumnIds.length > 0) { - const initialVisibility: VisibilityState = { ...columnVisibility } - propertyColumnIds.forEach(id => { - initialVisibility[id] = false + return useCallback( + (updater: SetStateAction) => { + setColumnVisibility(prev => { + const next = typeof updater === 'function' + ? (updater as (p: VisibilityState) => VisibilityState)(prev) + : updater + if (propertyColumnIds.length === 0) { + return normalizedVisibilityForViewCompare(next) === normalizedVisibilityForViewCompare(prev) + ? prev + : next + } + const merged: VisibilityState = { ...next } + for (const id of propertyColumnIds) { + if (!(id in merged)) { + merged[id] = false + } + } + return normalizedVisibilityForViewCompare(merged) === normalizedVisibilityForViewCompare(prev) + ? prev + : merged }) - setColumnVisibility(initialVisibility) - } - }, [propertyDefinitionsData, entity, columnVisibility, setColumnVisibility]) + }, + [propertyColumnIds, setColumnVisibility] + ) } diff --git a/web/hooks/useTableState.ts b/web/hooks/useTableState.ts index 907561b..5046d44 100644 --- a/web/hooks/useTableState.ts +++ b/web/hooks/useTableState.ts @@ -1,13 +1,11 @@ import type { - ColumnFilter, ColumnFiltersState, + ColumnOrderState, PaginationState, SortingState, VisibilityState } from '@tanstack/react-table' -import type { Dispatch, SetStateAction } from 'react' -import type { DataType, FilterOperator, FilterParameter, FilterValue } from '@helpwave/hightide' -import { useStorage } from '@/hooks/useStorage' +import { useCallback, useMemo, useState, type Dispatch, type SetStateAction } from 'react' const defaultPagination: PaginationState = { pageSize: 10, @@ -17,12 +15,14 @@ const defaultPagination: PaginationState = { const defaultSorting: SortingState = [] const defaultFilters: ColumnFiltersState = [] const defaultColumnVisibility: VisibilityState = {} +const defaultColumnOrder: ColumnOrderState = [] export type UseTableStateOptions = { defaultSorting?: SortingState, defaultPagination?: PaginationState, defaultFilters?: ColumnFiltersState, defaultColumnVisibility?: VisibilityState, + defaultColumnOrder?: ColumnOrderState, } export type UseTableStateResult = { @@ -34,78 +34,56 @@ export type UseTableStateResult = { setFilters: Dispatch>, columnVisibility: VisibilityState, setColumnVisibility: Dispatch>, + columnOrder: ColumnOrderState, + setColumnOrder: Dispatch>, } -export function useStorageSyncedTableState( - storageKeyPrefix: string, - options: UseTableStateOptions = {} -): UseTableStateResult { +export function useTableState(options: UseTableStateOptions = {}): UseTableStateResult { const { defaultSorting: initialSorting = defaultSorting, defaultPagination: initialPagination = defaultPagination, defaultFilters: initialFilters = defaultFilters, defaultColumnVisibility: initialColumnVisibility = defaultColumnVisibility, + defaultColumnOrder: initialColumnOrder = defaultColumnOrder, } = options - const { value: pagination, setValue: setPagination } = useStorage({ - key: `${storageKeyPrefix}-column-pagination`, - defaultValue: initialPagination, - }) - const { value: sorting, setValue: setSorting } = useStorage({ - key: `${storageKeyPrefix}-column-sorting`, - defaultValue: initialSorting, - }) - const { value: filters, setValue: setFilters } = useStorage({ - key: `${storageKeyPrefix}-column-filters`, - defaultValue: initialFilters, - serialize: (value) => { - const mappedColumnFilter = value.map((filter) => { - const tableFilterValue = filter.value as FilterValue - const filterParameter = tableFilterValue.parameter - const parameter: Record = { - ...filterParameter, - compareDate: filterParameter.compareDate ? filterParameter.compareDate.toISOString() : undefined, - minDate: filterParameter.minDate ? filterParameter.minDate.toISOString() : undefined, - maxDate: filterParameter.maxDate ? filterParameter.maxDate.toISOString() : undefined, - } - return { - ...filter, - id: filter.id, - value: { - ...tableFilterValue, - parameter, - }, - } - }) - return JSON.stringify(mappedColumnFilter) - }, - deserialize: (value) => { - const mappedColumnFilter = JSON.parse(value) as Record[] - return mappedColumnFilter.map((filter): ColumnFilter => { - const value = filter['value'] as Record - const parameter: Record = value['parameter'] as Record - const filterParameter: FilterParameter = { - ...parameter, - compareDate: parameter['compareDate'] ? new Date(parameter['compareDate'] as string) : undefined, - minDate: parameter['minDate'] ? new Date(parameter['minDate'] as string) : undefined, - maxDate: parameter['maxDate'] ? new Date(parameter['maxDate'] as string) : undefined, - } - const mappedValue: FilterValue = { - operator: value['operator'] as FilterOperator, - dataType: value['dataType'] as DataType, - parameter: filterParameter, - } - return { - ...filter, - value: mappedValue, - } as ColumnFilter - }) - }, - }) - const { value: columnVisibility, setValue: setColumnVisibility } = useStorage({ - key: `${storageKeyPrefix}-column-visibility`, - defaultValue: initialColumnVisibility, - }) + const [pagination, setPagination] = useState(initialPagination) + const [sorting, setSorting] = useState(initialSorting) + const [filters, setFilters] = useState(initialFilters) + const [columnVisibility, setColumnVisibility] = useState(initialColumnVisibility) + const [columnOrder, setColumnOrder] = useState(initialColumnOrder) + + return { + pagination, + setPagination, + sorting, + setSorting, + filters, + setFilters, + columnVisibility, + setColumnVisibility, + columnOrder, + setColumnOrder, + } +} + +export function useRecentOverviewTableState( + options: Pick = {} +): UseTableStateResult { + const { + defaultPagination: initialPagination = defaultPagination, + defaultColumnVisibility: initialColumnVisibility = defaultColumnVisibility, + defaultColumnOrder: initialColumnOrder = defaultColumnOrder, + } = options + + const [pagination, setPagination] = useState(initialPagination) + const [columnVisibility, setColumnVisibility] = useState(initialColumnVisibility) + const [columnOrder, setColumnOrder] = useState(initialColumnOrder) + + const sorting = useMemo(() => [] as SortingState, []) + const filters = useMemo(() => [] as ColumnFiltersState, []) + const setSorting = useCallback((_u: SetStateAction) => undefined, []) + const setFilters = useCallback((_u: SetStateAction) => undefined, []) return { pagination, @@ -116,5 +94,7 @@ export function useStorageSyncedTableState( setFilters, columnVisibility, setColumnVisibility, + columnOrder, + setColumnOrder, } } diff --git a/web/i18n/translations.ts b/web/i18n/translations.ts index 99c12bf..c6b598b 100644 --- a/web/i18n/translations.ts +++ b/web/i18n/translations.ts @@ -63,6 +63,7 @@ export type TasksTranslationEntries = { 'descriptionPlaceholder': string, 'deselectAll': string, 'developmentAndPreviewInstance': string, + 'discardViewChanges': string, 'dischargePatient': string, 'dischargePatientConfirmation': string, 'dismiss': string, @@ -160,6 +161,7 @@ export type TasksTranslationEntries = { 'priorityNone': string, 'privacy': string, 'properties': string, + 'propertiesSettingsDescription': string, 'property': string, 'pThemes': (values: { count: number }) => string, 'rAdd': (values: { name: string }) => string, @@ -174,7 +176,10 @@ export type TasksTranslationEntries = { 'rShow': (values: { name: string }) => string, 'savedViews': string, 'saveView': string, + 'saveViewAsNew': string, 'saveViewDescription': string, + 'saveViewDescriptionFromSystemList': string, + 'saveViewOverwriteCurrent': string, 'search': string, 'searchLocations': string, 'searchUserOrTeam': string, @@ -225,10 +230,14 @@ export type TasksTranslationEntries = { 'userInformation': string, 'users': string, 'viewDerivedPatientsHint': string, + 'views': string, 'viewsEntityPatient': string, 'viewsEntityTask': string, 'viewSettings': string, 'viewSettingsDescription': string, + 'viewVisibility': string, + 'viewVisibilityLinkShared': string, + 'viewVisibilityPrivate': string, 'waitingForPatient': string, 'waitPatient': string, 'wards': string, @@ -262,7 +271,7 @@ export const tasksTranslation: Translation { return TranslationGen.resolvePlural(count, { @@ -480,9 +491,12 @@ export const tasksTranslation: Translation { return `${name} anzeigen` }, - 'savedViews': `Gespeicherte Ansichten`, - 'saveView': `Ansicht speichern`, - 'saveViewDescription': `Geben Sie dieser Ansicht einen Namen. Filter, Sortierung, Suche und Standort werden gespeichert.`, + 'savedViews': `Schnellzugriff`, + 'saveView': `Schnellzugriff speichern`, + 'saveViewAsNew': `Als neuen Schnellzugriff speichern`, + 'saveViewDescription': `Benennen Sie diesen Schnellzugriff. Filter, Sortierung, Suche und Standort werden gespeichert.`, + 'saveViewDescriptionFromSystemList': `Legt einen neuen Schnellzugriff aus diesem Layout an. Diese Seite selbst wird nicht überschrieben und bleibt für alle gleich — den Schnellzugriff öffnen Sie bei Bedarf in der Seitenleiste.`, + 'saveViewOverwriteCurrent': `Aktuellen Schnellzugriff überschreiben`, 'search': `Suchen`, 'searchLocations': `Standorte suchen...`, 'searchUserOrTeam': `Nach Benutzer (oder Team) suchen...`, @@ -559,11 +573,15 @@ export const tasksTranslation: Translation { return TranslationGen.resolvePlural(count, { @@ -813,9 +833,12 @@ export const tasksTranslation: Translation { return `Show ${name}` }, - 'savedViews': `Saved views`, - 'saveView': `Save view`, - 'saveViewDescription': `Name this view. Filters, sorting, search, and location scope are stored.`, + 'savedViews': `Quick access`, + 'saveView': `Save quick access`, + 'saveViewAsNew': `Save as new quick access`, + 'saveViewDescription': `Name this quick access. Filters, sorting, search, and location scope are stored.`, + 'saveViewDescriptionFromSystemList': `Creates a new quick access entry from this layout. This page is fixed by the app and is not saved in place — open it from the sidebar when you need it.`, + 'saveViewOverwriteCurrent': `Overwrite current quick access`, 'search': `Search`, 'searchLocations': `Search locations...`, 'searchUserOrTeam': `Search for user (or team)...`, @@ -892,11 +915,15 @@ export const tasksTranslation: Translation { return TranslationGen.resolvePlural(count, { @@ -1145,9 +1174,12 @@ export const tasksTranslation: Translation { return `Mostrar ${name}` }, - 'savedViews': `Saved views`, - 'saveView': `Save view`, - 'saveViewDescription': `Name this view. Filters, sorting, search, and location scope are stored.`, + 'savedViews': `Acceso rápido`, + 'saveView': `Guardar acceso rápido`, + 'saveViewAsNew': `Guardar como acceso rápido nuevo`, + 'saveViewDescription': `Pon nombre a este acceso rápido. Se guardan filtros, ordenación, búsqueda y ámbito de ubicación.`, + 'saveViewDescriptionFromSystemList': `Crea un acceso rápido nuevo a partir de este diseño. Esta página está fijada por la aplicación y no se guarda en su sitio: ábralo desde la barra lateral cuando lo necesite.`, + 'saveViewOverwriteCurrent': `Sobrescribir acceso rápido actual`, 'search': `Buscar`, 'searchLocations': `Buscar ubicaciones...`, 'searchUserOrTeam': `Buscar usuario (o equipo)...`, @@ -1224,11 +1256,15 @@ export const tasksTranslation: Translation { return TranslationGen.resolvePlural(count, { @@ -1477,9 +1515,12 @@ export const tasksTranslation: Translation { return `Afficher ${name}` }, - 'savedViews': `Saved views`, - 'saveView': `Save view`, - 'saveViewDescription': `Name this view. Filters, sorting, search, and location scope are stored.`, + 'savedViews': `Accès rapide`, + 'saveView': `Enregistrer l'accès rapide`, + 'saveViewAsNew': `Enregistrer comme nouvel accès rapide`, + 'saveViewDescription': `Nommez cet accès rapide. Filtres, tri, recherche et périmètre de lieu sont enregistrés.`, + 'saveViewDescriptionFromSystemList': `Crée un nouvel accès rapide à partir de cette disposition. Cette page est fixée par l'application et n'est pas enregistrée en place — ouvrez-le depuis la barre latérale si besoin.`, + 'saveViewOverwriteCurrent': `Remplacer l'accès rapide actuel`, 'search': `Rechercher`, 'searchLocations': `Rechercher des emplacements...`, 'searchUserOrTeam': `Rechercher un utilisateur (ou une équipe)...`, @@ -1556,11 +1597,15 @@ export const tasksTranslation: Translation { let _out: string = '' @@ -1812,9 +1859,12 @@ export const tasksTranslation: Translation { return `${name} tonen` }, - 'savedViews': `Saved views`, + 'savedViews': `Snelle toegang`, 'saveView': `Save view`, + 'saveViewAsNew': `Save as new view`, 'saveViewDescription': `Name this view. Filters, sorting, search, and location scope are stored.`, + 'saveViewDescriptionFromSystemList': `Creates a new quick access entry from this layout. This page is fixed by the app and is not saved in place — open it from the sidebar when you need it.`, + 'saveViewOverwriteCurrent': `Overwrite current view`, 'search': `Zoeken`, 'searchLocations': `Locaties zoeken...`, 'searchUserOrTeam': `Zoek gebruiker (of team)...`, @@ -1891,11 +1941,15 @@ export const tasksTranslation: Translation { return TranslationGen.resolvePlural(count, { @@ -2144,9 +2200,12 @@ export const tasksTranslation: Translation { return `Mostrar ${name}` }, - 'savedViews': `Saved views`, + 'savedViews': `Acesso rápido`, 'saveView': `Save view`, + 'saveViewAsNew': `Save as new view`, 'saveViewDescription': `Name this view. Filters, sorting, search, and location scope are stored.`, + 'saveViewDescriptionFromSystemList': `Creates a new quick access entry from this layout. This page is fixed by the app and is not saved in place — open it from the sidebar when you need it.`, + 'saveViewOverwriteCurrent': `Overwrite current view`, 'search': `Pesquisar`, 'searchLocations': `Pesquisar localizações...`, 'searchUserOrTeam': `Pesquisar usuário (ou equipe)...`, @@ -2223,11 +2282,15 @@ export const tasksTranslation: Translation {

{translation('system')}

-
+
+ + undefined} + onOpenSaveAsNew={() => setIsSaveViewOpen(true)} + onDiscard={handleDiscardTasksView} + /> )} tableState={{ @@ -140,6 +182,8 @@ const TasksPage: NextPage = () => { setFilters, columnVisibility, setColumnVisibility, + columnOrder, + setColumnOrder, }} /> diff --git a/web/pages/view/[uid].tsx b/web/pages/view/[uid].tsx index 8abd4b6..f16230a 100644 --- a/web/pages/view/[uid].tsx +++ b/web/pages/view/[uid].tsx @@ -2,42 +2,57 @@ import type { NextPage } from 'next' import { useRouter } from 'next/router' -import { useCallback, useMemo, useState } from 'react' +import { useCallback, useEffect, useMemo, useState } from 'react' import { useMutation } from '@apollo/client/react' import { Page } from '@/components/layout/Page' import titleWrapper from '@/utils/titleWrapper' import { useTasksTranslation } from '@/i18n/useTasksTranslation' import { ContentPanel } from '@/components/layout/ContentPanel' -import { Button, Chip, LoadingContainer, TabList, TabPanel, TabSwitcher } from '@helpwave/hightide' +import { Button, Chip, IconButton, LoadingContainer, TabList, TabPanel, TabSwitcher, Visibility } from '@helpwave/hightide' import { CenteredLoadingLogo } from '@/components/CenteredLoadingLogo' import { PatientList } from '@/components/tables/PatientList' import { TaskList, type TaskViewModel } from '@/components/tables/TaskList' import { PatientViewTasksPanel } from '@/components/views/PatientViewTasksPanel' import { TaskViewPatientsPanel } from '@/components/views/TaskViewPatientsPanel' -import { useSavedView } from '@/data' +import { usePropertyDefinitions, useSavedView, useTasksPaginated } from '@/data' +import { getPropertyColumnIds } from '@/hooks/usePropertyColumnVisibility' import { DuplicateSavedViewDocument, MySavedViewsDocument, + SavedViewDocument, + UpdateSavedViewDocument, type DuplicateSavedViewMutation, type DuplicateSavedViewMutationVariables, + type UpdateSavedViewMutation, + type UpdateSavedViewMutationVariables, + PropertyEntity, SavedViewEntityType } from '@/api/gql/generated' import { getParsedDocument } from '@/data/hooks/queryHelpers' import { deserializeColumnFiltersFromView, deserializeSortingFromView, - parseViewParameters + parseViewParameters, + serializeColumnFiltersForView, + serializeSortingForView, + stringifyViewParameters, + tableViewStateMatchesBaseline } from '@/utils/viewDefinition' +import { SaveViewDialog } from '@/components/views/SaveViewDialog' +import { SaveViewActionsMenu } from '@/components/views/SaveViewActionsMenu' +import { SavedViewEntityTypeChip } from '@/components/views/SavedViewEntityTypeChip' +import type { ColumnFiltersState } from '@tanstack/react-table' import { useTasksContext } from '@/hooks/useTasksContext' -import { useStorageSyncedTableState } from '@/hooks/useTableState' +import { useTableState } from '@/hooks/useTableState' import { columnFiltersToQueryFilterClauses, paginationStateToPaginationInput, sortingStateToQuerySortClauses } from '@/utils/tableStateToApi' -import { useTasksPaginated } from '@/data' +import { Share2 } from 'lucide-react' type SavedTaskViewTabProps = { viewId: string, filterDefinition: string, sortDefinition: string, parameters: ReturnType, + isOwner: boolean, } function SavedTaskViewTab({ @@ -45,7 +60,9 @@ function SavedTaskViewTab({ filterDefinition, sortDefinition, parameters, + isOwner, }: SavedTaskViewTabProps) { + const router = useRouter() const { selectedRootLocationIds, user } = useTasksContext() const defaultFilters = deserializeColumnFiltersFromView(filterDefinition) const defaultSorting = deserializeSortingFromView(sortDefinition) @@ -55,6 +72,40 @@ function SavedTaskViewTab({ { id: 'dueDate', desc: false }, ], []) + const viewSortBaseline = useMemo( + () => (defaultSorting.length > 0 ? defaultSorting : baselineSort), + [defaultSorting, baselineSort] + ) + + const baselineSearch = parameters.searchQuery ?? '' + const baselineColumnVisibility = useMemo( + () => parameters.columnVisibility ?? {}, + [parameters.columnVisibility] + ) + const baselineColumnOrder = useMemo( + () => parameters.columnOrder ?? [], + [parameters.columnOrder] + ) + + const { data: propertyDefinitionsData } = usePropertyDefinitions() + const propertyColumnIds = useMemo( + () => getPropertyColumnIds(propertyDefinitionsData, PropertyEntity.Task), + [propertyDefinitionsData] + ) + + const persistedViewContentKey = useMemo( + () => + `${filterDefinition}\0${sortDefinition}\0${stringifyViewParameters({ + rootLocationIds: parameters.rootLocationIds, + locationId: parameters.locationId, + searchQuery: parameters.searchQuery, + assigneeId: parameters.assigneeId, + columnVisibility: parameters.columnVisibility, + columnOrder: parameters.columnOrder, + })}`, + [filterDefinition, sortDefinition, parameters] + ) + const { pagination, setPagination, @@ -64,18 +115,123 @@ function SavedTaskViewTab({ setFilters, columnVisibility, setColumnVisibility, - } = useStorageSyncedTableState(`saved-view-${viewId}-task`, { + columnOrder, + setColumnOrder, + } = useTableState({ defaultFilters, - defaultSorting: defaultSorting.length > 0 ? defaultSorting : baselineSort, + defaultSorting: viewSortBaseline, + defaultColumnVisibility: baselineColumnVisibility, + defaultColumnOrder: baselineColumnOrder, }) - const apiFilters = useMemo(() => columnFiltersToQueryFilterClauses(filters), [filters]) - const apiSorting = useMemo(() => sortingStateToQuerySortClauses(sorting), [sorting]) - const apiPagination = useMemo(() => paginationStateToPaginationInput(pagination), [pagination]) + const [searchQuery, setSearchQuery] = useState(baselineSearch) + const [isSaveViewOpen, setIsSaveViewOpen] = useState(false) + + useEffect(() => { + const nextFilters = deserializeColumnFiltersFromView(filterDefinition) + const nextSort = deserializeSortingFromView(sortDefinition) + const nextSortBaseline = nextSort.length > 0 + ? nextSort + : [ + { id: 'done', desc: false }, + { id: 'dueDate', desc: false }, + ] + setFilters(nextFilters) + setSorting(nextSortBaseline) + setSearchQuery(parameters.searchQuery ?? '') + setColumnVisibility(parameters.columnVisibility ?? {}) + setColumnOrder(parameters.columnOrder ?? []) + setPagination({ pageSize: 10, pageIndex: 0 }) + }, [persistedViewContentKey]) + + const viewMatchesBaseline = useMemo( + () => tableViewStateMatchesBaseline({ + filters: filters as ColumnFiltersState, + baselineFilters: defaultFilters, + sorting, + baselineSorting: viewSortBaseline, + searchQuery, + baselineSearch, + columnVisibility, + baselineColumnVisibility, + columnOrder, + baselineColumnOrder, + propertyColumnIds, + }), + [ + filters, + defaultFilters, + sorting, + viewSortBaseline, + searchQuery, + baselineSearch, + columnVisibility, + baselineColumnVisibility, + columnOrder, + baselineColumnOrder, + propertyColumnIds, + ] + ) + const hasUnsavedViewChanges = !viewMatchesBaseline + + const [updateSavedView, { loading: overwriteLoading }] = useMutation< + UpdateSavedViewMutation, + UpdateSavedViewMutationVariables + >(getParsedDocument(UpdateSavedViewDocument), { + refetchQueries: [ + { query: getParsedDocument(SavedViewDocument), variables: { id: viewId } }, + { query: getParsedDocument(MySavedViewsDocument) }, + ], + }) + + const handleDiscardTaskView = useCallback(() => { + setFilters(defaultFilters) + setSorting(viewSortBaseline) + setSearchQuery(baselineSearch) + setColumnVisibility(baselineColumnVisibility) + setColumnOrder(baselineColumnOrder) + }, [ + baselineSearch, + baselineColumnOrder, + baselineColumnVisibility, + defaultFilters, + setFilters, + setSorting, + setSearchQuery, + setColumnVisibility, + setColumnOrder, + viewSortBaseline, + ]) const rootIds = parameters.rootLocationIds?.length ? parameters.rootLocationIds : selectedRootLocationIds const assigneeId = parameters.assigneeId ?? user?.id + const handleOverwriteTaskView = useCallback(async () => { + await updateSavedView({ + variables: { + id: viewId, + data: { + filterDefinition: serializeColumnFiltersForView(filters as ColumnFiltersState), + sortDefinition: serializeSortingForView(sorting), + parameters: stringifyViewParameters({ + rootLocationIds: rootIds ?? undefined, + assigneeId: assigneeId ?? undefined, + searchQuery: searchQuery || undefined, + columnVisibility, + columnOrder, + }), + }, + }, + }) + }, [updateSavedView, viewId, filters, sorting, rootIds, assigneeId, searchQuery, columnVisibility, columnOrder]) + + const apiFilters = useMemo(() => columnFiltersToQueryFilterClauses(filters), [filters]) + const apiSorting = useMemo(() => sortingStateToQuerySortClauses(sorting), [sorting]) + const apiPagination = useMemo(() => paginationStateToPaginationInput(pagination), [pagination]) + const searchInput = searchQuery + ? { searchText: searchQuery, includeProperties: true } + : undefined + const { data: tasksData, refetch, totalCount, loading: tasksLoading } = useTasksPaginated( rootIds && assigneeId ? { rootLocationIds: rootIds, assigneeId } @@ -84,6 +240,7 @@ function SavedTaskViewTab({ pagination: apiPagination, sorts: apiSorting.length > 0 ? apiSorting : undefined, filters: apiFilters.length > 0 ? apiFilters : undefined, + search: searchInput, } ) @@ -112,24 +269,56 @@ function SavedTaskViewTab({ })) }, [tasksData]) + const viewParametersForSave = useMemo(() => stringifyViewParameters({ + rootLocationIds: rootIds ?? undefined, + assigneeId: assigneeId ?? undefined, + searchQuery: searchQuery || undefined, + }), [rootIds, assigneeId, searchQuery]) + return ( - void refetch()} - showAssignee={false} - totalCount={totalCount} - loading={tasksLoading} - tableState={{ - pagination, - setPagination, - sorting, - setSorting, - filters, - setFilters, - columnVisibility, - setColumnVisibility, - }} - /> + <> + setIsSaveViewOpen(false)} + baseEntityType={SavedViewEntityType.Task} + filterDefinition={serializeColumnFiltersForView(filters as ColumnFiltersState)} + sortDefinition={serializeSortingForView(sorting)} + parameters={viewParametersForSave} + onCreated={(id) => router.push(`/view/${id}`)} + /> + void refetch()} + showAssignee={false} + totalCount={totalCount} + loading={tasksLoading} + searchQuery={searchQuery} + onSearchQueryChange={setSearchQuery} + saveViewSlot={isOwner ? ( + + setIsSaveViewOpen(true)} + onDiscard={handleDiscardTaskView} + /> + + ) : undefined} + tableState={{ + pagination, + setPagination, + sorting, + setSorting, + filters, + setFilters, + columnVisibility, + setColumnVisibility, + columnOrder, + setColumnOrder, + }} + /> + ) } @@ -206,19 +395,20 @@ const ViewPage: NextPage = () => {
{view.name} - - {view.baseEntityType === SavedViewEntityType.Patient - ? translation('viewsEntityPatient') - : translation('viewsEntityTask')} - + {!view.isOwner && ( {translation('readOnlyView')} )}
-
- +
+ + + {!view.isOwner && (
+ {multiUserSelect && onMultiUserIdsSelected && ( +
+ + +
+ )}
) diff --git a/web/components/tasks/TaskDataEditor.tsx b/web/components/tasks/TaskDataEditor.tsx index 6a6ec2b..3395653 100644 --- a/web/components/tasks/TaskDataEditor.tsx +++ b/web/components/tasks/TaskDataEditor.tsx @@ -2,7 +2,7 @@ import { useEffect, useState, useMemo } from 'react' import { useTasksTranslation } from '@/i18n/useTasksTranslation' import type { CreateTaskInput, UpdateTaskInput, TaskPriority } from '@/api/gql/generated' import { PatientState } from '@/api/gql/generated' -import { useCreateTask, useDeleteTask, usePatients, useTask, useUpdateTask, useAssignTask, useAssignTaskToTeam, useUnassignTask, useRefreshingEntityIds } from '@/data' +import { useCreateTask, useDeleteTask, usePatients, useTask, useUpdateTask, useUsers, useRefreshingEntityIds } from '@/data' import type { FormFieldDataHandling } from '@helpwave/hightide' import { Button, @@ -33,6 +33,7 @@ import { PriorityUtils } from '@/utils/priority' type TaskFormValues = CreateTaskInput & { done: boolean, + assigneeIds?: string[] | null, assigneeTeamId?: string | null, } @@ -73,9 +74,7 @@ export const TaskDataEditor = ({ const [createTask, { loading: isCreating }] = useCreateTask() const [updateTaskMutate] = useUpdateTask() - const [assignTask] = useAssignTask() - const [assignTaskToTeam] = useAssignTaskToTeam() - const [unassignTask] = useUnassignTask() + const { data: usersData } = useUsers() const updateTask = (vars: { id: string, data: UpdateTaskInput }) => { updateTaskMutate({ variables: vars, @@ -96,7 +95,7 @@ export const TaskDataEditor = ({ title: '', description: '', patientId: initialPatientId || '', - assigneeId: null, + assigneeIds: [], assigneeTeamId: null, dueDate: null, priority: null, @@ -108,9 +107,9 @@ export const TaskDataEditor = ({ variables: { data: { title: values.title, - patientId: values.patientId, + patientId: values.patientId || null, description: values.description, - assigneeId: values.assigneeId, + assigneeIds: values.assigneeIds ?? [], assigneeTeamId: values.assigneeTeamId, dueDate: values.dueDate ? localToUTCWithSameTime(values.dueDate)?.toISOString() : null, priority: (values.priority as TaskPriority | null) || undefined, @@ -134,23 +133,18 @@ export const TaskDataEditor = ({ } return null }, - patientId: (value) => { - if (!value || !value.trim()) { - return translation('patient') + ' is required' - } - return null - }, }, onValidUpdate: (_, updates) => { if (!isEditMode || !taskId || !taskData) return const data: UpdateTaskInput = { title: updates?.title, + patientId: updates?.patientId === undefined ? undefined : (updates.patientId || null), description: updates?.description, dueDate: updates?.dueDate ? localToUTCWithSameTime(updates.dueDate)?.toISOString() : undefined, priority: updates?.priority as TaskPriority | null | undefined, estimatedTime: updates?.estimatedTime, done: updates?.done, - assigneeId: updates?.assigneeId, + assigneeIds: updates?.assigneeIds, assigneeTeamId: updates?.assigneeTeamId, } const current = taskData @@ -160,9 +154,13 @@ export const TaskDataEditor = ({ const samePriority = (data.priority ?? current.priority ?? null) === (current.priority ?? null) const sameEstimatedTime = (data.estimatedTime ?? current.estimatedTime ?? null) === (current.estimatedTime ?? null) const sameDone = (data.done ?? current.done) === current.done - const sameAssigneeId = (data.assigneeId ?? current.assignee?.id ?? null) === (current.assignee?.id ?? null) + const currentAssigneeIds = [...(current.assignees?.map((assignee) => assignee.id) ?? [])].sort() + const nextAssigneeIds = [...(data.assigneeIds ?? currentAssigneeIds)].sort() + const sameAssigneeIds = currentAssigneeIds.length === nextAssigneeIds.length + && currentAssigneeIds.every((assigneeId, index) => assigneeId === nextAssigneeIds[index]) const sameAssigneeTeamId = (data.assigneeTeamId ?? current.assigneeTeam?.id ?? null) === (current.assigneeTeam?.id ?? null) - if (sameTitle && sameDescription && sameDueDate && samePriority && sameEstimatedTime && sameDone && sameAssigneeId && sameAssigneeTeamId) return + const samePatientId = (data.patientId ?? current.patient?.id ?? null) === (current.patient?.id ?? null) + if (sameTitle && sameDescription && sameDueDate && samePriority && sameEstimatedTime && sameDone && samePatientId && sameAssigneeIds && sameAssigneeTeamId) return updateTask({ id: taskId, data }) } }) @@ -177,7 +175,7 @@ export const TaskDataEditor = ({ title: task.title, description: task.description || '', patientId: task.patient?.id || '', - assigneeId: task.assignee?.id || null, + assigneeIds: task.assignees?.map((assignee) => assignee.id) ?? [], assigneeTeamId: task.assigneeTeam?.id || null, dueDate: task.dueDate ? new Date(task.dueDate) : null, priority: (task.priority as TaskPriority | null) || null, @@ -193,7 +191,12 @@ export const TaskDataEditor = ({ const dueDate = useFormObserverKey({ formStore: form.store, formKey: 'dueDate' })?.value ?? null const estimatedTime = useFormObserverKey({ formStore: form.store, formKey: 'estimatedTime' })?.value ?? null + const assigneeIds = useFormObserverKey({ formStore: form.store, formKey: 'assigneeIds' })?.value as string[] | null | undefined const assigneeTeamId = useFormObserverKey({ formStore: form.store, formKey: 'assigneeTeamId' })?.value as string | null | undefined + const selectedAssignees = useMemo( + () => usersData?.users?.filter((user) => (assigneeIds ?? []).includes(user.id)) ?? [], + [usersData, assigneeIds] + ) const expectedFinishDate = useMemo(() => { if (!dueDate || !estimatedTime) return null const finishDate = new Date(dueDate) @@ -233,7 +236,6 @@ export const TaskDataEditor = ({ id="task-done" value={done || false} onValueChange={(checked) => { - // TODO replace with form.update when it allows setting the update trigger form.store.setValue('done', checked, true) }} className={clsx('rounded-full scale-125', @@ -263,14 +265,13 @@ export const TaskDataEditor = ({ name="patientId" label={translation('patient')} - required - showRequiredIndicator={!isEditMode} > {({ dataProps, focusableElementProps, interactionStates }) => { return (!isEditMode) ? (