diff --git a/backend/README.md b/backend/README.md index a35edd61..4c416449 100644 --- a/backend/README.md +++ b/backend/README.md @@ -72,6 +72,8 @@ alembic downgrade -1 alembic revision --autogenerate -m "description" ``` +If `alembic heads` shows more than one head, run `alembic upgrade head` after pulling so merge revisions are applied. After upgrades that add tables (`task_assignees`, etc.), `alembic upgrade head` must succeed before the API can query those tables. + ## Docker The backend is containerized and available as: diff --git a/backend/api/decorators/__init__.py b/backend/api/decorators/__init__.py index ef535125..3090f26e 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 7545f8ad..00000000 --- a/backend/api/decorators/filter_sort.py +++ /dev/null @@ -1,727 +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.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 b251996d..00000000 --- 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 204a80fc..4350165d 100644 --- a/backend/api/inputs.py +++ b/backend/api/inputs.py @@ -110,10 +110,10 @@ class UpdatePatientInput: @strawberry.input class CreateTaskInput: title: str - patient_id: strawberry.ID + patient_id: strawberry.ID | None = None description: str | None = None due_date: datetime | None = None - assignee_id: strawberry.ID | None = None + assignee_ids: list[strawberry.ID] | None = None assignee_team_id: strawberry.ID | None = None previous_task_ids: list[strawberry.ID] | None = None properties: list[PropertyValueInput] | None = None @@ -124,10 +124,11 @@ class CreateTaskInput: @strawberry.input class UpdateTaskInput: title: str | None = None + patient_id: strawberry.ID | None = strawberry.UNSET description: str | None = None done: bool | None = None due_date: datetime | None = strawberry.UNSET - assignee_id: strawberry.ID | None = None + assignee_ids: list[strawberry.ID] | None = strawberry.UNSET assignee_team_id: strawberry.ID | None = strawberry.UNSET previous_task_ids: list[strawberry.ID] | None = None properties: list[PropertyValueInput] | None = None @@ -180,109 +181,38 @@ 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 +class PaginationInput: + page_index: int = 0 + page_size: int | 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.enum +class SavedViewEntityType(Enum): + TASK = "task" + PATIENT = "patient" -@strawberry.input -class PaginationInput: - page_index: int = 0 - page_size: int | None = None +@strawberry.enum +class SavedViewVisibility(Enum): + PRIVATE = "private" + LINK_SHARED = "link_shared" @strawberry.input -class QueryOptionsInput: - sorting: list[SortInput] | None = None - filtering: list[FilterInput] | None = None - pagination: PaginationInput | None = None +class CreateSavedViewInput: + name: str + base_entity_type: SavedViewEntityType + filter_definition: str + sort_definition: str + parameters: str + visibility: SavedViewVisibility = SavedViewVisibility.PRIVATE @strawberry.input -class FullTextSearchInput: - search_text: str - search_columns: list[str] | None = None - include_properties: bool = False - property_definition_ids: list[str] | None = None +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/query/__init__.py b/backend/api/query/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/backend/api/query/adapters/patient.py b/backend/api/query/adapters/patient.py new file mode 100644 index 00000000..a4e310ca --- /dev/null +++ b/backend/api/query/adapters/patient.py @@ -0,0 +1,448 @@ +from typing import Any + +from sqlalchemy import Select, and_, case, or_ +from sqlalchemy.orm import aliased + +from api.context import Info +from api.errors import raise_forbidden +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, + sort_directions_for, +) +from api.query.inputs import QueryFilterClauseInput, QuerySearchInput, QuerySortClauseInput +from api.query.property_sql import join_property_value +from api.query.patient_location_scope import ( + apply_patient_subtree_filter_from_cte, + build_location_descendants_cte, +) +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], + info: Info | None = None, +) -> 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": + if op in (QueryOperator.EQ, QueryOperator.IN) and val: + has_uuid = (val.uuid_value is not None and val.uuid_value != "") or ( + val.uuid_values is not None and len(val.uuid_values) > 0 + ) + if has_uuid: + if not info or not hasattr(info, "context"): + return query.where(False) + accessible = getattr(info.context, "_accessible_location_ids", None) + if accessible is None: + return query.where(False) + if op == QueryOperator.EQ: + if not val.uuid_value: + return query + lid = val.uuid_value + if lid not in accessible: + raise_forbidden() + filter_cte = build_location_descendants_cte( + [lid], cte_name="filter_loc_subtree" + ) + ctx["needs_distinct"] = True + return apply_patient_subtree_filter_from_cte(query, filter_cte) + if op == QueryOperator.IN: + ids: list[str] = [] + if val.uuid_values: + ids = [x for x in val.uuid_values if x in accessible] + elif val.uuid_value and val.uuid_value in accessible: + ids = [val.uuid_value] + if not ids: + return query.where(False) + filter_cte = build_location_descendants_cte( + ids, cte_name="filter_loc_subtree_m" + ) + ctx["needs_distinct"] = True + return apply_patient_subtree_filter_from_cte(query, filter_cte) + query, ln = _ensure_position_join(query, ctx) + expr = location_title_expr(ln) + if 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 + if 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 + + 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() + ) + + if not order_parts: + return query.order_by(models.Patient.id.asc()) + if ctx.get("needs_distinct"): + return query.order_by(models.Patient.id.asc(), *order_parts) + return query.order_by(*order_parts, models.Patient.id.asc()) + + +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, + sort_directions=sort_directions_for(True), + searchable=True, + ), + QueryableField( + key="firstname", + label="First name", + kind=QueryableFieldKind.SCALAR, + value_type=QueryableValueType.STRING, + allowed_operators=str_ops, + sortable=True, + sort_directions=sort_directions_for(True), + searchable=True, + ), + QueryableField( + key="lastname", + label="Last name", + kind=QueryableFieldKind.SCALAR, + value_type=QueryableValueType.STRING, + allowed_operators=str_ops, + sortable=True, + sort_directions=sort_directions_for(True), + searchable=True, + ), + QueryableField( + key="state", + label="State", + kind=QueryableFieldKind.CHOICE, + value_type=QueryableValueType.STRING, + allowed_operators=choice_ops, + sortable=True, + sort_directions=sort_directions_for(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, + sort_directions=sort_directions_for(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, + sort_directions=sort_directions_for(True), + searchable=False, + ), + QueryableField( + key="description", + label="Description", + kind=QueryableFieldKind.SCALAR, + value_type=QueryableValueType.STRING, + allowed_operators=str_ops, + sortable=True, + sort_directions=sort_directions_for(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, + sort_directions=sort_directions_for(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 00000000..94fa9620 --- /dev/null +++ b/backend/api/query/adapters/task.py @@ -0,0 +1,624 @@ +from typing import Any + +from sqlalchemy import Select, and_, case, exists, func, or_, select +from sqlalchemy.orm import aliased + +from api.context import Info +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, + 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, 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 _assignee_label_exists(op: QueryOperator, val: Any) -> Any: + u = aliased(models.User) + label_expr = user_display_label_expr(u) + label_condition = apply_ops_to_column(label_expr, op, val) + if label_condition is None: + return None + return exists( + select(1) + .select_from(models.task_assignees.join(u, models.task_assignees.c.user_id == u.id)) + .where( + models.task_assignees.c.task_id == models.Task.id, + label_condition, + ) + ) + + +def _assignee_label_sort_expr() -> Any: + u = aliased(models.User) + return ( + select(func.min(user_display_label_expr(u))) + .select_from(models.task_assignees.join(u, models.task_assignees.c.user_id == u.id)) + .where(models.task_assignees.c.task_id == models.Task.id) + .scalar_subquery() + ) + + +def _patient_label_expr() -> Any: + p = aliased(models.Patient) + return ( + select(patient_display_name_expr(p)) + .where(p.id == models.Task.patient_id) + .scalar_subquery() + ) + + +def _patient_label_exists(op: QueryOperator, val: Any) -> Any: + p = aliased(models.Patient) + expr = patient_display_name_expr(p) + condition = apply_ops_to_column(expr, op, val) + if condition is None: + return None + return exists( + select(1).where( + p.id == models.Task.patient_id, + condition, + ) + ) + + +def _patient_label_ilike_exists(pattern: str) -> Any: + p = aliased(models.Patient) + return exists( + select(1).where( + p.id == models.Task.patient_id, + patient_display_name_expr(p).ilike(pattern), + ) + ) + + +def _assignee_label_ilike_exists(pattern: str) -> Any: + u = aliased(models.User) + return exists( + select(1) + .select_from(models.task_assignees.join(u, models.task_assignees.c.user_id == u.id)) + .where( + models.task_assignees.c.task_id == models.Task.id, + user_display_label_expr(u).ilike(pattern), + ) + ) + + +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], + info: Info | None = None, +) -> 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": + if op in (QueryOperator.EQ, QueryOperator.IN) and val and ( + val.uuid_value or val.uuid_values + ): + if val.uuid_value: + query = query.where( + exists( + select(1).where( + models.task_assignees.c.task_id == models.Task.id, + models.task_assignees.c.user_id == val.uuid_value, + ) + ) + ) + elif val.uuid_values: + query = query.where( + exists( + select(1).where( + models.task_assignees.c.task_id == models.Task.id, + models.task_assignees.c.user_id.in_(val.uuid_values), + ) + ) + ) + elif op in ( + QueryOperator.CONTAINS, + QueryOperator.STARTS_WITH, + QueryOperator.ENDS_WITH, + ): + c = _assignee_label_exists(op, val) + if c is not None: + query = query.where(c) + elif op in (QueryOperator.IS_NULL, QueryOperator.IS_NOT_NULL): + has_assignees = exists( + select(1).where(models.task_assignees.c.task_id == models.Task.id) + ) + if op == QueryOperator.IS_NULL: + query = query.where(~has_assignees) + else: + query = query.where(has_assignees) + 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": + 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.IS_NULL, QueryOperator.IS_NOT_NULL): + c = apply_ops_to_column(models.Task.patient_id, op, None) + if c is not None: + query = query.where(c) + elif op in ( + QueryOperator.CONTAINS, + QueryOperator.STARTS_WITH, + QueryOperator.ENDS_WITH, + ): + c = _patient_label_exists(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": + label = _assignee_label_sort_expr() + 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_label_expr() + order_parts.append( + expr.desc().nulls_last() if desc_order else expr.asc().nulls_first() + ) + + if not order_parts: + return query.order_by(models.Task.id.asc()) + if ctx.get("needs_distinct"): + return query.order_by(models.Task.id.asc(), *order_parts) + return query.order_by(*order_parts, models.Task.id.asc()) + + +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()}%" + parts: list[Any] = [ + models.Task.title.ilike(pattern), + models.Task.description.ilike(pattern), + _patient_label_ilike_exists(pattern), + _assignee_label_ilike_exists(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, + sort_directions=sort_directions_for(True), + searchable=True, + ), + QueryableField( + key="description", + label="Description", + kind=QueryableFieldKind.SCALAR, + value_type=QueryableValueType.STRING, + allowed_operators=str_ops, + sortable=True, + sort_directions=sort_directions_for(True), + searchable=True, + ), + QueryableField( + key="done", + label="Done", + kind=QueryableFieldKind.SCALAR, + value_type=QueryableValueType.BOOLEAN, + allowed_operators=bool_ops, + sortable=True, + sort_directions=sort_directions_for(True), + searchable=False, + ), + QueryableField( + key="dueDate", + label="Due date", + kind=QueryableFieldKind.SCALAR, + value_type=QueryableValueType.DATETIME, + allowed_operators=dt_ops, + sortable=True, + sort_directions=sort_directions_for(True), + searchable=False, + ), + QueryableField( + key="priority", + label="Priority", + kind=QueryableFieldKind.CHOICE, + 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"], + 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, + sort_directions=sort_directions_for(True), + searchable=False, + ), + QueryableField( + key="creationDate", + label="Creation date", + kind=QueryableFieldKind.SCALAR, + value_type=QueryableValueType.DATETIME, + allowed_operators=dt_ops, + sortable=True, + sort_directions=sort_directions_for(True), + searchable=False, + ), + QueryableField( + key="updateDate", + label="Update date", + kind=QueryableFieldKind.SCALAR, + value_type=QueryableValueType.DATETIME, + allowed_operators=dt_ops, + sortable=True, + sort_directions=sort_directions_for(True), + searchable=False, + ), + QueryableField( + key="patient", + label="Patient", + kind=QueryableFieldKind.REFERENCE, + value_type=QueryableValueType.UUID, + allowed_operators=ref_ops, + sortable=True, + sort_directions=sort_directions_for(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, + sort_directions=sort_directions_for(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, + sort_directions=sort_directions_for(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 00000000..2fbc51d8 --- /dev/null +++ b/backend/api/query/adapters/user.py @@ -0,0 +1,149 @@ +from typing import Any + +from sqlalchemy import Select, or_ + +from api.context import Info +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, 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 + + +def apply_user_filter_clause( + query: Select[Any], + clause: QueryFilterClauseInput, + ctx: dict[str, Any], + property_field_types: dict[str, str], + info: Info | None = None, +) -> 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, + sort_directions=sort_directions_for(True), + searchable=True, + ), + QueryableField( + key="email", + label="Email", + kind=QueryableFieldKind.SCALAR, + value_type=QueryableValueType.STRING, + allowed_operators=str_ops, + sortable=True, + sort_directions=sort_directions_for(True), + searchable=True, + ), + QueryableField( + key="name", + label="Name", + kind=QueryableFieldKind.SCALAR, + value_type=QueryableValueType.STRING, + allowed_operators=str_ops, + sortable=True, + sort_directions=sort_directions_for(True), + searchable=True, + ), + ] diff --git a/backend/api/query/context.py b/backend/api/query/context.py new file mode 100644 index 00000000..e5b1bc6c --- /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 00000000..1856fe06 --- /dev/null +++ b/backend/api/query/engine.py @@ -0,0 +1,84 @@ +from typing import Any + +import strawberry +from sqlalchemy import Select + +from api.context import Info +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, + info: Info | None = None, +) -> 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, info=info + ) + + 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(handler["root_model"].id) + + 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 00000000..cb099161 --- /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 00000000..fe3f88f4 --- /dev/null +++ b/backend/api/query/execute.py @@ -0,0 +1,91 @@ +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, + info=info, + ) + + 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, + info: Info | None = None, +) -> int: + stmt = await apply_unified_query( + stmt, + entity=entity, + db=db, + filters=filters, + sorts=sorts, + search=search, + pagination=None, + for_count=True, + info=info, + ) + 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 00000000..e6a63d18 --- /dev/null +++ b/backend/api/query/field_ops.py @@ -0,0 +1,211 @@ +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.bool_value is not None: + return column == value.bool_value + if value.date_value is not None: + return column == value.date_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.date_value is not None: + return pred(column, value.date_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.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) + 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 + + +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_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 + 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 00000000..3464b19d --- /dev/null +++ b/backend/api/query/graphql_types.py @@ -0,0 +1,46 @@ +import strawberry + +from api.inputs import SortDirection +from api.query.enums import ( + QueryOperator, + QueryableFieldKind, + QueryableValueType, + ReferenceFilterMode, +) + + +def sort_directions_for(sortable: bool) -> list[SortDirection]: + return [SortDirection.ASC, SortDirection.DESC] if sortable else [] + + +@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 + 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/inputs.py b/backend/api/query/inputs.py new file mode 100644 index 00000000..2180ab66 --- /dev/null +++ b/backend/api/query/inputs.py @@ -0,0 +1,40 @@ +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 + bool_value: bool | None = None + date_value: datetime | None = None + date_min: date | None = None + date_max: date | 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 00000000..14375f33 --- /dev/null +++ b/backend/api/query/metadata_service.py @@ -0,0 +1,279 @@ +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, + sort_directions_for, +) +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, + sort_directions=sort_directions_for(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, + sort_directions=sort_directions_for(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, + sort_directions=sort_directions_for(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, + sort_directions=sort_directions_for(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, + sort_directions=sort_directions_for(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, + sort_directions=sort_directions_for(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, + sort_directions=sort_directions_for(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, + sort_directions=sort_directions_for(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, + sort_directions=sort_directions_for(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/patient_location_scope.py b/backend/api/query/patient_location_scope.py new file mode 100644 index 00000000..d3bcc04a --- /dev/null +++ b/backend/api/query/patient_location_scope.py @@ -0,0 +1,71 @@ +from typing import Any + +from sqlalchemy import Select, select +from sqlalchemy.orm import aliased + +from database import models + + +def build_location_descendants_cte( + seed_ids: list[str], + *, + cte_name: str = "location_descendants", +) -> Any: + if not seed_ids: + raise ValueError("seed_ids must not be empty") + if len(seed_ids) == 1: + anchor = select(models.LocationNode.id).where( + models.LocationNode.id == seed_ids[0] + ) + else: + anchor = select(models.LocationNode.id).where( + models.LocationNode.id.in_(seed_ids) + ) + filter_cte = anchor.cte(name=cte_name, recursive=True) + children = select(models.LocationNode.id).join( + filter_cte, + models.LocationNode.parent_id == filter_cte.c.id, + ) + return filter_cte.union_all(children) + + +def apply_patient_subtree_filter_from_cte( + query: Select[Any], + filter_cte: Any, +) -> Select[Any]: + patient_locations_filter = aliased(models.patient_locations) + patient_teams_filter = aliased(models.patient_teams) + + return ( + query.outerjoin( + patient_locations_filter, + models.Patient.id == patient_locations_filter.c.patient_id, + ) + .outerjoin( + patient_teams_filter, + models.Patient.id == patient_teams_filter.c.patient_id, + ) + .where( + (models.Patient.clinic_id.in_(select(filter_cte.c.id))) + | ( + models.Patient.position_id.isnot(None) + & models.Patient.position_id.in_(select(filter_cte.c.id)) + ) + | ( + models.Patient.assigned_location_id.isnot(None) + & models.Patient.assigned_location_id.in_( + select(filter_cte.c.id) + ) + ) + | ( + patient_locations_filter.c.location_id.in_( + select(filter_cte.c.id) + ) + ) + | ( + patient_teams_filter.c.location_id.in_( + select(filter_cte.c.id) + ) + ) + ) + ) diff --git a/backend/api/query/property_sql.py b/backend/api/query/property_sql.py new file mode 100644 index 00000000..0be5e223 --- /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 00000000..0108bb94 --- /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 00000000..9c0bf5f4 --- /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 b053198d..45d127cc 100644 --- a/backend/api/resolvers/__init__.py +++ b/backend/api/resolvers/__init__.py @@ -4,6 +4,8 @@ from .location import LocationMutation, LocationQuery, LocationSubscription from .patient import PatientMutation, PatientQuery, PatientSubscription from .property import PropertyDefinitionMutation, PropertyDefinitionQuery +from .query_metadata import QueryMetadataQuery +from .saved_view import SavedViewMutation, SavedViewQuery from .task import TaskMutation, TaskQuery, TaskSubscription from .user import UserMutation, UserQuery @@ -16,6 +18,8 @@ class Query( PropertyDefinitionQuery, UserQuery, AuditQuery, + QueryMetadataQuery, + SavedViewQuery, ): pass @@ -27,6 +31,7 @@ class Mutation( PropertyDefinitionMutation, LocationMutation, UserMutation, + SavedViewMutation, ): pass diff --git a/backend/api/resolvers/patient.py b/backend/api/resolvers/patient.py index 946e77b7..13b8cb92 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 @@ -28,6 +20,10 @@ from api.services.property import PropertyService from api.types.patient import PatientType from api.errors import raise_forbidden +from api.query.patient_location_scope import ( + apply_patient_subtree_filter_from_cte, + build_location_descendants_cte, +) from database import models from sqlalchemy import func, select from sqlalchemy.orm import aliased, selectinload @@ -74,16 +70,10 @@ async def _build_patients_base_query( if location_node_id: if location_node_id not in accessible_location_ids: raise_forbidden() - filter_cte = ( - select(models.LocationNode.id) - .where(models.LocationNode.id == location_node_id) - .cte(name="location_descendants", recursive=True) - ) - children = select(models.LocationNode.id).join( - filter_cte, - models.LocationNode.parent_id == filter_cte.c.id, + filter_cte = build_location_descendants_cte( + [str(location_node_id)], + cte_name="location_descendants", ) - filter_cte = filter_cte.union_all(children) elif root_location_ids: valid_root_location_ids = [ lid for lid in root_location_ids if lid in accessible_location_ids @@ -91,56 +81,13 @@ async def _build_patients_base_query( if not valid_root_location_ids: return query.where(False), [] root_location_ids = valid_root_location_ids - filter_cte = ( - select(models.LocationNode.id) - .where(models.LocationNode.id.in_(root_location_ids)) - .cte(name="root_location_descendants", recursive=True) - ) - root_children = select(models.LocationNode.id).join( - filter_cte, models.LocationNode.parent_id == filter_cte.c.id + filter_cte = build_location_descendants_cte( + [str(lid) for lid in root_location_ids], + cte_name="root_location_descendants", ) - filter_cte = filter_cte.union_all(root_children) if filter_cte is not None: - patient_locations_filter = aliased(models.patient_locations) - patient_teams_filter = aliased(models.patient_teams) - - query = ( - query.outerjoin( - patient_locations_filter, - models.Patient.id == patient_locations_filter.c.patient_id, - ) - .outerjoin( - patient_teams_filter, - models.Patient.id == patient_teams_filter.c.patient_id, - ) - .where( - (models.Patient.clinic_id.in_(select(filter_cte.c.id))) - | ( - models.Patient.position_id.isnot(None) - & models.Patient.position_id.in_( - select(filter_cte.c.id) - ) - ) - | ( - models.Patient.assigned_location_id.isnot(None) - & models.Patient.assigned_location_id.in_( - select(filter_cte.c.id) - ) - ) - | ( - patient_locations_filter.c.location_id.in_( - select(filter_cte.c.id) - ) - ) - | ( - patient_teams_filter.c.location_id.in_( - select(filter_cte.c.id) - ) - ) - ) - .distinct() - ) + query = apply_patient_subtree_filter_from_cte(query, filter_cte).distinct() return query, accessible_location_ids @@ -170,18 +117,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 +141,34 @@ 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, + info=info, ) - 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 +252,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 +326,15 @@ 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, + info=info, ) - 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 00000000..0f825cf3 --- /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/saved_view.py b/backend/api/resolvers/saved_view.py new file mode 100644 index 00000000..31d03492 --- /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/resolvers/task.py b/backend/api/resolvers/task.py index cc41c29d..fe8e8717 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,10 +20,39 @@ from api.types.task import TaskType from database import models from graphql import GraphQLError -from sqlalchemy import desc, func, select +from sqlalchemy import and_, exists, or_, select from sqlalchemy.orm import aliased, selectinload +def _assignee_match_clause(user_id: strawberry.ID | None): + if not user_id: + return None + return exists( + select(1).where( + models.task_assignees.c.task_id == models.Task.id, + models.task_assignees.c.user_id == user_id, + ) + ) + + +def _patient_visibility_clause(location_cte, patient_locations, patient_teams): + return ( + (models.Patient.clinic_id.in_(select(location_cte.c.id))) + | ( + models.Patient.position_id.isnot(None) + & models.Patient.position_id.in_(select(location_cte.c.id)) + ) + | ( + models.Patient.assigned_location_id.isnot(None) + & models.Patient.assigned_location_id.in_( + select(location_cte.c.id), + ) + ) + | (patient_locations.c.location_id.in_(select(location_cte.c.id))) + | (patient_teams.c.location_id.in_(select(location_cte.c.id))) + ) + + @strawberry.type class TaskQuery: @strawberry.field @@ -46,22 +64,22 @@ async def task(self, info: Info, id: strawberry.ID) -> TaskType | None: selectinload(models.Task.patient).selectinload( models.Patient.assigned_locations, ), + selectinload(models.Task.assignees), ), ) task = result.scalars().first() - if task and task.patient: + if task: auth_service = AuthorizationService(info.context.db) - if not await auth_service.can_access_patient( + if not await auth_service.can_access_task( info.context.user, - task.patient, + task, info.context, ): raise_forbidden() return task @strawberry.field - @filtered_and_sorted_query() - @full_text_search_query() + @unified_list_query(TASK) async def tasks( self, info: Info, @@ -69,10 +87,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) @@ -90,12 +108,20 @@ async def tasks( selectinload(models.Task.patient).selectinload( models.Patient.assigned_locations, ), + selectinload(models.Task.assignees), ) .where(models.Task.patient_id == patient_id) ) if assignee_id: - query = query.where(models.Task.assignee_id == assignee_id) + query = query.where( + exists( + select(1).where( + models.task_assignees.c.task_id == models.Task.id, + models.task_assignees.c.user_id == assignee_id, + ) + ) + ) if assignee_team_id: query = query.where( models.Task.assignee_team_id == assignee_team_id, @@ -164,14 +190,25 @@ async def tasks( ) team_location_cte = team_location_cte.union_all(team_children) + viewer_assignee_clause = _assignee_match_clause( + info.context.user.id if info.context.user else None + ) + no_patient_scope_clause = models.Task.assignee_team_id.in_(select(root_cte.c.id)) + if viewer_assignee_clause is not None: + no_patient_scope_clause = or_( + viewer_assignee_clause, + no_patient_scope_clause, + ) + query = ( select(models.Task) .options( selectinload(models.Task.patient).selectinload( models.Patient.assigned_locations, ), + selectinload(models.Task.assignees), ) - .join(models.Patient, models.Task.patient_id == models.Patient.id) + .outerjoin(models.Patient, models.Task.patient_id == models.Patient.id) .outerjoin( patient_locations, models.Patient.id == patient_locations.c.patient_id, @@ -181,30 +218,32 @@ async def tasks( models.Patient.id == patient_teams.c.patient_id, ) .where( - (models.Patient.clinic_id.in_(select(root_cte.c.id))) - | ( - models.Patient.position_id.isnot(None) - & models.Patient.position_id.in_(select(root_cte.c.id)) - ) - | ( - models.Patient.assigned_location_id.isnot(None) - & models.Patient.assigned_location_id.in_( - select(root_cte.c.id), + ( + and_( + models.Task.patient_id.isnot(None), + _patient_visibility_clause(root_cte, patient_locations, patient_teams), + models.Patient.state.notin_( + [PatientState.DISCHARGED.value, PatientState.DEAD.value] + ), ) ) - | (patient_locations.c.location_id.in_(select(root_cte.c.id))) - | (patient_teams.c.location_id.in_(select(root_cte.c.id))), - ) - .where( - models.Patient.state.notin_( - [PatientState.DISCHARGED.value, PatientState.DEAD.value] + | and_( + models.Task.patient_id.is_(None), + no_patient_scope_clause, ) ) .distinct() ) if assignee_id: - query = query.where(models.Task.assignee_id == assignee_id) + query = query.where( + exists( + select(1).where( + models.task_assignees.c.task_id == models.Task.id, + models.task_assignees.c.user_id == assignee_id, + ) + ) + ) if assignee_team_id: query = query.where( models.Task.assignee_team_id.in_( @@ -222,9 +261,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) @@ -241,7 +280,14 @@ async def tasksTotal( ) if assignee_id: - query = query.where(models.Task.assignee_id == assignee_id) + query = query.where( + exists( + select(1).where( + models.task_assignees.c.task_id == models.Task.id, + models.task_assignees.c.user_id == assignee_id, + ) + ) + ) if assignee_team_id: query = query.where( models.Task.assignee_team_id == assignee_team_id, @@ -308,9 +354,19 @@ async def tasksTotal( ) team_location_cte = team_location_cte.union_all(team_children) + viewer_assignee_clause = _assignee_match_clause( + info.context.user.id if info.context.user else None + ) + no_patient_scope_clause = models.Task.assignee_team_id.in_(select(root_cte.c.id)) + if viewer_assignee_clause is not None: + no_patient_scope_clause = or_( + viewer_assignee_clause, + no_patient_scope_clause, + ) + query = ( select(models.Task) - .join( + .outerjoin( models.Patient, models.Task.patient_id == models.Patient.id, ) @@ -323,34 +379,36 @@ async def tasksTotal( models.Patient.id == patient_teams.c.patient_id, ) .where( - (models.Patient.clinic_id.in_(select(root_cte.c.id))) - | ( - models.Patient.position_id.isnot(None) - & models.Patient.position_id.in_(select(root_cte.c.id)) - ) - | ( - models.Patient.assigned_location_id.isnot(None) - & models.Patient.assigned_location_id.in_( - select(root_cte.c.id), + ( + and_( + models.Task.patient_id.isnot(None), + _patient_visibility_clause( + root_cte, + patient_locations, + patient_teams, + ), + models.Patient.state.notin_( + [PatientState.DISCHARGED.value, PatientState.DEAD.value] + ), ) ) - | ( - patient_locations.c.location_id.in_( - select(root_cte.c.id), - ) - ) - | (patient_teams.c.location_id.in_(select(root_cte.c.id))), - ) - .where( - models.Patient.state.notin_( - [PatientState.DISCHARGED.value, PatientState.DEAD.value] + | and_( + models.Task.patient_id.is_(None), + no_patient_scope_clause, ) ) .distinct() ) if assignee_id: - query = query.where(models.Task.assignee_id == assignee_id) + query = query.where( + exists( + select(1).where( + models.task_assignees.c.task_id == models.Task.id, + models.task_assignees.c.user_id == assignee_id, + ) + ) + ) if assignee_team_id: query = query.where( models.Task.assignee_team_id.in_( @@ -358,45 +416,34 @@ 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, + info=info, ) - 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 = ( @@ -446,14 +493,25 @@ async def recent_tasks( else: location_cte = cte + viewer_assignee_clause = _assignee_match_clause( + info.context.user.id if info.context.user else None + ) + no_patient_scope_clause = models.Task.assignee_team_id.in_(select(location_cte.c.id)) + if viewer_assignee_clause is not None: + no_patient_scope_clause = or_( + viewer_assignee_clause, + no_patient_scope_clause, + ) + query = ( select(models.Task) .options( selectinload(models.Task.patient).selectinload( models.Patient.assigned_locations, ), + selectinload(models.Task.assignees), ) - .join(models.Patient, models.Task.patient_id == models.Patient.id) + .outerjoin(models.Patient, models.Task.patient_id == models.Patient.id) .outerjoin( patient_locations, models.Patient.id == patient_locations.c.patient_id, @@ -463,36 +521,27 @@ async def recent_tasks( models.Patient.id == patient_teams.c.patient_id, ) .where( - (models.Patient.clinic_id.in_(select(location_cte.c.id))) - | ( - models.Patient.position_id.isnot(None) - & models.Patient.position_id.in_(select(location_cte.c.id)) - ) - | ( - models.Patient.assigned_location_id.isnot(None) - & models.Patient.assigned_location_id.in_( - select(location_cte.c.id), - ) - ) - | ( - patient_locations.c.location_id.in_( - select(location_cte.c.id), + ( + and_( + models.Task.patient_id.isnot(None), + _patient_visibility_clause( + location_cte, + patient_locations, + patient_teams, + ), + models.Patient.state.notin_( + [PatientState.DISCHARGED.value, PatientState.DEAD.value] + ), ) ) - | (patient_teams.c.location_id.in_(select(location_cte.c.id))), - ) - .where( - models.Patient.state.notin_( - [PatientState.DISCHARGED.value, PatientState.DEAD.value] + | and_( + models.Task.patient_id.is_(None), + no_patient_scope_clause, ) ) .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 +549,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 = ( @@ -552,9 +601,19 @@ async def recentTasksTotal( else: location_cte = cte + viewer_assignee_clause = _assignee_match_clause( + info.context.user.id if info.context.user else None + ) + no_patient_scope_clause = models.Task.assignee_team_id.in_(select(location_cte.c.id)) + if viewer_assignee_clause is not None: + no_patient_scope_clause = or_( + viewer_assignee_clause, + no_patient_scope_clause, + ) + query = ( select(models.Task) - .join(models.Patient, models.Task.patient_id == models.Patient.id) + .outerjoin(models.Patient, models.Task.patient_id == models.Patient.id) .outerjoin( patient_locations, models.Patient.id == patient_locations.c.patient_id, @@ -564,59 +623,36 @@ async def recentTasksTotal( models.Patient.id == patient_teams.c.patient_id, ) .where( - (models.Patient.clinic_id.in_(select(location_cte.c.id))) - | ( - models.Patient.position_id.isnot(None) - & models.Patient.position_id.in_(select(location_cte.c.id)) - ) - | ( - models.Patient.assigned_location_id.isnot(None) - & models.Patient.assigned_location_id.in_( - select(location_cte.c.id), - ) - ) - | ( - patient_locations.c.location_id.in_( - select(location_cte.c.id), + ( + and_( + models.Task.patient_id.isnot(None), + _patient_visibility_clause( + location_cte, + patient_locations, + patient_teams, + ), + models.Patient.state.notin_( + [PatientState.DISCHARGED.value, PatientState.DEAD.value] + ), ) ) - | (patient_teams.c.location_id.in_(select(location_cte.c.id))), - ) - .where( - models.Patient.state.notin_( - [PatientState.DISCHARGED.value, PatientState.DEAD.value] + | and_( + models.Task.patient_id.is_(None), + no_patient_scope_clause, ) ) .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, + info=info, ) - 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 @@ -625,31 +661,62 @@ class TaskMutation(BaseMutationResolver[models.Task]): def _get_property_service(db) -> PropertyService: return PropertyService(db) + @staticmethod + async def _users_by_ids(info: Info, user_ids: list[strawberry.ID] | None) -> list[models.User]: + if not user_ids: + return [] + result = await info.context.db.execute( + select(models.User).where(models.User.id.in_(user_ids)) + ) + users = result.scalars().all() + if len(users) != len(set(str(user_id) for user_id in user_ids)): + raise GraphQLError( + "One or more assignee users were not found.", + extensions={"code": "BAD_REQUEST"}, + ) + return users + + @staticmethod + def _validate_task_scope( + patient_id: strawberry.ID | None, + assignee_count: int, + assignee_team_id: strawberry.ID | None, + ) -> None: + if assignee_count > 0 and assignee_team_id is not None: + raise GraphQLError( + "Cannot assign both users and a team. Please assign either users or a team.", + extensions={"code": "BAD_REQUEST"}, + ) + if patient_id is None and assignee_count == 0 and assignee_team_id is None: + raise GraphQLError( + "Task must have a patient, assignees, or an assignee team.", + extensions={"code": "BAD_REQUEST"}, + ) + @strawberry.mutation @audit_log("create_task") async def create_task(self, info: Info, data: CreateTaskInput) -> TaskType: auth_service = AuthorizationService(info.context.db) - if not await auth_service.can_access_patient_id( + if data.patient_id and not await auth_service.can_access_patient_id( info.context.user, data.patient_id, info.context, ): raise_forbidden() - if data.assignee_id and data.assignee_team_id: - raise GraphQLError( - "Cannot assign both a user and a team. Please assign either a user or a team.", - extensions={"code": "BAD_REQUEST"}, - ) + assignees = await TaskMutation._users_by_ids(info, data.assignee_ids) + TaskMutation._validate_task_scope( + data.patient_id, + len(assignees), + data.assignee_team_id, + ) new_task = models.Task( title=data.title, description=data.description, patient_id=data.patient_id, - assignee_id=data.assignee_id, - assignee_team_id=( - data.assignee_team_id if not data.assignee_id else None - ), + assignees=assignees, + assignee_team_id=(data.assignee_team_id if len(assignees) == 0 else None), due_date=normalize_datetime_to_utc(data.due_date), priority=data.priority.value if data.priority else None, estimated_time=data.estimated_time, @@ -702,20 +769,20 @@ async def update_task( selectinload(models.Task.patient).selectinload( models.Patient.assigned_locations, ), + selectinload(models.Task.assignees), ), ) task = result.scalars().first() if not task: raise Exception("Task not found") - if task.patient: - auth_service = AuthorizationService(db) - if not await auth_service.can_access_patient( - info.context.user, - task.patient, - info.context, - ): - raise_forbidden() + auth_service = AuthorizationService(db) + if not await auth_service.can_access_task( + info.context.user, + task, + info.context, + ): + raise_forbidden() if data.checksum: validate_checksum(task, data.checksum, "Task") @@ -724,6 +791,14 @@ async def update_task( task.title = data.title if data.description is not None: task.description = data.description + if data.patient_id is not strawberry.UNSET: + if data.patient_id and not await auth_service.can_access_patient_id( + info.context.user, + data.patient_id, + info.context, + ): + raise_forbidden() + task.patient_id = data.patient_id if data.done is not None: task.done = data.done @@ -740,22 +815,27 @@ async def update_task( if data.estimated_time is not strawberry.UNSET: task.estimated_time = data.estimated_time - if ( - data.assignee_id is not None - and data.assignee_team_id is not strawberry.UNSET - and data.assignee_team_id is not None - ): - raise GraphQLError( - "Cannot assign both a user and a team. Please assign either a user or a team.", - extensions={"code": "BAD_REQUEST"}, - ) + next_assignees = task.assignees + if data.assignee_ids is not strawberry.UNSET: + next_assignees = await TaskMutation._users_by_ids(info, data.assignee_ids) + task.assignees = next_assignees - if data.assignee_id is not None: - task.assignee_id = data.assignee_id - task.assignee_team_id = None - elif data.assignee_team_id is not strawberry.UNSET: + next_assignee_team_id = task.assignee_team_id + if data.assignee_team_id is not strawberry.UNSET: + next_assignee_team_id = data.assignee_team_id task.assignee_team_id = data.assignee_team_id - task.assignee_id = None + if data.assignee_team_id is not None: + task.assignees = [] + next_assignees = [] + elif data.assignee_ids is not strawberry.UNSET and len(next_assignees) > 0: + task.assignee_team_id = None + next_assignee_team_id = None + + TaskMutation._validate_task_scope( + task.patient_id, + len(next_assignees), + next_assignee_team_id, + ) if data.properties is not None: property_service = TaskMutation._get_property_service(db) @@ -788,22 +868,27 @@ async def _update_task_field( selectinload(models.Task.patient).selectinload( models.Patient.assigned_locations, ), + selectinload(models.Task.assignees), ), ) task = result.scalars().first() if not task: raise Exception("Task not found") - if task.patient: - auth_service = AuthorizationService(db) - if not await auth_service.can_access_patient( - info.context.user, - task.patient, - info.context, - ): - raise_forbidden() + auth_service = AuthorizationService(db) + if not await auth_service.can_access_task( + info.context.user, + task, + info.context, + ): + raise_forbidden() field_updater(task) + TaskMutation._validate_task_scope( + task.patient_id, + len(task.assignees), + task.assignee_team_id, + ) await BaseMutationResolver.update_and_notify( info, task, @@ -815,31 +900,46 @@ async def _update_task_field( return task @strawberry.mutation - @audit_log("assign_task") - async def assign_task( + @audit_log("add_task_assignee") + async def add_task_assignee( self, info: Info, id: strawberry.ID, user_id: strawberry.ID, ) -> TaskType: + user_result = await info.context.db.execute( + select(models.User).where(models.User.id == user_id) + ) + user = user_result.scalars().first() + if user is None: + raise GraphQLError( + "Assignee user was not found.", + extensions={"code": "BAD_REQUEST"}, + ) return await TaskMutation._update_task_field( info, id, lambda task: ( - setattr(task, "assignee_id", user_id), setattr(task, "assignee_team_id", None), + task.assignees.append(user) if user not in task.assignees else None, ), ) @strawberry.mutation - @audit_log("unassign_task") - async def unassign_task(self, info: Info, id: strawberry.ID) -> TaskType: + @audit_log("remove_task_assignee") + async def remove_task_assignee( + self, + info: Info, + id: strawberry.ID, + user_id: strawberry.ID, + ) -> TaskType: return await TaskMutation._update_task_field( info, id, - lambda task: ( - setattr(task, "assignee_id", None), - setattr(task, "assignee_team_id", None), + lambda task: setattr( + task, + "assignees", + [assignee for assignee in task.assignees if assignee.id != user_id], ), ) @@ -855,8 +955,8 @@ async def assign_task_to_team( info, id, lambda task: ( - setattr(task, "assignee_id", None), setattr(task, "assignee_team_id", team_id), + setattr(task, "assignees", []), ), ) @@ -871,7 +971,6 @@ async def unassign_task_from_team( info, id, lambda task: ( - setattr(task, "assignee_id", None), setattr(task, "assignee_team_id", None), ), ) @@ -917,20 +1016,20 @@ async def delete_task(self, info: Info, id: strawberry.ID) -> bool: selectinload(models.Task.patient).selectinload( models.Patient.assigned_locations, ), + selectinload(models.Task.assignees), ), ) task = result.scalars().first() if not task: return False - if task.patient: - auth_service = AuthorizationService(db) - if not await auth_service.can_access_patient( - info.context.user, - task.patient, - info.context, - ): - raise_forbidden() + auth_service = AuthorizationService(db) + if not await auth_service.can_access_task( + info.context.user, + task, + info.context, + ): + raise_forbidden() patient_id = task.patient_id await BaseMutationResolver.delete_entity( diff --git a/backend/api/resolvers/user.py b/backend/api/resolvers/user.py index e4d97525..4b9ac92a 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/backend/api/services/authorization.py b/backend/api/services/authorization.py index 88449675..eee0cabb 100644 --- a/backend/api/services/authorization.py +++ b/backend/api/services/authorization.py @@ -122,6 +122,35 @@ async def can_access_patient_id( return await self.can_access_patient(user, patient, context) + async def can_access_task( + self, + user: models.User | None, + task: models.Task, + context=None, + ) -> bool: + if not user: + return False + + if task.patient_id: + if task.patient is not None: + return await self.can_access_patient(user, task.patient, context) + return await self.can_access_patient_id(user, task.patient_id, context) + + result = await self.db.execute( + select(models.task_assignees.c.user_id).where( + models.task_assignees.c.task_id == task.id, + models.task_assignees.c.user_id == user.id, + ) + ) + if result.first() is not None: + return True + + if task.assignee_team_id: + accessible_location_ids = await self.get_user_accessible_location_ids(user, context) + return task.assignee_team_id in accessible_location_ids + + return False + def filter_patients_by_access( self, user: models.User | None, query, accessible_location_ids: set[str] | None = None ): diff --git a/backend/api/services/subscription.py b/backend/api/services/subscription.py index 4a44757e..fd773ead 100644 --- a/backend/api/services/subscription.py +++ b/backend/api/services/subscription.py @@ -136,12 +136,29 @@ async def task_belongs_to_root_locations( ) task = result.scalars().first() - if not task or not task.patient: + if not task: return False - return await patient_belongs_to_root_locations( - db, task.patient.id, root_location_ids + if task.patient: + return await patient_belongs_to_root_locations( + db, task.patient.id, root_location_ids + ) + + if not task.assignee_team_id: + return False + + root_cte = ( + select(models.LocationNode.id) + .where(models.LocationNode.id.in_(root_location_ids)) + .cte(name="root_location_descendants", recursive=True) + ) + root_children = select(models.LocationNode.id).join( + root_cte, models.LocationNode.parent_id == root_cte.c.id ) + root_cte = root_cte.union(root_children) + result = await db.execute(select(root_cte.c.id)) + root_location_descendants = {row[0] for row in result.all()} + return task.assignee_team_id in root_location_descendants async def subscribe_with_location_filter( diff --git a/backend/api/types/saved_view.py b/backend/api/types/saved_view.py new file mode 100644 index 00000000..b9eca424 --- /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/api/types/task.py b/backend/api/types/task.py index 6699be02..929aa1c3 100644 --- a/backend/api/types/task.py +++ b/backend/api/types/task.py @@ -6,8 +6,10 @@ from api.types.base import calculate_checksum_for_instance from api.types.property import PropertyValueType from database import models +from sqlalchemy import inspect as sa_inspect from sqlalchemy import select from sqlalchemy.orm import selectinload +from sqlalchemy.orm.attributes import NO_VALUE if TYPE_CHECKING: from api.types.location import LocationNodeType @@ -24,24 +26,29 @@ class TaskType: due_date: datetime | None creation_date: datetime update_date: datetime | None - assignee_id: strawberry.ID | None assignee_team_id: strawberry.ID | None - patient_id: strawberry.ID + patient_id: strawberry.ID | None priority: str | None estimated_time: int | None @strawberry.field - async def assignee( + async def assignees( self, info: Info, - ) -> Annotated["UserType", strawberry.lazy("api.types.user")] | None: - - if not self.assignee_id: - return None + ) -> list[Annotated["UserType", strawberry.lazy("api.types.user")]]: + try: + state = sa_inspect(self) + attr = state.attrs.assignees + if attr.loaded_value is not NO_VALUE: + return list(attr.value) + except Exception: + pass result = await info.context.db.execute( - select(models.User).where(models.User.id == self.assignee_id), + select(models.User) + .join(models.task_assignees, models.task_assignees.c.user_id == models.User.id) + .where(models.task_assignees.c.task_id == self.id), ) - return result.scalars().first() + return result.scalars().all() @strawberry.field async def assignee_team( @@ -59,8 +66,9 @@ async def assignee_team( async def patient( self, info: Info, - ) -> Annotated["PatientType", strawberry.lazy("api.types.patient")]: - + ) -> Annotated["PatientType", strawberry.lazy("api.types.patient")] | None: + if not self.patient_id: + return None result = await info.context.db.execute( select(models.Patient).where(models.Patient.id == self.patient_id), ) diff --git a/backend/api/types/user.py b/backend/api/types/user.py index 37e52fae..373ac3b3 100644 --- a/backend/api/types/user.py +++ b/backend/api/types/user.py @@ -97,7 +97,11 @@ async def tasks( query = ( select(models.Task) - .join(models.Patient, models.Task.patient_id == models.Patient.id) + .join( + models.task_assignees, + models.task_assignees.c.task_id == models.Task.id, + ) + .outerjoin(models.Patient, models.Task.patient_id == models.Patient.id) .outerjoin( patient_locations, models.Patient.id == patient_locations.c.patient_id, @@ -107,32 +111,35 @@ async def tasks( models.Patient.id == patient_teams.c.patient_id, ) .where( - models.Task.assignee_id == self.id, + models.task_assignees.c.user_id == self.id, ( - (models.Patient.clinic_id.in_(select(root_cte.c.id))) - | ( - models.Patient.position_id.isnot(None) - & models.Patient.position_id.in_( - select(root_cte.c.id) - ) - ) + models.Task.patient_id.is_(None) | ( - models.Patient.assigned_location_id.isnot(None) - & models.Patient.assigned_location_id.in_( - select(root_cte.c.id) + ( + (models.Patient.clinic_id.in_(select(root_cte.c.id))) + | ( + models.Patient.position_id.isnot(None) + & models.Patient.position_id.in_( + select(root_cte.c.id) + ) + ) + | ( + models.Patient.assigned_location_id.isnot(None) + & models.Patient.assigned_location_id.in_( + select(root_cte.c.id) + ) + ) + | ( + patient_locations.c.location_id.in_( + select(root_cte.c.id) + ) + ) + | (patient_teams.c.location_id.in_(select(root_cte.c.id))) ) - ) - | ( - patient_locations.c.location_id.in_( - select(root_cte.c.id) + & models.Patient.state.notin_( + [PatientState.DISCHARGED.value, PatientState.DEAD.value] ) ) - | (patient_teams.c.location_id.in_(select(root_cte.c.id))) - ) - ) - .where( - models.Patient.state.notin_( - [PatientState.DISCHARGED.value, PatientState.DEAD.value] ) ) .distinct() 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 1b31f782..df72ddeb 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 6b44a7a5..f84853b1 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/backend/database/migrations/versions/add_saved_views_table.py b/backend/database/migrations/versions/add_saved_views_table.py new file mode 100644 index 00000000..82cc9e00 --- /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/migrations/versions/add_task_assignees_optional_patient.py b/backend/database/migrations/versions/add_task_assignees_optional_patient.py new file mode 100644 index 00000000..f26ccded --- /dev/null +++ b/backend/database/migrations/versions/add_task_assignees_optional_patient.py @@ -0,0 +1,82 @@ +"""Add task_assignees table and make task patient optional. + +Revision ID: task_assignees_opt_patient +Revises: add_patient_deleted +Create Date: 2026-03-23 00:00:00.000000 +""" + +from typing import Sequence, Union + +from alembic import op +import sqlalchemy as sa + + +revision: str = "task_assignees_opt_patient" +down_revision: Union[str, Sequence[str], None] = "add_patient_deleted" +branch_labels: Union[str, Sequence[str], None] = None +depends_on: Union[str, Sequence[str], None] = None + + +def _drop_fk_for_column(table_name: str, column_name: str) -> None: + bind = op.get_bind() + inspector = sa.inspect(bind) + for fk in inspector.get_foreign_keys(table_name): + constrained_columns = fk.get("constrained_columns", []) + if column_name in constrained_columns and fk.get("name"): + op.drop_constraint(fk["name"], table_name, type_="foreignkey") + break + + +def upgrade() -> None: + op.create_table( + "task_assignees", + sa.Column("task_id", sa.String(), nullable=False), + sa.Column("user_id", sa.String(), nullable=False), + sa.ForeignKeyConstraint(["task_id"], ["tasks.id"], ondelete="CASCADE"), + sa.ForeignKeyConstraint(["user_id"], ["users.id"]), + sa.PrimaryKeyConstraint("task_id", "user_id"), + ) + + op.execute( + sa.text( + """ + INSERT INTO task_assignees (task_id, user_id) + SELECT id, assignee_id + FROM tasks + WHERE assignee_id IS NOT NULL + """ + ) + ) + + _drop_fk_for_column("tasks", "assignee_id") + op.drop_column("tasks", "assignee_id") + op.alter_column("tasks", "patient_id", existing_type=sa.String(), nullable=True) + + +def downgrade() -> None: + op.add_column("tasks", sa.Column("assignee_id", sa.String(), nullable=True)) + op.create_foreign_key( + "tasks_assignee_id_fkey", + "tasks", + "users", + ["assignee_id"], + ["id"], + ) + + op.execute( + sa.text( + """ + UPDATE tasks + SET assignee_id = sub.user_id + FROM ( + SELECT task_id, MIN(user_id) AS user_id + FROM task_assignees + GROUP BY task_id + ) AS sub + WHERE tasks.id = sub.task_id + """ + ) + ) + + op.alter_column("tasks", "patient_id", existing_type=sa.String(), nullable=False) + op.drop_table("task_assignees") diff --git a/backend/database/migrations/versions/merge_saved_views_and_task_assignees.py b/backend/database/migrations/versions/merge_saved_views_and_task_assignees.py new file mode 100644 index 00000000..106a4e38 --- /dev/null +++ b/backend/database/migrations/versions/merge_saved_views_and_task_assignees.py @@ -0,0 +1,27 @@ +"""Merge migration heads: saved_views and task_assignees branches. + +Revision ID: merge_saved_views_task_assignees +Revises: add_saved_views_table, task_assignees_opt_patient +Create Date: 2026-03-23 + +""" + +from typing import Sequence, Union + +from alembic import op + +revision: str = "merge_saved_views_task_assignees" +down_revision: Union[str, Sequence[str], None] = ( + "add_saved_views_table", + "task_assignees_opt_patient", +) +branch_labels: Union[str, Sequence[str], None] = None +depends_on: Union[str, Sequence[str], None] = None + + +def upgrade() -> None: + pass + + +def downgrade() -> None: + pass diff --git a/backend/database/models/__init__.py b/backend/database/models/__init__.py index 8108c8c7..5332f16c 100644 --- a/backend/database/models/__init__.py +++ b/backend/database/models/__init__.py @@ -1,6 +1,7 @@ from .user import User, user_root_locations # noqa: F401 from .location import LocationNode, location_organizations # noqa: F401 from .patient import Patient, patient_locations, patient_teams # noqa: F401 -from .task import Task, task_dependencies # noqa: F401 +from .task import Task, task_assignees, 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/patient.py b/backend/database/models/patient.py index 4791a650..9474966a 100644 --- a/backend/database/models/patient.py +++ b/backend/database/models/patient.py @@ -84,7 +84,6 @@ class Patient(Base): tasks: Mapped[list[Task]] = relationship( "Task", back_populates="patient", - cascade="all, delete-orphan", ) properties: Mapped[list[PropertyValue]] = relationship( "PropertyValue", diff --git a/backend/database/models/saved_view.py b/backend/database/models/saved_view.py new file mode 100644 index 00000000..e885d187 --- /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/task.py b/backend/database/models/task.py index 54961151..e0888144 100644 --- a/backend/database/models/task.py +++ b/backend/database/models/task.py @@ -21,6 +21,13 @@ Column("next_task_id", ForeignKey("tasks.id"), primary_key=True), ) +task_assignees = Table( + "task_assignees", + Base.metadata, + Column("task_id", ForeignKey("tasks.id", ondelete="CASCADE"), primary_key=True), + Column("user_id", ForeignKey("users.id"), primary_key=True), +) + class Task(Base): __tablename__ = "tasks" @@ -40,27 +47,24 @@ class Task(Base): default=datetime.now, onupdate=datetime.now, ) - assignee_id: Mapped[str | None] = mapped_column( - ForeignKey("users.id"), - nullable=True, - ) assignee_team_id: Mapped[str | None] = mapped_column( ForeignKey("location_nodes.id"), nullable=True, ) - patient_id: Mapped[str] = mapped_column(ForeignKey("patients.id")) + patient_id: Mapped[str | None] = mapped_column(ForeignKey("patients.id"), nullable=True) priority: Mapped[str | None] = mapped_column(String, nullable=True) estimated_time: Mapped[int | None] = mapped_column(Integer, nullable=True) - assignee: Mapped[User | None] = relationship( + assignees: Mapped[list[User]] = relationship( "User", + secondary=task_assignees, back_populates="tasks", ) assignee_team: Mapped["LocationNode | None"] = relationship( "LocationNode", foreign_keys=[assignee_team_id], ) - patient: Mapped[Patient] = relationship("Patient", back_populates="tasks") + patient: Mapped[Patient | None] = relationship("Patient", back_populates="tasks") properties: Mapped[list[PropertyValue]] = relationship( "PropertyValue", back_populates="task", diff --git a/backend/database/models/user.py b/backend/database/models/user.py index f8b0f0a8..a894badf 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( @@ -43,7 +44,14 @@ class User(Base): nullable=True, ) - tasks: Mapped[list[Task]] = relationship("Task", back_populates="assignee") + tasks: Mapped[list[Task]] = relationship( + "Task", + secondary="task_assignees", + back_populates="assignees", + ) + 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 00000000..a21af7b8 --- /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 c614ec58..6f95fb8e 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; @@ -61,13 +58,22 @@ 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; + assigneeIds?: InputMaybe>; assigneeTeamId?: InputMaybe; description?: InputMaybe; dueDate?: InputMaybe; estimatedTime?: InputMaybe; - patientId: Scalars['ID']['input']; + patientId?: InputMaybe; previousTaskIds?: InputMaybe>; priority?: InputMaybe; properties?: InputMaybe>; @@ -86,83 +92,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; @@ -188,40 +117,44 @@ export enum LocationType { export type Mutation = { __typename?: 'Mutation'; + addTaskAssignee: TaskType; admitPatient: PatientType; - assignTask: TaskType; assignTaskToTeam: TaskType; completeTask: TaskType; 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; + removeTaskAssignee: TaskType; reopenTask: TaskType; - unassignTask: TaskType; unassignTaskFromTeam: TaskType; updateLocationNode: LocationNodeType; updatePatient: PatientType; updateProfilePicture: UserType; updatePropertyDefinition: PropertyDefinitionType; + updateSavedView: SavedView; updateTask: TaskType; waitPatient: PatientType; }; -export type MutationAdmitPatientArgs = { +export type MutationAddTaskAssigneeArgs = { id: Scalars['ID']['input']; + userId: Scalars['ID']['input']; }; -export type MutationAssignTaskArgs = { +export type MutationAdmitPatientArgs = { id: Scalars['ID']['input']; - userId: Scalars['ID']['input']; }; @@ -251,6 +184,11 @@ export type MutationCreatePropertyDefinitionArgs = { }; +export type MutationCreateSavedViewArgs = { + data: CreateSavedViewInput; +}; + + export type MutationCreateTaskArgs = { data: CreateTaskInput; }; @@ -271,6 +209,11 @@ export type MutationDeletePropertyDefinitionArgs = { }; +export type MutationDeleteSavedViewArgs = { + id: Scalars['ID']['input']; +}; + + export type MutationDeleteTaskArgs = { id: Scalars['ID']['input']; }; @@ -281,17 +224,24 @@ export type MutationDischargePatientArgs = { }; +export type MutationDuplicateSavedViewArgs = { + id: Scalars['ID']['input']; + name: Scalars['String']['input']; +}; + + export type MutationMarkPatientDeadArgs = { id: Scalars['ID']['input']; }; -export type MutationReopenTaskArgs = { +export type MutationRemoveTaskAssigneeArgs = { id: Scalars['ID']['input']; + userId: Scalars['ID']['input']; }; -export type MutationUnassignTaskArgs = { +export type MutationReopenTaskArgs = { id: Scalars['ID']['input']; }; @@ -324,6 +274,12 @@ export type MutationUpdatePropertyDefinitionArgs = { }; +export type MutationUpdateSavedViewArgs = { + data: UpdateSavedViewInput; + id: Scalars['ID']['input']; +}; + + export type MutationUpdateTaskArgs = { data: UpdateTaskInput; id: Scalars['ID']['input']; @@ -426,14 +382,17 @@ export type Query = { locationNodes: Array; locationRoots: Array; me?: Maybe; + mySavedViews: Array; patient?: Maybe; patients: Array; patientsTotal: Scalars['Int']['output']; propertyDefinitions: Array; + queryableFields: Array; recentPatients: Array; recentPatientsTotal: Scalars['Int']['output']; recentTasks: Array; recentTasksTotal: Scalars['Int']['output']; + savedView?: Maybe; task?: Maybe; tasks: Array; tasksTotal: Scalars['Int']['output']; @@ -471,53 +430,67 @@ 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>; +}; + + +export type QuerySavedViewArgs = { + id: Scalars['ID']['input']; }; @@ -529,23 +502,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 +528,145 @@ 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; + dateValue?: InputMaybe; + floatMax?: InputMaybe; + floatMin?: InputMaybe; + floatValue?: 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; + 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; +}; + +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 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', @@ -572,13 +678,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']; @@ -645,10 +744,9 @@ export enum TaskPriority { export type TaskType = { __typename?: 'TaskType'; - assignee?: Maybe; - assigneeId?: Maybe; assigneeTeam?: Maybe; assigneeTeamId?: Maybe; + assignees: Array; checksum: Scalars['String']['output']; creationDate: Scalars['DateTime']['output']; description?: Maybe; @@ -656,8 +754,8 @@ export type TaskType = { dueDate?: Maybe; estimatedTime?: Maybe; id: Scalars['ID']['output']; - patient: PatientType; - patientId: Scalars['ID']['output']; + patient?: Maybe; + patientId?: Maybe; priority?: Maybe; properties: Array; title: Scalars['String']['output']; @@ -697,14 +795,23 @@ export type UpdatePropertyDefinitionInput = { options?: InputMaybe>; }; +export type UpdateSavedViewInput = { + filterDefinition?: InputMaybe; + name?: InputMaybe; + parameters?: InputMaybe; + sortDefinition?: InputMaybe; + visibility?: InputMaybe; +}; + export type UpdateTaskInput = { - assigneeId?: InputMaybe; + assigneeIds?: InputMaybe>; assigneeTeamId?: InputMaybe; checksum?: InputMaybe; description?: InputMaybe; done?: InputMaybe; dueDate?: InputMaybe; estimatedTime?: InputMaybe; + patientId?: InputMaybe; previousTaskIds?: InputMaybe>; priority?: InputMaybe; properties?: InputMaybe>; @@ -760,62 +867,62 @@ export type GetLocationsQuery = { __typename?: 'Query', locationNodes: Array<{ _ export type GetMyTasksQueryVariables = Exact<{ [key: string]: never; }>; -export type GetMyTasksQuery = { __typename?: 'Query', me?: { __typename?: 'UserType', id: string, tasks: Array<{ __typename?: 'TaskType', id: string, title: string, description?: string | null, done: boolean, dueDate?: any | null, priority?: string | null, estimatedTime?: number | null, creationDate: any, updateDate?: any | null, patient: { __typename?: 'PatientType', id: string, name: string, assignedLocation?: { __typename?: 'LocationNodeType', id: string, title: string, parent?: { __typename?: 'LocationNodeType', id: string, title: string } | null } | null, assignedLocations: Array<{ __typename?: 'LocationNodeType', id: string, title: string, kind: LocationType, parent?: { __typename?: 'LocationNodeType', id: string, title: string, parent?: { __typename?: 'LocationNodeType', id: string, title: string } | null } | null }> }, assignee?: { __typename?: 'UserType', id: string, name: string, avatarUrl?: string | null, lastOnline?: any | null, isOnline: boolean } | null }> } | null }; +export type GetMyTasksQuery = { __typename?: 'Query', me?: { __typename?: 'UserType', id: string, tasks: Array<{ __typename?: 'TaskType', id: string, title: string, description?: string | null, done: boolean, dueDate?: any | null, priority?: string | null, estimatedTime?: number | null, creationDate: any, updateDate?: any | null, patient?: { __typename?: 'PatientType', id: string, name: string, assignedLocation?: { __typename?: 'LocationNodeType', id: string, title: string, parent?: { __typename?: 'LocationNodeType', id: string, title: string } | null } | null, assignedLocations: Array<{ __typename?: 'LocationNodeType', id: string, title: string, kind: LocationType, parent?: { __typename?: 'LocationNodeType', id: string, title: string, parent?: { __typename?: 'LocationNodeType', id: string, title: string } | null } | null }> } | null, assignees: Array<{ __typename?: 'UserType', id: string, name: string, avatarUrl?: string | null, lastOnline?: any | null, isOnline: boolean }> }> } | null }; 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; }>; -export type GetOverviewDataQuery = { __typename?: 'Query', recentPatientsTotal: number, recentTasksTotal: number, recentPatients: Array<{ __typename?: 'PatientType', id: string, name: string, sex: Sex, birthdate: any, position?: { __typename?: 'LocationNodeType', id: string, title: string, kind: LocationType, parent?: { __typename?: 'LocationNodeType', id: string, title: string } | null } | null, tasks: Array<{ __typename?: 'TaskType', updateDate?: any | null }>, properties: Array<{ __typename?: 'PropertyValueType', id: string, textValue?: string | null, numberValue?: number | null, booleanValue?: boolean | null, dateValue?: any | null, dateTimeValue?: any | null, selectValue?: string | null, multiSelectValues?: Array | null, userValue?: string | null, definition: { __typename?: 'PropertyDefinitionType', id: string, name: string, description?: string | null, fieldType: FieldType, isActive: boolean, allowedEntities: Array, options: Array }, user?: { __typename?: 'UserType', id: string, name: string, avatarUrl?: string | null, lastOnline?: any | null, isOnline: boolean } | null, team?: { __typename?: 'LocationNodeType', id: string, title: string, kind: LocationType } | null }> }>, recentTasks: Array<{ __typename?: 'TaskType', id: string, title: string, description?: string | null, done: boolean, dueDate?: any | null, updateDate?: any | null, priority?: string | null, assignee?: { __typename?: 'UserType', id: string, name: string, avatarUrl?: string | null, lastOnline?: any | null, isOnline: boolean } | null, patient: { __typename?: 'PatientType', id: string, name: string, position?: { __typename?: 'LocationNodeType', id: string, title: string, kind: LocationType, parent?: { __typename?: 'LocationNodeType', id: string, title: string } | null } | null }, properties: Array<{ __typename?: 'PropertyValueType', id: string, textValue?: string | null, numberValue?: number | null, booleanValue?: boolean | null, dateValue?: any | null, dateTimeValue?: any | null, selectValue?: string | null, multiSelectValues?: Array | null, userValue?: string | null, definition: { __typename?: 'PropertyDefinitionType', id: string, name: string, description?: string | null, fieldType: FieldType, isActive: boolean, allowedEntities: Array, options: Array }, user?: { __typename?: 'UserType', id: string, name: string, avatarUrl?: string | null, lastOnline?: any | null, isOnline: boolean } | null, team?: { __typename?: 'LocationNodeType', id: string, title: string, kind: LocationType } | null }> }> }; +export type GetOverviewDataQuery = { __typename?: 'Query', recentPatientsTotal: number, recentTasksTotal: number, recentPatients: Array<{ __typename?: 'PatientType', id: string, name: string, sex: Sex, birthdate: any, position?: { __typename?: 'LocationNodeType', id: string, title: string, kind: LocationType, parent?: { __typename?: 'LocationNodeType', id: string, title: string } | null } | null, tasks: Array<{ __typename?: 'TaskType', updateDate?: any | null }>, properties: Array<{ __typename?: 'PropertyValueType', id: string, textValue?: string | null, numberValue?: number | null, booleanValue?: boolean | null, dateValue?: any | null, dateTimeValue?: any | null, selectValue?: string | null, multiSelectValues?: Array | null, userValue?: string | null, definition: { __typename?: 'PropertyDefinitionType', id: string, name: string, description?: string | null, fieldType: FieldType, isActive: boolean, allowedEntities: Array, options: Array }, user?: { __typename?: 'UserType', id: string, name: string, avatarUrl?: string | null, lastOnline?: any | null, isOnline: boolean } | null, team?: { __typename?: 'LocationNodeType', id: string, title: string, kind: LocationType } | null }> }>, recentTasks: Array<{ __typename?: 'TaskType', id: string, title: string, description?: string | null, done: boolean, dueDate?: any | null, updateDate?: any | null, priority?: string | null, assignees: Array<{ __typename?: 'UserType', id: string, name: string, avatarUrl?: string | null, lastOnline?: any | null, isOnline: boolean }>, patient?: { __typename?: 'PatientType', id: string, name: string, position?: { __typename?: 'LocationNodeType', id: string, title: string, kind: LocationType, parent?: { __typename?: 'LocationNodeType', id: string, title: string } | null } | null } | null, properties: Array<{ __typename?: 'PropertyValueType', id: string, textValue?: string | null, numberValue?: number | null, booleanValue?: boolean | null, dateValue?: any | null, dateTimeValue?: any | null, selectValue?: string | null, multiSelectValues?: Array | null, userValue?: string | null, definition: { __typename?: 'PropertyDefinitionType', id: string, name: string, description?: string | null, fieldType: FieldType, isActive: boolean, allowedEntities: Array, options: Array }, user?: { __typename?: 'UserType', id: string, name: string, avatarUrl?: string | null, lastOnline?: any | null, isOnline: boolean } | null, team?: { __typename?: 'LocationNodeType', id: string, title: string, kind: LocationType } | null }> }> }; export type GetPatientQueryVariables = Exact<{ id: Scalars['ID']['input']; }>; -export type GetPatientQuery = { __typename?: 'Query', patient?: { __typename?: 'PatientType', id: string, firstname: string, lastname: string, birthdate: any, sex: Sex, state: PatientState, description?: string | null, checksum: string, assignedLocation?: { __typename?: 'LocationNodeType', id: string, title: string } | null, assignedLocations: Array<{ __typename?: 'LocationNodeType', id: string, title: string }>, clinic: { __typename?: 'LocationNodeType', id: string, title: string, kind: LocationType, parent?: { __typename?: 'LocationNodeType', id: string, title: string, parent?: { __typename?: 'LocationNodeType', id: string, title: string, parent?: { __typename?: 'LocationNodeType', id: string, title: string, parent?: { __typename?: 'LocationNodeType', id: string, title: string } | null } | null } | null } | null }, position?: { __typename?: 'LocationNodeType', id: string, title: string, kind: LocationType, parent?: { __typename?: 'LocationNodeType', id: string, title: string, parent?: { __typename?: 'LocationNodeType', id: string, title: string, parent?: { __typename?: 'LocationNodeType', id: string, title: string, parent?: { __typename?: 'LocationNodeType', id: string, title: string } | null } | null } | null } | null } | null, teams: Array<{ __typename?: 'LocationNodeType', id: string, title: string, kind: LocationType, parent?: { __typename?: 'LocationNodeType', id: string, title: string, parent?: { __typename?: 'LocationNodeType', id: string, title: string, parent?: { __typename?: 'LocationNodeType', id: string, title: string, parent?: { __typename?: 'LocationNodeType', id: string, title: string } | null } | null } | null } | null }>, tasks: Array<{ __typename?: 'TaskType', id: string, title: string, description?: string | null, done: boolean, dueDate?: any | null, priority?: string | null, estimatedTime?: number | null, updateDate?: any | null, assignee?: { __typename?: 'UserType', id: string, name: string, avatarUrl?: string | null, lastOnline?: any | null, isOnline: boolean } | null, assigneeTeam?: { __typename?: 'LocationNodeType', id: string, title: string, kind: LocationType } | null }>, properties: Array<{ __typename?: 'PropertyValueType', id: string, textValue?: string | null, numberValue?: number | null, booleanValue?: boolean | null, dateValue?: any | null, dateTimeValue?: any | null, selectValue?: string | null, multiSelectValues?: Array | null, userValue?: string | null, definition: { __typename?: 'PropertyDefinitionType', id: string, name: string, description?: string | null, fieldType: FieldType, isActive: boolean, allowedEntities: Array, options: Array }, user?: { __typename?: 'UserType', id: string, name: string, avatarUrl?: string | null, lastOnline?: any | null, isOnline: boolean } | null, team?: { __typename?: 'LocationNodeType', id: string, title: string, kind: LocationType } | null }> } | null }; +export type GetPatientQuery = { __typename?: 'Query', patient?: { __typename?: 'PatientType', id: string, firstname: string, lastname: string, birthdate: any, sex: Sex, state: PatientState, description?: string | null, checksum: string, assignedLocation?: { __typename?: 'LocationNodeType', id: string, title: string } | null, assignedLocations: Array<{ __typename?: 'LocationNodeType', id: string, title: string }>, clinic: { __typename?: 'LocationNodeType', id: string, title: string, kind: LocationType, parent?: { __typename?: 'LocationNodeType', id: string, title: string, parent?: { __typename?: 'LocationNodeType', id: string, title: string, parent?: { __typename?: 'LocationNodeType', id: string, title: string, parent?: { __typename?: 'LocationNodeType', id: string, title: string } | null } | null } | null } | null }, position?: { __typename?: 'LocationNodeType', id: string, title: string, kind: LocationType, parent?: { __typename?: 'LocationNodeType', id: string, title: string, parent?: { __typename?: 'LocationNodeType', id: string, title: string, parent?: { __typename?: 'LocationNodeType', id: string, title: string, parent?: { __typename?: 'LocationNodeType', id: string, title: string } | null } | null } | null } | null } | null, teams: Array<{ __typename?: 'LocationNodeType', id: string, title: string, kind: LocationType, parent?: { __typename?: 'LocationNodeType', id: string, title: string, parent?: { __typename?: 'LocationNodeType', id: string, title: string, parent?: { __typename?: 'LocationNodeType', id: string, title: string, parent?: { __typename?: 'LocationNodeType', id: string, title: string } | null } | null } | null } | null }>, tasks: Array<{ __typename?: 'TaskType', id: string, title: string, description?: string | null, done: boolean, dueDate?: any | null, priority?: string | null, estimatedTime?: number | null, updateDate?: any | null, assignees: Array<{ __typename?: 'UserType', id: string, name: string, avatarUrl?: string | null, lastOnline?: any | null, isOnline: boolean }>, assigneeTeam?: { __typename?: 'LocationNodeType', id: string, title: string, kind: LocationType } | null }>, properties: Array<{ __typename?: 'PropertyValueType', id: string, textValue?: string | null, numberValue?: number | null, booleanValue?: boolean | null, dateValue?: any | null, dateTimeValue?: any | null, selectValue?: string | null, multiSelectValues?: Array | null, userValue?: string | null, definition: { __typename?: 'PropertyDefinitionType', id: string, name: string, description?: string | null, fieldType: FieldType, isActive: boolean, allowedEntities: Array, options: Array }, user?: { __typename?: 'UserType', id: string, name: string, avatarUrl?: string | null, lastOnline?: any | null, isOnline: boolean } | null, team?: { __typename?: 'LocationNodeType', id: string, title: string, kind: LocationType } | null }> } | null }; 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; }>; -export type GetPatientsQuery = { __typename?: 'Query', patientsTotal: number, patients: Array<{ __typename?: 'PatientType', id: string, name: string, firstname: string, lastname: string, birthdate: any, sex: Sex, state: PatientState, assignedLocation?: { __typename?: 'LocationNodeType', id: string, title: string, parent?: { __typename?: 'LocationNodeType', id: string, title: string } | null } | null, assignedLocations: Array<{ __typename?: 'LocationNodeType', id: string, title: string, kind: LocationType, parent?: { __typename?: 'LocationNodeType', id: string, title: string, parent?: { __typename?: 'LocationNodeType', id: string, title: string, parent?: { __typename?: 'LocationNodeType', id: string, title: string } | null } | null } | null }>, clinic: { __typename?: 'LocationNodeType', id: string, title: string, kind: LocationType, parent?: { __typename?: 'LocationNodeType', id: string, title: string, parent?: { __typename?: 'LocationNodeType', id: string, title: string, parent?: { __typename?: 'LocationNodeType', id: string, title: string, parent?: { __typename?: 'LocationNodeType', id: string, title: string } | null } | null } | null } | null }, position?: { __typename?: 'LocationNodeType', id: string, title: string, kind: LocationType, parent?: { __typename?: 'LocationNodeType', id: string, title: string, kind: LocationType, parent?: { __typename?: 'LocationNodeType', id: string, title: string, kind: LocationType, parent?: { __typename?: 'LocationNodeType', id: string, title: string, kind: LocationType, parent?: { __typename?: 'LocationNodeType', id: string, title: string, kind: LocationType } | null } | null } | null } | null } | null, teams: Array<{ __typename?: 'LocationNodeType', id: string, title: string, kind: LocationType, parent?: { __typename?: 'LocationNodeType', id: string, title: string, parent?: { __typename?: 'LocationNodeType', id: string, title: string, parent?: { __typename?: 'LocationNodeType', id: string, title: string, parent?: { __typename?: 'LocationNodeType', id: string, title: string } | null } | null } | null } | null }>, tasks: Array<{ __typename?: 'TaskType', id: string, title: string, description?: string | null, done: boolean, dueDate?: any | null, priority?: string | null, estimatedTime?: number | null, creationDate: any, updateDate?: any | null, assignee?: { __typename?: 'UserType', id: string, name: string, avatarUrl?: string | null, lastOnline?: any | null, isOnline: boolean } | null, assigneeTeam?: { __typename?: 'LocationNodeType', id: string, title: string, kind: LocationType } | null }>, properties: Array<{ __typename?: 'PropertyValueType', id: string, textValue?: string | null, numberValue?: number | null, booleanValue?: boolean | null, dateValue?: any | null, dateTimeValue?: any | null, selectValue?: string | null, multiSelectValues?: Array | null, userValue?: string | null, definition: { __typename?: 'PropertyDefinitionType', id: string, name: string, description?: string | null, fieldType: FieldType, isActive: boolean, allowedEntities: Array, options: Array }, user?: { __typename?: 'UserType', id: string, name: string, avatarUrl?: string | null, lastOnline?: any | null, isOnline: boolean } | null, team?: { __typename?: 'LocationNodeType', id: string, title: string, kind: LocationType } | null }> }> }; +export type GetPatientsQuery = { __typename?: 'Query', patientsTotal: number, patients: Array<{ __typename?: 'PatientType', id: string, name: string, firstname: string, lastname: string, birthdate: any, sex: Sex, state: PatientState, assignedLocation?: { __typename?: 'LocationNodeType', id: string, title: string, parent?: { __typename?: 'LocationNodeType', id: string, title: string } | null } | null, assignedLocations: Array<{ __typename?: 'LocationNodeType', id: string, title: string, kind: LocationType, parent?: { __typename?: 'LocationNodeType', id: string, title: string, parent?: { __typename?: 'LocationNodeType', id: string, title: string, parent?: { __typename?: 'LocationNodeType', id: string, title: string } | null } | null } | null }>, clinic: { __typename?: 'LocationNodeType', id: string, title: string, kind: LocationType, parent?: { __typename?: 'LocationNodeType', id: string, title: string, parent?: { __typename?: 'LocationNodeType', id: string, title: string, parent?: { __typename?: 'LocationNodeType', id: string, title: string, parent?: { __typename?: 'LocationNodeType', id: string, title: string } | null } | null } | null } | null }, position?: { __typename?: 'LocationNodeType', id: string, title: string, kind: LocationType, parent?: { __typename?: 'LocationNodeType', id: string, title: string, kind: LocationType, parent?: { __typename?: 'LocationNodeType', id: string, title: string, kind: LocationType, parent?: { __typename?: 'LocationNodeType', id: string, title: string, kind: LocationType, parent?: { __typename?: 'LocationNodeType', id: string, title: string, kind: LocationType } | null } | null } | null } | null } | null, teams: Array<{ __typename?: 'LocationNodeType', id: string, title: string, kind: LocationType, parent?: { __typename?: 'LocationNodeType', id: string, title: string, parent?: { __typename?: 'LocationNodeType', id: string, title: string, parent?: { __typename?: 'LocationNodeType', id: string, title: string, parent?: { __typename?: 'LocationNodeType', id: string, title: string } | null } | null } | null } | null }>, tasks: Array<{ __typename?: 'TaskType', id: string, title: string, description?: string | null, done: boolean, dueDate?: any | null, priority?: string | null, estimatedTime?: number | null, creationDate: any, updateDate?: any | null, assignees: Array<{ __typename?: 'UserType', id: string, name: string, avatarUrl?: string | null, lastOnline?: any | null, isOnline: boolean }>, assigneeTeam?: { __typename?: 'LocationNodeType', id: string, title: string, kind: LocationType } | null }>, properties: Array<{ __typename?: 'PropertyValueType', id: string, textValue?: string | null, numberValue?: number | null, booleanValue?: boolean | null, dateValue?: any | null, dateTimeValue?: any | null, selectValue?: string | null, multiSelectValues?: Array | null, userValue?: string | null, definition: { __typename?: 'PropertyDefinitionType', id: string, name: string, description?: string | null, fieldType: FieldType, isActive: boolean, allowedEntities: Array, options: Array }, user?: { __typename?: 'UserType', id: string, name: string, avatarUrl?: string | null, lastOnline?: any | null, isOnline: boolean } | null, team?: { __typename?: 'LocationNodeType', id: string, title: string, kind: LocationType } | null }> }> }; export type GetTaskQueryVariables = Exact<{ id: Scalars['ID']['input']; }>; -export type GetTaskQuery = { __typename?: 'Query', task?: { __typename?: 'TaskType', id: string, title: string, description?: string | null, done: boolean, dueDate?: any | null, priority?: string | null, estimatedTime?: number | null, checksum: string, updateDate?: any | null, patient: { __typename?: 'PatientType', id: string, name: string }, assignee?: { __typename?: 'UserType', id: string, name: string, avatarUrl?: string | null, lastOnline?: any | null, isOnline: boolean } | null, assigneeTeam?: { __typename?: 'LocationNodeType', id: string, title: string, kind: LocationType } | null, properties: Array<{ __typename?: 'PropertyValueType', id: string, textValue?: string | null, numberValue?: number | null, booleanValue?: boolean | null, dateValue?: any | null, dateTimeValue?: any | null, selectValue?: string | null, multiSelectValues?: Array | null, userValue?: string | null, definition: { __typename?: 'PropertyDefinitionType', id: string, name: string, description?: string | null, fieldType: FieldType, isActive: boolean, allowedEntities: Array, options: Array }, user?: { __typename?: 'UserType', id: string, name: string, avatarUrl?: string | null, lastOnline?: any | null, isOnline: boolean } | null, team?: { __typename?: 'LocationNodeType', id: string, title: string, kind: LocationType } | null }> } | null }; +export type GetTaskQuery = { __typename?: 'Query', task?: { __typename?: 'TaskType', id: string, title: string, description?: string | null, done: boolean, dueDate?: any | null, priority?: string | null, estimatedTime?: number | null, checksum: string, updateDate?: any | null, patient?: { __typename?: 'PatientType', id: string, name: string } | null, assignees: Array<{ __typename?: 'UserType', id: string, name: string, avatarUrl?: string | null, lastOnline?: any | null, isOnline: boolean }>, assigneeTeam?: { __typename?: 'LocationNodeType', id: string, title: string, kind: LocationType } | null, properties: Array<{ __typename?: 'PropertyValueType', id: string, textValue?: string | null, numberValue?: number | null, booleanValue?: boolean | null, dateValue?: any | null, dateTimeValue?: any | null, selectValue?: string | null, multiSelectValues?: Array | null, userValue?: string | null, definition: { __typename?: 'PropertyDefinitionType', id: string, name: string, description?: string | null, fieldType: FieldType, isActive: boolean, allowedEntities: Array, options: Array }, user?: { __typename?: 'UserType', id: string, name: string, avatarUrl?: string | null, lastOnline?: any | null, isOnline: boolean } | null, team?: { __typename?: 'LocationNodeType', id: string, title: string, kind: LocationType } | null }> } | null }; 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; }>; -export type GetTasksQuery = { __typename?: 'Query', tasksTotal: number, tasks: Array<{ __typename?: 'TaskType', id: string, title: string, description?: string | null, done: boolean, dueDate?: any | null, priority?: string | null, estimatedTime?: number | null, creationDate: any, updateDate?: any | null, patient: { __typename?: 'PatientType', id: string, name: string, assignedLocation?: { __typename?: 'LocationNodeType', id: string, title: string, parent?: { __typename?: 'LocationNodeType', id: string, title: string } | null } | null, assignedLocations: Array<{ __typename?: 'LocationNodeType', id: string, title: string, kind: LocationType, parent?: { __typename?: 'LocationNodeType', id: string, title: string, kind: LocationType, parent?: { __typename?: 'LocationNodeType', id: string, title: string, kind: LocationType, parent?: { __typename?: 'LocationNodeType', id: string, title: string, kind: LocationType } | null } | null } | null }> }, assignee?: { __typename?: 'UserType', id: string, name: string, avatarUrl?: string | null, lastOnline?: any | null, isOnline: boolean } | null, assigneeTeam?: { __typename?: 'LocationNodeType', id: string, title: string, kind: LocationType } | null, properties: Array<{ __typename?: 'PropertyValueType', id: string, textValue?: string | null, numberValue?: number | null, booleanValue?: boolean | null, dateValue?: any | null, dateTimeValue?: any | null, selectValue?: string | null, multiSelectValues?: Array | null, userValue?: string | null, definition: { __typename?: 'PropertyDefinitionType', id: string, name: string, description?: string | null, fieldType: FieldType, isActive: boolean, allowedEntities: Array, options: Array }, user?: { __typename?: 'UserType', id: string, name: string, avatarUrl?: string | null, lastOnline?: any | null, isOnline: boolean } | null, team?: { __typename?: 'LocationNodeType', id: string, title: string, kind: LocationType } | null }> }> }; +export type GetTasksQuery = { __typename?: 'Query', tasksTotal: number, tasks: Array<{ __typename?: 'TaskType', id: string, title: string, description?: string | null, done: boolean, dueDate?: any | null, priority?: string | null, estimatedTime?: number | null, creationDate: any, updateDate?: any | null, patient?: { __typename?: 'PatientType', id: string, name: string, assignedLocation?: { __typename?: 'LocationNodeType', id: string, title: string, parent?: { __typename?: 'LocationNodeType', id: string, title: string } | null } | null, assignedLocations: Array<{ __typename?: 'LocationNodeType', id: string, title: string, kind: LocationType, parent?: { __typename?: 'LocationNodeType', id: string, title: string, kind: LocationType, parent?: { __typename?: 'LocationNodeType', id: string, title: string, kind: LocationType, parent?: { __typename?: 'LocationNodeType', id: string, title: string, kind: LocationType } | null } | null } | null }> } | null, assignees: Array<{ __typename?: 'UserType', id: string, name: string, avatarUrl?: string | null, lastOnline?: any | null, isOnline: boolean }>, assigneeTeam?: { __typename?: 'LocationNodeType', id: string, title: string, kind: LocationType } | null, properties: Array<{ __typename?: 'PropertyValueType', id: string, textValue?: string | null, numberValue?: number | null, booleanValue?: boolean | null, dateValue?: any | null, dateTimeValue?: any | null, selectValue?: string | null, multiSelectValues?: Array | null, userValue?: string | null, definition: { __typename?: 'PropertyDefinitionType', id: string, name: string, description?: string | null, fieldType: FieldType, isActive: boolean, allowedEntities: Array, options: Array }, user?: { __typename?: 'UserType', id: string, name: string, avatarUrl?: string | null, lastOnline?: any | null, isOnline: boolean } | null, team?: { __typename?: 'LocationNodeType', id: string, title: string, kind: LocationType } | null }> }> }; export type GetUserQueryVariables = Exact<{ id: Scalars['ID']['input']; @@ -921,6 +1028,55 @@ 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, 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; }>; + + +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']>; }>; @@ -988,7 +1144,7 @@ export type CreateTaskMutationVariables = Exact<{ }>; -export type CreateTaskMutation = { __typename?: 'Mutation', createTask: { __typename?: 'TaskType', id: string, title: string, description?: string | null, done: boolean, dueDate?: any | null, updateDate?: any | null, assignee?: { __typename?: 'UserType', id: string, name: string, avatarUrl?: string | null, lastOnline?: any | null, isOnline: boolean } | null, patient: { __typename?: 'PatientType', id: string, name: string } } }; +export type CreateTaskMutation = { __typename?: 'Mutation', createTask: { __typename?: 'TaskType', id: string, title: string, description?: string | null, done: boolean, dueDate?: any | null, updateDate?: any | null, assignees: Array<{ __typename?: 'UserType', id: string, name: string, avatarUrl?: string | null, lastOnline?: any | null, isOnline: boolean }>, patient?: { __typename?: 'PatientType', id: string, name: string } | null } }; export type UpdateTaskMutationVariables = Exact<{ id: Scalars['ID']['input']; @@ -996,22 +1152,23 @@ export type UpdateTaskMutationVariables = Exact<{ }>; -export type UpdateTaskMutation = { __typename?: 'Mutation', updateTask: { __typename?: 'TaskType', id: string, title: string, description?: string | null, done: boolean, dueDate?: any | null, priority?: string | null, estimatedTime?: number | null, updateDate?: any | null, checksum: string, patient: { __typename?: 'PatientType', id: string, name: string }, assignee?: { __typename?: 'UserType', id: string, name: string, avatarUrl?: string | null, lastOnline?: any | null, isOnline: boolean } | null, properties: Array<{ __typename?: 'PropertyValueType', id: string, textValue?: string | null, numberValue?: number | null, booleanValue?: boolean | null, dateValue?: any | null, dateTimeValue?: any | null, selectValue?: string | null, multiSelectValues?: Array | null, userValue?: string | null, definition: { __typename?: 'PropertyDefinitionType', id: string, name: string, description?: string | null, fieldType: FieldType, isActive: boolean, allowedEntities: Array, options: Array }, user?: { __typename?: 'UserType', id: string, name: string, avatarUrl?: string | null, lastOnline?: any | null, isOnline: boolean } | null, team?: { __typename?: 'LocationNodeType', id: string, title: string, kind: LocationType } | null }> } }; +export type UpdateTaskMutation = { __typename?: 'Mutation', updateTask: { __typename?: 'TaskType', id: string, title: string, description?: string | null, done: boolean, dueDate?: any | null, priority?: string | null, estimatedTime?: number | null, updateDate?: any | null, checksum: string, patient?: { __typename?: 'PatientType', id: string, name: string } | null, assignees: Array<{ __typename?: 'UserType', id: string, name: string, avatarUrl?: string | null, lastOnline?: any | null, isOnline: boolean }>, properties: Array<{ __typename?: 'PropertyValueType', id: string, textValue?: string | null, numberValue?: number | null, booleanValue?: boolean | null, dateValue?: any | null, dateTimeValue?: any | null, selectValue?: string | null, multiSelectValues?: Array | null, userValue?: string | null, definition: { __typename?: 'PropertyDefinitionType', id: string, name: string, description?: string | null, fieldType: FieldType, isActive: boolean, allowedEntities: Array, options: Array }, user?: { __typename?: 'UserType', id: string, name: string, avatarUrl?: string | null, lastOnline?: any | null, isOnline: boolean } | null, team?: { __typename?: 'LocationNodeType', id: string, title: string, kind: LocationType } | null }> } }; -export type AssignTaskMutationVariables = Exact<{ +export type AddTaskAssigneeMutationVariables = Exact<{ id: Scalars['ID']['input']; userId: Scalars['ID']['input']; }>; -export type AssignTaskMutation = { __typename?: 'Mutation', assignTask: { __typename?: 'TaskType', id: string, assignee?: { __typename?: 'UserType', id: string, name: string, avatarUrl?: string | null, lastOnline?: any | null, isOnline: boolean } | null } }; +export type AddTaskAssigneeMutation = { __typename?: 'Mutation', addTaskAssignee: { __typename?: 'TaskType', id: string, assignees: Array<{ __typename?: 'UserType', id: string, name: string, avatarUrl?: string | null, lastOnline?: any | null, isOnline: boolean }> } }; -export type UnassignTaskMutationVariables = Exact<{ +export type RemoveTaskAssigneeMutationVariables = Exact<{ id: Scalars['ID']['input']; + userId: Scalars['ID']['input']; }>; -export type UnassignTaskMutation = { __typename?: 'Mutation', unassignTask: { __typename?: 'TaskType', id: string, assignee?: { __typename?: 'UserType', id: string, name: string, avatarUrl?: string | null, lastOnline?: any | null, isOnline: boolean } | null } }; +export type RemoveTaskAssigneeMutation = { __typename?: 'Mutation', removeTaskAssignee: { __typename?: 'TaskType', id: string, assignees: Array<{ __typename?: 'UserType', id: string, name: string, avatarUrl?: string | null, lastOnline?: any | null, isOnline: boolean }> } }; export type DeleteTaskMutationVariables = Exact<{ id: Scalars['ID']['input']; @@ -1060,12 +1217,12 @@ export type UpdateProfilePictureMutation = { __typename?: 'Mutation', updateProf export const GetAuditLogsDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"GetAuditLogs"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"caseId"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"ID"}}}},{"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":"auditLogs"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"caseId"},"value":{"kind":"Variable","name":{"kind":"Name","value":"caseId"}}},{"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":"caseId"}},{"kind":"Field","name":{"kind":"Name","value":"activity"}},{"kind":"Field","name":{"kind":"Name","value":"userId"}},{"kind":"Field","name":{"kind":"Name","value":"timestamp"}},{"kind":"Field","name":{"kind":"Name","value":"context"}}]}}]}}]} as unknown as DocumentNode; 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 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; -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 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":"assignees"},"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":"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":"assignees"},"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":"assignees"},"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":"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":"assignees"},"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":"assignees"},"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":"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":"assignees"},"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 +1238,13 @@ 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":"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; +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; @@ -1090,10 +1254,10 @@ export const TaskDeletedDocument = {"kind":"Document","definitions":[{"kind":"Op export const LocationNodeUpdatedDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"subscription","name":{"kind":"Name","value":"LocationNodeUpdated"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"locationId"}},"type":{"kind":"NamedType","name":{"kind":"Name","value":"ID"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"locationNodeUpdated"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"locationId"},"value":{"kind":"Variable","name":{"kind":"Name","value":"locationId"}}}]}]}}]} as unknown as DocumentNode; export const LocationNodeCreatedDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"subscription","name":{"kind":"Name","value":"LocationNodeCreated"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"locationNodeCreated"}}]}}]} as unknown as DocumentNode; export const LocationNodeDeletedDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"subscription","name":{"kind":"Name","value":"LocationNodeDeleted"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"locationNodeDeleted"}}]}}]} as unknown as DocumentNode; -export const CreateTaskDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"CreateTask"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"data"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"CreateTaskInput"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"createTask"},"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":"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":"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"}}]}}]}}]}}]} as unknown as DocumentNode; -export const UpdateTaskDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"UpdateTask"},"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":"UpdateTaskInput"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"updateTask"},"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":"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":"checksum"}},{"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":"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 AssignTaskDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"AssignTask"},"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":"userId"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"ID"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"assignTask"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"id"},"value":{"kind":"Variable","name":{"kind":"Name","value":"id"}}},{"kind":"Argument","name":{"kind":"Name","value":"userId"},"value":{"kind":"Variable","name":{"kind":"Name","value":"userId"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"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 UnassignTaskDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"UnassignTask"},"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":"unassignTask"},"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":"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 CreateTaskDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"CreateTask"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"data"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"CreateTaskInput"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"createTask"},"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":"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":"assignees"},"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"}}]}}]}}]}}]} as unknown as DocumentNode; +export const UpdateTaskDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"UpdateTask"},"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":"UpdateTaskInput"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"updateTask"},"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":"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":"checksum"}},{"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":"assignees"},"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":"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 AddTaskAssigneeDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"AddTaskAssignee"},"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":"userId"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"ID"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"addTaskAssignee"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"id"},"value":{"kind":"Variable","name":{"kind":"Name","value":"id"}}},{"kind":"Argument","name":{"kind":"Name","value":"userId"},"value":{"kind":"Variable","name":{"kind":"Name","value":"userId"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"assignees"},"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 RemoveTaskAssigneeDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"RemoveTaskAssignee"},"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":"userId"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"ID"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"removeTaskAssignee"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"id"},"value":{"kind":"Variable","name":{"kind":"Name","value":"id"}}},{"kind":"Argument","name":{"kind":"Name","value":"userId"},"value":{"kind":"Variable","name":{"kind":"Name","value":"userId"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"assignees"},"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 DeleteTaskDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"DeleteTask"},"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":"deleteTask"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"id"},"value":{"kind":"Variable","name":{"kind":"Name","value":"id"}}}]}]}}]} as unknown as DocumentNode; export const CompleteTaskDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"CompleteTask"},"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":"completeTask"},"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":"done"}},{"kind":"Field","name":{"kind":"Name","value":"updateDate"}}]}}]}}]} as unknown as DocumentNode; export const ReopenTaskDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"ReopenTask"},"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":"reopenTask"},"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":"done"}},{"kind":"Field","name":{"kind":"Name","value":"updateDate"}}]}}]}}]} as unknown as DocumentNode; diff --git a/web/api/graphql/GetMyTasks.graphql b/web/api/graphql/GetMyTasks.graphql index e9fd0262..fe7d66ce 100644 --- a/web/api/graphql/GetMyTasks.graphql +++ b/web/api/graphql/GetMyTasks.graphql @@ -36,7 +36,7 @@ query GetMyTasks { } } } - assignee { + assignees { id name avatarUrl diff --git a/web/api/graphql/GetOverviewData.graphql b/web/api/graphql/GetOverviewData.graphql index cacdfb15..41566ff6 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 @@ -58,7 +58,7 @@ query GetOverviewData($rootLocationIds: [ID!], $recentPatientsFiltering: [Filter dueDate updateDate priority - assignee { + assignees { id name avatarUrl @@ -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/GetPatient.graphql b/web/api/graphql/GetPatient.graphql index 1ec43314..36263f17 100644 --- a/web/api/graphql/GetPatient.graphql +++ b/web/api/graphql/GetPatient.graphql @@ -88,7 +88,7 @@ query GetPatient($id: ID!) { priority estimatedTime updateDate - assignee { + assignees { id name avatarUrl diff --git a/web/api/graphql/GetPatients.graphql b/web/api/graphql/GetPatients.graphql index 1875fa4d..810a89f0 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 @@ -109,7 +109,7 @@ query GetPatients($locationId: ID, $rootLocationIds: [ID!], $states: [PatientSta estimatedTime creationDate updateDate - assignee { + assignees { id name avatarUrl @@ -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/GetTask.graphql b/web/api/graphql/GetTask.graphql index 8c4c8e1f..322c7f54 100644 --- a/web/api/graphql/GetTask.graphql +++ b/web/api/graphql/GetTask.graphql @@ -13,7 +13,7 @@ query GetTask($id: ID!) { id name } - assignee { + assignees { id name avatarUrl diff --git a/web/api/graphql/GetTasks.graphql b/web/api/graphql/GetTasks.graphql index 7864f970..6014b38e 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 @@ -41,7 +41,7 @@ query GetTasks($rootLocationIds: [ID!], $assigneeId: ID, $assigneeTeamId: ID, $f } } } - assignee { + assignees { id name avatarUrl @@ -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 00000000..b5b78dbc --- /dev/null +++ b/web/api/graphql/QueryableFields.graphql @@ -0,0 +1,24 @@ +query QueryableFields($entity: String!) { + queryableFields(entity: $entity) { + key + label + kind + valueType + allowedOperators + sortable + sortDirections + searchable + filterable + propertyDefinitionId + relation { + targetEntity + idFieldKey + labelFieldKey + allowedFilterModes + } + choice { + optionKeys + optionLabels + } + } +} diff --git a/web/api/graphql/SavedView.graphql b/web/api/graphql/SavedView.graphql new file mode 100644 index 00000000..dea03946 --- /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/api/graphql/TaskMutations.graphql b/web/api/graphql/TaskMutations.graphql index 27bad5e6..07040d82 100644 --- a/web/api/graphql/TaskMutations.graphql +++ b/web/api/graphql/TaskMutations.graphql @@ -6,7 +6,7 @@ mutation CreateTask($data: CreateTaskInput!) { done dueDate updateDate - assignee { + assignees { id name avatarUrl @@ -35,7 +35,7 @@ mutation UpdateTask($id: ID!, $data: UpdateTaskInput!) { id name } - assignee { + assignees { id name avatarUrl @@ -77,10 +77,10 @@ mutation UpdateTask($id: ID!, $data: UpdateTaskInput!) { } } -mutation AssignTask($id: ID!, $userId: ID!) { - assignTask(id: $id, userId: $userId) { +mutation AddTaskAssignee($id: ID!, $userId: ID!) { + addTaskAssignee(id: $id, userId: $userId) { id - assignee { + assignees { id name avatarUrl @@ -90,10 +90,10 @@ mutation AssignTask($id: ID!, $userId: ID!) { } } -mutation UnassignTask($id: ID!) { - unassignTask(id: $id) { +mutation RemoveTaskAssignee($id: ID!, $userId: ID!) { + removeTaskAssignee(id: $id, userId: $userId) { id - assignee { + assignees { id name avatarUrl diff --git a/web/api/mutations/tasks/assignTask.plan.ts b/web/api/mutations/tasks/assignTask.plan.ts index b027984a..aba7986e 100644 --- a/web/api/mutations/tasks/assignTask.plan.ts +++ b/web/api/mutations/tasks/assignTask.plan.ts @@ -28,7 +28,13 @@ export const assignTaskOptimisticPlan: OptimisticPlan = { cache.modify({ id, fields: { - assignee: () => ({ __ref: `UserType:${userId}` }), + assignees: (existing: ReadonlyArray<{ __ref: string }> | undefined = []) => { + const ref = `UserType:${userId}` + if (existing.some((entry) => entry.__ref === ref)) { + return existing + } + return [...existing, { __ref: ref }] + }, assigneeTeam: () => null, }, }) diff --git a/web/api/mutations/tasks/assignTaskToTeam.plan.ts b/web/api/mutations/tasks/assignTaskToTeam.plan.ts index 877eb796..5c5d9020 100644 --- a/web/api/mutations/tasks/assignTaskToTeam.plan.ts +++ b/web/api/mutations/tasks/assignTaskToTeam.plan.ts @@ -28,7 +28,7 @@ export const assignTaskToTeamOptimisticPlan: OptimisticPlan null, + assignees: () => [], assigneeTeam: (_existing, { toReference }) => toReference({ __typename: 'LocationNodeType', id: teamId }, true), }, diff --git a/web/codegen.ts b/web/codegen.ts index 92774cfb..806fdff8 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/Notifications.tsx b/web/components/Notifications.tsx index bafcde54..cdb1f29f 100644 --- a/web/components/Notifications.tsx +++ b/web/components/Notifications.tsx @@ -78,7 +78,7 @@ export const Notifications = () => { const recentTasks = data?.recentTasks?.slice(0, 5) || [] recentTasks.forEach((task) => { - if (task.assignee?.id === user?.id) { + if (task.assignees.some((assignee) => assignee.id === user?.id)) { return } const id = `task-${task.id}` diff --git a/web/components/layout/Page.tsx b/web/components/layout/Page.tsx index 0f17f84f..4b0362b5 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, + Rabbit } 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,27 @@ 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} + + ))} + +
+ )} {(context?.teams?.length ?? 0) > 0 && ( {sexOptions.map(option => ( - - {option.label} - + ))} )} diff --git a/web/components/patients/PatientTasksView.tsx b/web/components/patients/PatientTasksView.tsx index 2ab446fb..bdd2e47a 100644 --- a/web/components/patients/PatientTasksView.tsx +++ b/web/components/patients/PatientTasksView.tsx @@ -110,7 +110,7 @@ export const PatientTasksView = ({ onClick={(t) => setTaskId(t.id)} onToggleDone={handleToggleDone} showPatient={false} - showAssignee={!!(task.assignee || task.assigneeTeam)} + showAssignee={!!((task.assignees?.length ?? 0) > 0 || task.assigneeTeam)} fullWidth={true} /> ))} @@ -136,7 +136,7 @@ export const PatientTasksView = ({ onClick={(t) => setTaskId(t.id)} onToggleDone={handleToggleDone} showPatient={false} - showAssignee={!!(task.assignee || task.assigneeTeam)} + showAssignee={!!((task.assignees?.length ?? 0) > 0 || task.assigneeTeam)} fullWidth={true} /> ))} diff --git a/web/components/properties/PropertyDetailView.tsx b/web/components/properties/PropertyDetailView.tsx index 54b34172..6977ffeb 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 5c2f6052..537d3382 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/LocationSubtreeFilterPopUp.tsx b/web/components/tables/LocationSubtreeFilterPopUp.tsx new file mode 100644 index 00000000..6ea99d84 --- /dev/null +++ b/web/components/tables/LocationSubtreeFilterPopUp.tsx @@ -0,0 +1,130 @@ +import { useTasksTranslation } from '@/i18n/useTasksTranslation' +import { Button, FilterBasePopUp, type FilterListPopUpBuilderProps } from '@helpwave/hightide' +import { useId, useMemo, useState } from 'react' +import { MapPin } from 'lucide-react' +import type { LocationNodeType } from '@/api/gql/generated' +import { LocationSelectionDialog } from '@/components/locations/LocationSelectionDialog' +import { useLocations } from '@/data' + +export const LocationSubtreeFilterPopUp = ({ value, onValueChange, onRemove, name }: FilterListPopUpBuilderProps) => { + const translation = useTasksTranslation() + const { data: locationsData } = useLocations() + const id = useId() + const [dialogOpen, setDialogOpen] = useState(false) + const operator = useMemo(() => { + const suggestion = value?.operator ?? 'equals' + return suggestion === 'contains' ? 'contains' : 'equals' + }, [value?.operator]) + + const uuidValue = value?.parameter?.uuidValue + const uuidValues = value?.parameter?.uuidValues + const isMulti = operator === 'contains' + + const initialSelectedIds = useMemo(() => { + if (isMulti) { + const v = uuidValues + return Array.isArray(v) ? v.map(String) : [] + } + const u = uuidValue + return u != null && String(u) !== '' ? [String(u)] : [] + }, [isMulti, uuidValue, uuidValues]) + + const handleLocationsSelected = (locations: LocationNodeType[]) => { + const baseParam = value?.parameter ?? {} + const ids = locations.map(l => l.id) + if (isMulti) { + onValueChange({ + ...value, + dataType: 'singleTag', + operator: 'contains', + parameter: { ...baseParam, uuidValue: undefined, uuidValues: ids }, + }) + } else { + const first = ids[0] + onValueChange({ + ...value, + dataType: 'singleTag', + operator: 'equals', + parameter: { ...baseParam, uuidValue: first, uuidValues: undefined }, + }) + } + setDialogOpen(false) + } + + const summary = useMemo(() => { + const nodes = locationsData?.locationNodes + if (isMulti) { + const n = (uuidValues as string[] | undefined)?.length ?? 0 + return n === 0 ? translation('selectLocation') : `${n} ${translation('location')}` + } + const uid = uuidValue != null && String(uuidValue) !== '' + ? String(uuidValue) + : undefined + if (!uid) { + return translation('selectLocation') + } + const node = nodes?.find(n => n.id === uid) + return node?.title ?? translation('selectLocation') + }, [isMulti, locationsData?.locationNodes, uuidValue, uuidValues, translation]) + + return ( + <> + { + const baseParam = value?.parameter ?? {} + const next = newOperator === 'contains' ? 'contains' : 'equals' + if (next === 'equals') { + const u = baseParam.uuidValues + const first = Array.isArray(u) && u.length > 0 ? String(u[0]) : undefined + onValueChange({ + dataType: 'singleTag', + parameter: { ...baseParam, uuidValue: first, uuidValues: undefined }, + operator: 'equals', + }) + } else { + const u = baseParam.uuidValue + onValueChange({ + dataType: 'singleTag', + parameter: { + ...baseParam, + uuidValue: undefined, + uuidValues: u != null && String(u) !== '' ? [String(u)] : [], + }, + operator: 'contains', + }) + } + }} + onRemove={onRemove} + allowedOperators={['equals', 'contains']} + noParameterRequired={false} + > +
+ +
+ +
+
+
+ setDialogOpen(false)} + onSelect={handleLocationsSelected} + initialSelectedIds={initialSelectedIds} + multiSelect={isMulti} + useCase="default" + /> + + ) +} diff --git a/web/components/tables/PatientList.tsx b/web/components/tables/PatientList.tsx index e1450d49..14bf2dfb 100644 --- a/web/components/tables/PatientList.tsx +++ b/web/components/tables/PatientList.tsx @@ -1,8 +1,11 @@ import { useMemo, useState, forwardRef, useImperativeHandle, useEffect, useCallback, useRef } from 'react' -import { Chip, FillerCell, HelpwaveLogo, LoadingContainer, SearchBar, ProgressIndicator, Tooltip, Drawer, TableProvider, TableDisplay, TableColumnSwitcher, TablePagination, IconButton, useLocale } from '@helpwave/hightide' +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' -import { Sex, PatientState, type GetPatientsQuery, type TaskType, PropertyEntity, type FullTextSearchInput, type LocationType } from '@/api/gql/generated' -import { usePropertyDefinitions, usePatientsPaginated, useRefreshingEntityIds } from '@/data' +import type { LocationType } from '@/api/gql/generated' +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' @@ -10,11 +13,37 @@ 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, Row, 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 { columnFiltersToFilterInput, paginationStateToPaginationInput, sortingStateToSortInput } from '@/utils/tableStateToApi' +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 { 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 { replaceSavedViewInMySavedViewsCache } from '@/utils/savedViewsCache' +import { useDeferredColumnOrderChange } from '@/hooks/useDeferredColumnOrderChange' +import { columnIdsFromColumnDefs, sanitizeColumnOrderForKnownColumns } from '@/utils/columnOrder' +import { + hasActiveLocationFilter, + normalizedColumnOrderForViewCompare, + normalizedVisibilityForViewCompare, + serializeColumnFiltersForView, + serializeSortingForView, + stringifyViewParameters, + tableViewStateMatchesBaseline +} from '@/utils/viewDefinition' +import type { ViewParameters } from '@/utils/viewDefinition' export type PatientViewModel = { id: string, @@ -51,38 +80,159 @@ type PatientListProps = { acceptedStates?: PatientState[], rootLocationIds?: string[], locationId?: 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 }, 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() 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) - const [searchQuery, setSearchQuery] = useState('') + const [searchQuery, setSearchQuery] = useState(viewDefaultSearchQuery ?? '') const [openedPatientId, setOpenedPatientId] = useState(null) + const [isShowFilters, setIsShowFilters] = useState(false) + const [isShowSorting, setIsShowSorting] = useState(false) - const { - pagination, - setPagination, - sorting, - setSorting, - filters, - setFilters, - columnVisibility, - setColumnVisibility, - } = useStorageSyncedTableState('patient-list') + 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 ?? []) - usePropertyColumnVisibility( + const setColumnVisibility = useColumnVisibilityWithPropertyDefaults( propertyDefinitionsData, PropertyEntity.Patient, - columnVisibility, - setColumnVisibility + 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 hasLocationFilter = useMemo( + () => hasActiveLocationFilter(filters), + [filters] ) + const propertyColumnIds = useMemo( + () => getPropertyColumnIds(propertyDefinitionsData, PropertyEntity.Patient), + [propertyDefinitionsData] + ) + + const persistedSavedViewContentKey = useMemo( + () => + `${serializeColumnFiltersForView(baselineFilters)}|${serializeSortingForView(baselineSorting)}|${baselineSearch}|${normalizedVisibilityForViewCompare(baselineColumnVisibility)}|${normalizedColumnOrderForViewCompare(baselineColumnOrder)}`, + [baselineFilters, baselineSorting, baselineSearch, baselineColumnVisibility, baselineColumnOrder] + ) + + useEffect(() => { + if (!savedViewId) { + return + } + setFilters(baselineFilters) + setSorting(baselineSorting) + setSearchQuery(baselineSearch) + setColumnVisibility(baselineColumnVisibility) + setColumnOrder(baselineColumnOrder) + setPagination({ pageSize: 10, pageIndex: 0 }) + }, [ + savedViewId, + persistedSavedViewContentKey, + baselineFilters, + baselineSorting, + baselineSearch, + baselineColumnVisibility, + baselineColumnOrder, + setColumnVisibility, + ]) + + const [isSaveViewDialogOpen, setIsSaveViewDialogOpen] = useState(false) + + const [updateSavedView, { loading: overwriteLoading }] = useMutation< + UpdateSavedViewMutation, + UpdateSavedViewMutationVariables + >(getParsedDocument(UpdateSavedViewDocument), { + awaitRefetchQueries: true, + refetchQueries: savedViewId + ? [ + { query: getParsedDocument(SavedViewDocument), variables: { id: savedViewId } }, + { query: getParsedDocument(MySavedViewsDocument) }, + ] + : [{ query: getParsedDocument(MySavedViewsDocument) }], + update(cache, { data }) { + const view = data?.updateSavedView + if (view) { + replaceSavedViewInMySavedViewsCache(cache, view) + } + }, + }) + + 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: hasLocationFilter ? undefined : (locationId ?? undefined), + searchQuery: searchQuery || undefined, + columnVisibility, + columnOrder, + } satisfies ViewParameters), + }, + }, + }) + }, [ + savedViewId, + updateSavedView, + filters, + sorting, + effectiveRootLocationIds, + hasLocationFilter, + locationId, + searchQuery, + columnVisibility, + columnOrder, + ]) + const allPatientStates: PatientState[] = useMemo(() => [ PatientState.Admitted, PatientState.Discharged, @@ -90,41 +240,44 @@ 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) const { data: patientsData, refetch, totalCount, loading: patientsLoading } = usePatientsPaginated( { - locationId: locationId || undefined, - rootLocationIds: !locationId && effectiveRootLocationIds && effectiveRootLocationIds.length > 0 ? effectiveRootLocationIds : undefined, + locationId: hasLocationFilter ? undefined : (locationId || undefined), + rootLocationIds: hasLocationFilter || locationId + ? undefined + : (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 @@ -190,7 +343,7 @@ export const PatientList = forwardRef(({ initi } const patientPropertyColumns = useMemo[]>( - () => getPropertyColumnsForEntity(propertyDefinitionsData, PropertyEntity.Patient), + () => getPropertyColumnsForEntity(propertyDefinitionsData, PropertyEntity.Patient, false), [propertyDefinitionsData] ) @@ -211,7 +364,6 @@ export const PatientList = forwardRef(({ initi minSize: 200, size: 250, maxSize: 300, - filterFn: 'text', }, { id: 'state', @@ -229,12 +381,6 @@ export const PatientList = forwardRef(({ initi minSize: 120, size: 144, maxSize: 180, - filterFn: 'singleTag', - meta: { - filterData: { - tags: allPatientStates.map(state => ({ label: translation('patientState', { state: state as string }), tag: state })), - } - } }, { id: 'sex', @@ -272,16 +418,6 @@ export const PatientList = forwardRef(({ initi minSize: 160, size: 160, maxSize: 200, - filterFn: 'singleTag', - meta: { - filterData: { - tags: [ - { label: translation('male'), tag: Sex.Male }, - { label: translation('female'), tag: Sex.Female }, - { label: translation('diverse'), tag: Sex.Unknown }, - ], - } - } }, { id: 'position', @@ -299,7 +435,6 @@ export const PatientList = forwardRef(({ initi minSize: 200, size: 260, maxSize: 320, - filterFn: 'text' as const, }, ...(['CLINIC', 'WARD', 'ROOM', 'BED'] as const).map((kind): ColumnDef => ({ id: `location-${kind}`, @@ -322,7 +457,6 @@ export const PatientList = forwardRef(({ initi minSize: 160, size: 220, maxSize: 300, - filterFn: 'text' as const, })), { id: 'birthdate', @@ -351,7 +485,6 @@ export const PatientList = forwardRef(({ initi minSize: 200, size: 200, maxSize: 200, - filterFn: 'date' as const, }, { id: 'tasks', @@ -393,7 +526,129 @@ export const PatientList = forwardRef(({ initi refreshingPatientIds.has(params.row.original.id) ? rowLoadingCell : (col.cell as (p: unknown) => React.ReactNode)(params) : undefined, })), - ], [translation, allPatientStates, patientPropertyColumns, refreshingPatientIds, rowLoadingCell, dateFormat]) + ], [translation, patientPropertyColumns, refreshingPatientIds, rowLoadingCell, dateFormat]) + + 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 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 knownColumnIdsOrdered = useMemo( + () => columnIdsFromColumnDefs(columns), + [columns] + ) + + const sanitizedColumnOrder = useMemo( + () => sanitizeColumnOrderForKnownColumns(columnOrder, knownColumnIdsOrdered), + [columnOrder, knownColumnIdsOrdered] + ) + + const sanitizedBaselineColumnOrder = useMemo( + () => sanitizeColumnOrderForKnownColumns(baselineColumnOrder, knownColumnIdsOrdered), + [baselineColumnOrder, knownColumnIdsOrdered] + ) + + const viewMatchesBaseline = useMemo( + () => tableViewStateMatchesBaseline({ + filters: filters as ColumnFiltersState, + baselineFilters, + sorting, + baselineSorting, + searchQuery, + baselineSearch, + columnVisibility, + baselineColumnVisibility, + columnOrder: sanitizedColumnOrder, + baselineColumnOrder: sanitizedBaselineColumnOrder, + propertyColumnIds, + }), + [ + filters, + baselineFilters, + sorting, + baselineSorting, + searchQuery, + baselineSearch, + columnVisibility, + baselineColumnVisibility, + sanitizedColumnOrder, + sanitizedBaselineColumnOrder, + propertyColumnIds, + ] + ) + const hasUnsavedViewChanges = !viewMatchesBaseline + + const deferSetColumnOrder = useDeferredColumnOrderChange(setColumnOrder) const onRowClick = useCallback((row: Row) => handleEdit(row.original), [handleEdit]) const fillerRowCell = useCallback(() => (), []) @@ -404,7 +659,7 @@ export const PatientList = forwardRef(({ initi columns={columns} fillerRowCell={fillerRowCell} onRowClick={onRowClick} - manualPagination={true} + initialState={{ pagination: { pageSize: 10, @@ -412,29 +667,64 @@ export const PatientList = forwardRef(({ initi }} state={{ columnVisibility, + columnOrder: sanitizedColumnOrder, pagination, - sorting, - columnFilters: filters, } as Partial as TableState} onColumnVisibilityChange={setColumnVisibility} + onColumnOrderChange={deferSetColumnOrder} onPaginationChange={setPagination} onSortingChange={setSorting} onColumnFiltersChange={setFilters} enableMultiSort={true} + enablePinning={false} 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} - /> - -
-
+
+
+
+ setSearchQuery(e.target.value)} + onSearch={() => null} + containerProps={{ className: 'max-w-80' }} + /> + + + + + setIsSaveViewDialogOpen(true)} + onDiscard={handleDiscardViewChanges} + /> + +
{ @@ -446,6 +736,20 @@ export const PatientList = forwardRef(({ initi
+ {isShowFilters && ( + + )} + {isShowSorting && ( + + )}
{patientsLoading && ( @@ -475,6 +779,22 @@ export const PatientList = forwardRef(({ initi onSuccess={refetch} /> + setIsSaveViewDialogOpen(false)} + baseEntityType={SavedViewEntityType.Patient} + filterDefinition={serializeColumnFiltersForView(filters as ColumnFiltersState)} + sortDefinition={serializeSortingForView(sorting)} + parameters={stringifyViewParameters({ + rootLocationIds: effectiveRootLocationIds ?? undefined, + locationId: hasLocationFilter ? undefined : (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 53140c18..d55ba24e 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 cab08163..2a110437 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 3e6554e1..a1284686 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, 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' 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' @@ -15,13 +17,15 @@ 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 { useDeferredColumnOrderChange } from '@/hooks/useDeferredColumnOrderChange' +import { columnIdsFromColumnDefs, sanitizeColumnOrderForKnownColumns } from '@/utils/columnOrder' 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 = { id: string, @@ -65,6 +69,8 @@ type TaskListTableState = { setFilters: Dispatch>, columnVisibility: VisibilityState, setColumnVisibility: Dispatch>, + columnOrder: ColumnOrderState, + setColumnOrder: Dispatch>, } type TaskListProps = { @@ -74,74 +80,49 @@ 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 }, 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') - 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 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() @@ -154,12 +135,16 @@ 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) const [isConfirmDialogOpen, setIsConfirmDialogOpen] = useState(false) const isOpeningConfirmDialogRef = useRef(false) + const [isShowFilters, setIsShowFilters] = useState(false) + const [isShowSorting, setIsShowSorting] = useState(false) const hasPatients = (totalPatientsCount ?? 0) > 0 @@ -201,6 +186,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 +197,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 +320,44 @@ 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: 'dueDate', label: translation('dueDate'), dataType: 'date', tags: [] }, + { + 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 => ({ + 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 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(() => , []) @@ -339,14 +367,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 @@ -542,12 +562,23 @@ export const TaskList = forwardRef(({ tasks: initial }, [translation, completeTask, reopenTask, showAssignee, optimisticUpdates, taskPropertyColumns, refreshingTaskIds, rowLoadingCell, onRefetch]) + const knownColumnIdsOrdered = useMemo( + () => columnIdsFromColumnDefs(columns), + [columns] + ) + + const sanitizedColumnOrder = useMemo( + () => sanitizeColumnOrderForKnownColumns(columnOrder, knownColumnIdsOrdered), + [columnOrder, knownColumnIdsOrdered] + ) + + const deferSetColumnOrder = useDeferredColumnOrderChange(setColumnOrder) + return ( (), [])} - manualPagination={true} initialState={{ pagination: { pageSize: 10, @@ -555,60 +586,90 @@ export const TaskList = forwardRef(({ tasks: initial }} state={{ columnVisibility, + columnOrder: sanitizedColumnOrder, pagination, - sorting, - columnFilters: showAllTasksMode ? filters.filter(f => f.id !== 'done') : filters, } as Partial as TableState} - onColumnVisibilityChange={setColumnVisibility} + onColumnVisibilityChange={setColumnVisibilityMerged} + onColumnOrderChange={deferSetColumnOrder} 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' }} - /> - -
-
- - {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/tables/UserSelectFilterPopUp.tsx b/web/components/tables/UserSelectFilterPopUp.tsx new file mode 100644 index 00000000..c033491b --- /dev/null +++ b/web/components/tables/UserSelectFilterPopUp.tsx @@ -0,0 +1,48 @@ +import { useTasksTranslation } from '@/i18n/useTasksTranslation' +import { FilterBasePopUp, FilterOperatorUtils, Visibility, type FilterListPopUpBuilderProps } from '@helpwave/hightide' +import { useId, useMemo } from 'react' +import { AssigneeSelect } from '../tasks/AssigneeSelect' + + +export const UserSelectFilterPopUp = ({ value, onValueChange, onRemove, name }: FilterListPopUpBuilderProps) => { + const translation = useTasksTranslation() + const id = useId() + const ids = { + select: `user-select-filter-${id}`, + } + + const operator = useMemo(() => { + const suggestion = value?.operator ?? 'contains' + if (!FilterOperatorUtils.typeCheck.tagsSingle(suggestion)) { + return 'contains' + } + return suggestion + }, [value]) + + const parameter = value?.parameter ?? {} + + const needsParameterInput = operator !== 'isUndefined' && operator !== 'isNotUndefined' + + return ( + onValueChange({ dataType: 'singleTag', parameter, operator: newOperator })} + onRemove={onRemove} + allowedOperators={FilterOperatorUtils.operatorsByCategory.singleTag} + noParameterRequired={!needsParameterInput} + > + +
+ + onValueChange({ ...value, parameter: { ...parameter, uuidValue: newUserValue } })} + onDialogClose={(newUserValue) => onValueChange({ ...value, parameter: { ...parameter, uuidValue: newUserValue } })} + onValueClear={() => onValueChange({ ...value, parameter: { ...parameter, uuidValue: undefined } })} + /> +
+
+
+ ) +} \ No newline at end of file diff --git a/web/components/tasks/AssigneeSelect.tsx b/web/components/tasks/AssigneeSelect.tsx index 56d21981..ac632893 100644 --- a/web/components/tasks/AssigneeSelect.tsx +++ b/web/components/tasks/AssigneeSelect.tsx @@ -16,6 +16,8 @@ interface AssigneeSelectProps { allowTeams?: boolean, allowUnassigned?: boolean, excludeUserIds?: string[], + multiUserSelect?: boolean, + onMultiUserIdsSelected?: (userIds: string[]) => void, id?: string, className?: string, [key: string]: unknown, @@ -29,6 +31,8 @@ export const AssigneeSelect = ({ allowTeams = true, allowUnassigned: _allowUnassigned = false, excludeUserIds = [], + multiUserSelect = false, + onMultiUserIdsSelected, id, className, }: AssigneeSelectProps) => { @@ -162,6 +166,8 @@ export const AssigneeSelect = ({ isOpen={isOpen} onClose={handleClose} onUserInfoClick={(userId) => setSelectedUserPopupState({ isOpen: true, userId })} + multiUserSelect={multiUserSelect} + onMultiUserIdsSelected={onMultiUserIdsSelected} /> void, dialogTitle?: string, onUserInfoClick?: (userId: string) => void, + multiUserSelect?: boolean, + onMultiUserIdsSelected?: (userIds: string[]) => void, } export const AssigneeSelectDialog = ({ @@ -28,9 +30,12 @@ export const AssigneeSelectDialog = ({ onClose, dialogTitle, onUserInfoClick, + multiUserSelect = false, + onMultiUserIdsSelected, }: AssigneeSelectDialogProps) => { const translation = useTasksTranslation() const [searchQuery, setSearchQuery] = useState('') + const [pendingUserIds, setPendingUserIds] = useState>(new Set()) const searchInputRef = useRef(null) const { data: usersData } = useUsers() @@ -88,7 +93,10 @@ export const AssigneeSelectDialog = ({ } else { setSearchQuery('') } - }, [isOpen]) + if (isOpen && multiUserSelect) { + setPendingUserIds(new Set()) + } + }, [isOpen, multiUserSelect]) const handleSelect = (selectedValue: string) => { onValueChanged(selectedValue) @@ -96,8 +104,30 @@ export const AssigneeSelectDialog = ({ onClose() } + const togglePendingUser = (userId: string) => { + setPendingUserIds(prev => { + const next = new Set(prev) + if (next.has(userId)) { + next.delete(userId) + } else { + next.add(userId) + } + return next + }) + } + + const handleApplyMultiUsers = () => { + if (onMultiUserIdsSelected) { + onMultiUserIdsSelected([...pendingUserIds]) + } + setSearchQuery('') + setPendingUserIds(new Set()) + onClose() + } + const handleClose = () => { setSearchQuery('') + setPendingUserIds(new Set()) onClose() } @@ -129,12 +159,19 @@ export const AssigneeSelectDialog = ({ key={u.id} className={clsx( 'w-full px-3 py-2 hover:bg-surface-hover transition-colors flex items-center gap-2 bg-surface', - value === u.id && 'bg-surface-selected' + !multiUserSelect && value === u.id && 'bg-surface-selected', + multiUserSelect && pendingUserIds.has(u.id) && 'bg-surface-selected' )} > + {multiUserSelect && ( + togglePendingUser(u.id)} + /> + )}
+ {multiUserSelect && onMultiUserIdsSelected && ( +
+ + +
+ )}
) diff --git a/web/components/tasks/TaskDataEditor.tsx b/web/components/tasks/TaskDataEditor.tsx index a6579059..33956537 100644 --- a/web/components/tasks/TaskDataEditor.tsx +++ b/web/components/tasks/TaskDataEditor.tsx @@ -2,12 +2,11 @@ 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, Checkbox, - DateTimeInput, FormProvider, FormField, Input, @@ -19,7 +18,8 @@ import { Drawer, useFormObserverKey, Visibility, - FormObserver + FormObserver, + FlexibleDateTimeInput } from '@helpwave/hightide' import { CenteredLoadingLogo } from '@/components/CenteredLoadingLogo' import { useTasksContext } from '@/hooks/useTasksContext' @@ -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, @@ -86,7 +85,7 @@ export const TaskDataEditor = ({ message: err instanceof Error ? err.message : 'Update failed', }) }, - }).catch(() => {}) + }).catch(() => { }) } const [deleteTask, { loading: isDeleting }] = useDeleteTask() @@ -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,19 +265,16 @@ export const TaskDataEditor = ({ name="patientId" label={translation('patient')} - required - showRequiredIndicator={!isEditMode} > {({ dataProps, focusableElementProps, interactionStates }) => { return (!isEditMode) ? ( @@ -286,50 +285,81 @@ export const TaskDataEditor = ({ onClick={() => setIsShowingPatientDialog(true)} className="w-fit" > - - { taskData?.patient?.name} + + {taskData?.patient?.name || translation('none') || 'None'}
) }} - - name="assigneeId" + + name="assigneeIds" label={translation('assignedTo')} > - {({ dataProps }) => ( - { - updateForm(prev => { - if (value.startsWith('team:')) { - return { ...prev, assigneeId: null, assigneeTeamId: value.replace('team:', '') } - } - return { ...prev, assigneeId: value || null, assigneeTeamId: null } - }) - if (isEditMode && taskId) { - if (!value || value === '') { - unassignTask({ - variables: { id: taskId }, - onCompleted: () => onSuccess?.(), - }).catch(() => {}) - } else if (value.startsWith('team:')) { - assignTaskToTeam({ - variables: { id: taskId, teamId: value.replace('team:', '') }, - onCompleted: () => onSuccess?.(), - }).catch(() => {}) - } else { - assignTask({ - variables: { id: taskId, userId: value }, - onCompleted: () => onSuccess?.(), - }).catch(() => {}) - } - } - }} - allowTeams={true} - allowUnassigned={true} - /> + {() => ( +
+ { + updateForm(prev => { + if (!value) { + return { ...prev, assigneeIds: [], assigneeTeamId: null } + } + if (value.startsWith('team:')) { + return { ...prev, assigneeIds: [], assigneeTeamId: value.replace('team:', '') } + } + const currentAssigneeIds = prev.assigneeIds ?? [] + if (currentAssigneeIds.includes(value)) { + return prev + } + return { ...prev, assigneeIds: [...currentAssigneeIds, value], assigneeTeamId: null } + }) + }} + multiUserSelect={true} + onMultiUserIdsSelected={(ids) => { + if (ids.length === 0) return + updateForm(prev => ({ + ...prev, + assigneeIds: [...new Set([...(prev.assigneeIds ?? []), ...ids])], + assigneeTeamId: null, + })) + }} + allowTeams={true} + allowUnassigned={true} + excludeUserIds={assigneeIds ?? []} + /> + {(selectedAssignees.length > 0 || assigneeTeamId) && ( +
+ {selectedAssignees.map((assignee) => ( + + ))} + {assigneeTeamId && ( + + )} +
+ )} +
)} @@ -338,10 +368,10 @@ export const TaskDataEditor = ({ label={translation('dueDate')} > {({ dataProps, focusableElementProps, interactionStates }) => ( - )} @@ -362,9 +392,9 @@ export const TaskDataEditor = ({ dataProps.onEditComplete?.(priority) }} > - {translation('priorityNone')} + {priorities.map(({ value, label }) => ( - +
{label} @@ -471,7 +501,7 @@ export const TaskDataEditor = ({ setIsShowingPatientDialog(false)} - onSuccess={() => {}} + onSuccess={() => { }} /> diff --git a/web/components/views/PatientViewTasksPanel.tsx b/web/components/views/PatientViewTasksPanel.tsx new file mode 100644 index 00000000..28cd7d48 --- /dev/null +++ b/web/components/views/PatientViewTasksPanel.tsx @@ -0,0 +1,116 @@ +'use client' + +import { useMemo } from 'react' +import { usePatients } from '@/data' +import { PatientState } from '@/api/gql/generated' +import type { QuerySearchInput } from '@/api/gql/generated' +import { columnFiltersToQueryFilterClauses, sortingStateToQuerySortClauses } from '@/utils/tableStateToApi' +import { + deserializeColumnFiltersFromView, + deserializeSortingFromView, + hasActiveLocationFilter +} 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, +} + +export function PatientViewTasksPanel({ + filterDefinitionJson, + sortDefinitionJson, + parameters, +}: PatientViewTasksPanelProps) { + const filters = deserializeColumnFiltersFromView(filterDefinitionJson) + const sorting = deserializeSortingFromView(sortDefinitionJson) + const apiFilters = useMemo(() => columnFiltersToQueryFilterClauses(filters), [filters]) + const apiSorting = useMemo(() => sortingStateToQuerySortClauses(sorting), [sorting]) + const hasLocationFilter = useMemo( + () => hasActiveLocationFilter(filters), + [filters] + ) + + const allPatientStates: PatientState[] = useMemo(() => [ + PatientState.Admitted, + PatientState.Discharged, + PatientState.Dead, + PatientState.Wait, + ], []) + + const patientStates = useMemo(() => { + 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 = raw.filter(s => allowed.has(s)) + return filtered.length > 0 ? (filtered as PatientState[]) : allPatientStates + }, [apiFilters, allPatientStates]) + + const searchInput: QuerySearchInput | undefined = parameters.searchQuery + ? { searchText: parameters.searchQuery, includeProperties: true } + : undefined + + const { data: patientsData, loading, refetch } = usePatients({ + locationId: hasLocationFilter ? undefined : parameters.locationId, + rootLocationIds: hasLocationFilter || parameters.locationId + ? undefined + : (parameters.rootLocationIds && parameters.rootLocationIds.length > 0 ? parameters.rootLocationIds : undefined), + states: patientStates, + filters: apiFilters.length > 0 ? apiFilters : undefined, + sorts: 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.assignees[0] + ? { id: task.assignees[0].id, name: task.assignees[0].name, avatarURL: task.assignees[0].avatarUrl, isOnline: task.assignees[0].isOnline ?? null } + : undefined, + assigneeTeam: task.assigneeTeam + ? { id: task.assigneeTeam.id, title: task.assigneeTeam.title } + : undefined, + })) + }) + }, [patientsData]) + + return ( + + ) +} diff --git a/web/components/views/SaveViewActionsMenu.tsx b/web/components/views/SaveViewActionsMenu.tsx new file mode 100644 index 00000000..124c70aa --- /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 new file mode 100644 index 00000000..6499f934 --- /dev/null +++ b/web/components/views/SaveViewDialog.tsx @@ -0,0 +1,114 @@ +'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, + MySavedViewsDocument, + type CreateSavedViewMutation, + type CreateSavedViewMutationVariables, + SavedViewVisibility +} from '@/api/gql/generated' +import { getParsedDocument } from '@/data/hooks/queryHelpers' +import { useTasksTranslation } from '@/i18n/useTasksTranslation' +import { appendSavedViewToMySavedViewsCache } from '@/utils/savedViewsCache' + +type SaveViewDialogProps = { + isOpen: boolean, + onClose: () => void, + /** Entity this view is saved from */ + baseEntityType: SavedViewEntityType, + filterDefinition: string, + sortDefinition: string, + parameters: string, + presentation?: 'default' | 'fromSystemList', + /** Optional: navigate or toast after save */ + onCreated?: (id: string) => void, +} + +export function SaveViewDialog({ + isOpen, + onClose, + baseEntityType, + filterDefinition, + sortDefinition, + parameters, + presentation = 'default', + onCreated, +}: SaveViewDialogProps) { + const translation = useTasksTranslation() + const [name, setName] = useState('') + + const handleClose = useCallback(() => { + onClose() + setName('') + }, [onClose]) + + const [createSavedView, { loading }] = useMutation< + CreateSavedViewMutation, + CreateSavedViewMutationVariables + >(getParsedDocument(CreateSavedViewDocument), { + refetchQueries: [{ query: getParsedDocument(MySavedViewsDocument) }], + awaitRefetchQueries: true, + update(cache, { data }) { + const view = data?.createSavedView + if (view) { + appendSavedViewToMySavedViewsCache(cache, view) + } + }, + onCompleted(data) { + onCreated?.(data?.createSavedView?.id) + handleClose() + }, + }) + + return ( + +
+
+ + setName(e.target.value)} + /> +
+
+ + +
+
+
+ ) +} diff --git a/web/components/views/SavedViewEntityTypeChip.tsx b/web/components/views/SavedViewEntityTypeChip.tsx new file mode 100644 index 00000000..8b6354f5 --- /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/components/views/TaskViewPatientsPanel.tsx b/web/components/views/TaskViewPatientsPanel.tsx new file mode 100644 index 00000000..43da33f0 --- /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 { columnFiltersToQueryFilterClauses, sortingStateToQuerySortClauses } 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 apiFilters = useMemo(() => columnFiltersToQueryFilterClauses(filters), [filters]) + const apiSorting = useMemo(() => sortingStateToQuerySortClauses(sorting), [sorting]) + + const { data, loading } = useTasks( + { + rootLocationIds: parameters.rootLocationIds, + assigneeId: parameters.assigneeId, + filters: apiFilters.length > 0 ? apiFilters : undefined, + sorts: 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/cache/policies.ts b/web/data/cache/policies.ts index b0025b9c..7d179080 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 c0db2b5f..3db935fc 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' @@ -36,3 +37,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/queryHelpers.ts b/web/data/hooks/queryHelpers.ts index dee33a5c..346409a9 100644 --- a/web/data/hooks/queryHelpers.ts +++ b/web/data/hooks/queryHelpers.ts @@ -2,7 +2,6 @@ import { useMemo } from 'react' import { useQuery } from '@apollo/client/react' import type { DocumentNode } from 'graphql' import { parse } from 'graphql' -import { useApolloClientOptional } from '@/providers/ApolloProviderWithData' const parsedCache = new Map() @@ -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/useAssignTask.ts b/web/data/hooks/useAssignTask.ts index 0651f02a..23a3b5c3 100644 --- a/web/data/hooks/useAssignTask.ts +++ b/web/data/hooks/useAssignTask.ts @@ -1,8 +1,8 @@ import { useCallback, useState } from 'react' import { - AssignTaskDocument, - type AssignTaskMutation, - type AssignTaskMutationVariables + AddTaskAssigneeDocument, + type AddTaskAssigneeMutation, + type AddTaskAssigneeMutationVariables } from '@/api/gql/generated' import { assignTaskOptimisticPlan, @@ -13,8 +13,8 @@ import { useMutateOptimistic } from '@/hooks/useMutateOptimistic' import { useConflictOnConflict } from '@/providers/ConflictProvider' type MutateOptions = { - variables: AssignTaskMutationVariables, - onCompleted?: (data: AssignTaskMutation['assignTask']) => void, + variables: AddTaskAssigneeMutationVariables, + onCompleted?: (data: AddTaskAssigneeMutation['addTaskAssignee']) => void, onError?: (error: Error) => void, } @@ -25,23 +25,23 @@ export function useAssignTask() { const [error, setError] = useState(null) const mutate = useCallback( - async (options: MutateOptions): Promise => { + async (options: MutateOptions): Promise => { setError(null) setLoading(true) try { - const data = await mutateOptimisticFn({ - document: getParsedDocument(AssignTaskDocument), + const data = await mutateOptimisticFn({ + document: getParsedDocument(AddTaskAssigneeDocument), variables: options.variables, optimisticPlan: assignTaskOptimisticPlan, optimisticPlanKey: assignTaskOptimisticPlanKey, - onSuccess: (d) => options.onCompleted?.(d.assignTask), + onSuccess: (d) => options.onCompleted?.(d.addTaskAssignee), onError: (err) => { setError(err) options.onError?.(err) }, onConflict: onConflict ?? undefined, }) - return data?.assignTask + return data?.addTaskAssignee } catch (e) { const err = e instanceof Error ? e : new Error(String(e)) setError(err) diff --git a/web/data/hooks/usePaginatedEntityQuery.ts b/web/data/hooks/usePaginatedEntityQuery.ts index 98bf6695..bbdb1482 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 210258d9..22588660 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 00000000..78484080 --- /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/useSavedViews.ts b/web/data/hooks/useSavedViews.ts new file mode 100644 index 00000000..58bda130 --- /dev/null +++ b/web/data/hooks/useSavedViews.ts @@ -0,0 +1,33 @@ +import { + MySavedViewsDocument, + SavedViewDocument, + type MySavedViewsQuery, + type MySavedViewsQueryVariables, + type SavedViewQuery, + type SavedViewQueryVariables +} from '@/api/gql/generated' +import { useQueryWhenReady } from './queryHelpers' + +type MySavedViewsHookOptions = { + skip?: boolean, + fetchPolicy?: 'cache-first' | 'cache-and-network' | 'network-only', +} + +export function useMySavedViews(options?: MySavedViewsHookOptions) { + return useQueryWhenReady( + MySavedViewsDocument, + {}, + { + skip: options?.skip, + fetchPolicy: options?.fetchPolicy ?? 'cache-and-network', + } + ) +} + +export function useSavedView(id: string | undefined, options?: { skip?: boolean }) { + return useQueryWhenReady( + SavedViewDocument, + { id: id ?? '' }, + { skip: options?.skip ?? !id } + ) +} diff --git a/web/data/hooks/useTasksPaginated.ts b/web/data/hooks/useTasksPaginated.ts index 2b5c853d..5fa1db98 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/hooks/useUnassignTask.ts b/web/data/hooks/useUnassignTask.ts index 2135680a..17a074b8 100644 --- a/web/data/hooks/useUnassignTask.ts +++ b/web/data/hooks/useUnassignTask.ts @@ -1,15 +1,15 @@ import { useMutation } from '@apollo/client/react' import { - UnassignTaskDocument, - type UnassignTaskMutation, - type UnassignTaskMutationVariables + RemoveTaskAssigneeDocument, + type RemoveTaskAssigneeMutation, + type RemoveTaskAssigneeMutationVariables } from '@/api/gql/generated' import { getParsedDocument } from './queryHelpers' export function useUnassignTask() { const [mutate, result] = useMutation< - UnassignTaskMutation, - UnassignTaskMutationVariables - >(getParsedDocument(UnassignTaskDocument)) + RemoveTaskAssigneeMutation, + RemoveTaskAssigneeMutationVariables + >(getParsedDocument(RemoveTaskAssigneeDocument)) return [mutate, result] as const } diff --git a/web/data/index.ts b/web/data/index.ts index 6ce1ae9b..97a1701d 100644 --- a/web/data/index.ts +++ b/web/data/index.ts @@ -50,6 +50,7 @@ export { useMyTasks, useTasksPaginated, usePatientsPaginated, + useQueryableFields, useCreateTask, useUpdateTask, useDeleteTask, @@ -70,6 +71,8 @@ export { useUpdatePropertyDefinition, useDeletePropertyDefinition, useUpdateProfilePicture, + useMySavedViews, + useSavedView, } from './hooks' export type { ClientMutationId, diff --git a/web/globals.css b/web/globals.css index f12e4b41..edd35c08 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/useDeferredColumnOrderChange.ts b/web/hooks/useDeferredColumnOrderChange.ts new file mode 100644 index 00000000..78a64268 --- /dev/null +++ b/web/hooks/useDeferredColumnOrderChange.ts @@ -0,0 +1,46 @@ +import type { Dispatch, SetStateAction } from 'react' +import { useCallback, useEffect, useRef, startTransition } from 'react' +import type { ColumnOrderState } from '@tanstack/table-core' + +export function useDeferredColumnOrderChange( + setColumnOrder: Dispatch> +): Dispatch> { + const mountedRef = useRef(false) + const pendingRef = useRef[]>([]) + + useEffect(() => { + mountedRef.current = true + const pending = pendingRef.current + pendingRef.current = [] + for (const u of pending) { + startTransition(() => { + setColumnOrder(u) + }) + } + return () => { + mountedRef.current = false + pendingRef.current = [] + } + }, [setColumnOrder]) + + return useCallback( + (updater: SetStateAction) => { + if (typeof window === 'undefined') { + return + } + if (!mountedRef.current) { + pendingRef.current.push(updater) + return + } + window.setTimeout(() => { + if (!mountedRef.current) { + return + } + startTransition(() => { + setColumnOrder(updater) + }) + }, 0) + }, + [setColumnOrder] + ) +} diff --git a/web/hooks/usePropertyColumnVisibility.ts b/web/hooks/usePropertyColumnVisibility.ts index 09d80f5f..27a3555d 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 9acb60d4..5046d44e 100644 --- a/web/hooks/useTableState.ts +++ b/web/hooks/useTableState.ts @@ -1,12 +1,11 @@ import type { ColumnFiltersState, + ColumnOrderState, PaginationState, SortingState, VisibilityState } from '@tanstack/react-table' -import type { Dispatch, SetStateAction } from 'react' -import type { DateFilterParameter, DatetimeFilterParameter, TableFilterValue } from '@helpwave/hightide' -import { useStorage } from '@/hooks/useStorage' +import { useCallback, useMemo, useState, type Dispatch, type SetStateAction } from 'react' const defaultPagination: PaginationState = { pageSize: 10, @@ -16,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 = { @@ -33,98 +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 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, - } - } - 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) => { - 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, - } - } - 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, - } - } - return { - ...filter, - value: { - ...filterValue, - parameter, - }, - } - }) as unknown as ColumnFiltersState - }, - }) - 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, @@ -135,5 +94,7 @@ export function useStorageSyncedTableState( setFilters, columnVisibility, setColumnVisibility, + columnOrder, + setColumnOrder, } } diff --git a/web/i18n/translations.ts b/web/i18n/translations.ts index f2ec590f..05dcf33d 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, @@ -33,6 +35,7 @@ export type TasksTranslationEntries = { 'closedTasks': string, 'collapseAll': string, 'confirm': string, + 'confirmDeleteView': string, 'confirmSelection': string, 'confirmShiftHandover': string, 'confirmShiftHandoverDescription': string, @@ -41,6 +44,8 @@ export type TasksTranslationEntries = { 'connectionConnected': string, 'connectionConnecting': string, 'connectionDisconnected': string, + 'copyShareLink': string, + 'copyViewToMyViews': string, 'create': string, 'createTask': string, 'currentTime': string, @@ -58,6 +63,7 @@ export type TasksTranslationEntries = { 'descriptionPlaceholder': string, 'deselectAll': string, 'developmentAndPreviewInstance': string, + 'discardViewChanges': string, 'dischargePatient': string, 'dischargePatientConfirmation': string, 'dismiss': string, @@ -65,6 +71,7 @@ export type TasksTranslationEntries = { 'diverse': string, 'done': string, 'dueDate': string, + 'duplicate': string, 'edit': string, 'editPatient': string, 'editTask': string, @@ -76,6 +83,7 @@ export type TasksTranslationEntries = { 'feedback': string, 'feedbackDescription': string, 'female': string, + 'filter': string, 'filterAll': string, 'filterUndone': string, 'firstName': string, @@ -107,22 +115,27 @@ export type TasksTranslationEntries = { 'myOpenTasks': string, 'myTasks': string, 'name': string, + 'nFilter': (values: { count: number }) => string, 'no': string, 'noClosedTasks': string, 'noLocationsFound': string, + 'none': string, 'noNotifications': string, 'noOpenTasks': string, 'noPatient': string, + 'noPatientsInTaskView': string, 'noResultsFound': string, 'notAssigned': string, 'notifications': string, 'nPatient': (values: { count: number }) => string, + 'nSorting': (values: { count: number }) => string, 'nTask': (values: { count: number }) => string, 'nYears': (values: { years: number }) => string, 'occupancy': string, 'ok': string, 'openSurvey': string, 'openTasks': string, + 'openView': string, 'option': string, 'organizations': string, 'overview': string, @@ -149,9 +162,11 @@ export type TasksTranslationEntries = { 'priorityNone': string, 'privacy': string, 'properties': string, + 'propertiesSettingsDescription': string, 'property': string, 'pThemes': (values: { count: number }) => string, 'rAdd': (values: { name: string }) => string, + 'readOnlyView': string, 'recentPatients': string, 'recentTasks': string, 'rEdit': (values: { name: string }) => string, @@ -160,6 +175,12 @@ export type TasksTranslationEntries = { 'removePropertyConfirmation': string, 'retakeSurvey': string, 'rShow': (values: { name: string }) => string, + 'savedViews': string, + 'saveView': string, + 'saveViewAsNew': string, + 'saveViewDescription': string, + 'saveViewDescriptionFromSystemList': string, + 'saveViewOverwriteCurrent': string, 'search': string, 'searchLocations': string, 'searchUserOrTeam': string, @@ -181,6 +202,7 @@ export type TasksTranslationEntries = { 'shiftHandover': string, 'showAllTasks': string, 'showTeamTasks': string, + 'sorting': string, 'sPropertySubjectType': (values: { subject: string }) => string, 'sPropertyType': (values: { type: string }) => string, 'stagingModalDisclaimerMarkdown': string, @@ -205,8 +227,18 @@ export type TasksTranslationEntries = { 'type': string, 'updated': string, 'url': string, + 'user': string, '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, @@ -218,6 +250,8 @@ export const tasksTranslation: Translation { + return TranslationGen.resolvePlural(count, { + '=1': `${count} Filter`, + 'other': `${count} Filter`, + }) + }, 'no': `Nein`, 'noClosedTasks': `Keine erledigten Aufgaben`, 'noLocationsFound': `Keine Standorte gefunden`, + 'none': `Keine`, 'noNotifications': `Keine aktuellen Updates`, 'noOpenTasks': `Keine offenen Aufgaben`, 'noPatient': `Kein Patient`, + 'noPatientsInTaskView': `Keine Patienten für diesen Aufgaben-Schnellzugriff.`, 'noResultsFound': `Keine Ergebnisse gefunden`, 'notAssigned': `Nicht zugewiesen`, 'notifications': `Benachrichtigungen`, @@ -355,6 +403,12 @@ export const tasksTranslation: Translation { + return TranslationGen.resolvePlural(count, { + '=1': `${count} Sortierung`, + 'other': `${count} Sortierungen`, + }) + }, 'nTask': ({ count }): string => { return TranslationGen.resolvePlural(count, { '=1': `${count} Aufgabe`, @@ -372,6 +426,7 @@ export const tasksTranslation: Translation { return TranslationGen.resolvePlural(count, { @@ -424,6 +480,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 => { @@ -436,6 +493,12 @@ export const tasksTranslation: Translation { return `${name} anzeigen` }, + '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...`, @@ -457,6 +520,7 @@ export const tasksTranslation: Translation { return TranslationGen.resolveSelect(subject, { 'patient': `Patient`, @@ -508,8 +572,18 @@ export const tasksTranslation: Translation { + return TranslationGen.resolvePlural(count, { + '=1': `${count} filter`, + 'other': `${count} filters`, + }) + }, 'no': `No`, 'noClosedTasks': `No closed tasks`, 'noLocationsFound': `No locations found`, + 'none': `None`, 'noNotifications': `No recent updates`, 'noOpenTasks': `No open tasks`, 'noPatient': `No Patient`, + 'noPatientsInTaskView': `No patients found for this task quick access.`, 'noResultsFound': `No results found`, 'notAssigned': `Not assigned`, 'notifications': `Notifications`, @@ -656,6 +746,12 @@ export const tasksTranslation: Translation { + return TranslationGen.resolvePlural(count, { + '=1': `${count} sort`, + 'other': `${count} sorts`, + }) + }, 'nTask': ({ count }): string => { return TranslationGen.resolvePlural(count, { '=1': `${count} Task`, @@ -673,6 +769,7 @@ export const tasksTranslation: Translation { return TranslationGen.resolvePlural(count, { @@ -725,6 +823,7 @@ export const tasksTranslation: Translation { return `Add ${name}` }, + 'readOnlyView': `Read-only`, 'recentPatients': `Your Recent Patients`, 'recentTasks': `Your Recent Tasks`, 'rEdit': ({ name }): string => { @@ -737,6 +836,12 @@ export const tasksTranslation: Translation { return `Show ${name}` }, + 'savedViews': `Quick access`, + 'saveView': `Save quick access`, + 'saveViewAsNew': `Save as new quick access`, + 'saveViewDescription': `Name this quick access. Filters, sorting, search, and location 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)...`, @@ -758,6 +863,7 @@ export const tasksTranslation: Translation { return TranslationGen.resolveSelect(subject, { 'patient': `Patient`, @@ -809,8 +915,18 @@ export const tasksTranslation: Translation { + return TranslationGen.resolvePlural(count, { + '=1': `${count} filtro`, + 'other': `${count} filtros`, + }) + }, 'no': `No`, 'noClosedTasks': `No hay tareas cerradas`, 'noLocationsFound': `No se encontraron ubicaciones`, + 'none': `Ninguno`, 'noNotifications': `Sin actualizaciones recientes`, 'noOpenTasks': `No hay tareas abiertas`, 'noPatient': `Sin paciente`, + 'noPatientsInTaskView': `No se encontraron pacientes para este acceso rápido de tareas.`, 'noResultsFound': `No se encontraron resultados`, 'notAssigned': `No asignado`, 'notifications': `Notificaciones`, @@ -957,6 +1089,12 @@ export const tasksTranslation: Translation { + return TranslationGen.resolvePlural(count, { + '=1': `${count} ordenación`, + 'other': `${count} ordenaciones`, + }) + }, 'nTask': ({ count }): string => { return TranslationGen.resolvePlural(count, { '=1': `${count} Tarea`, @@ -974,6 +1112,7 @@ export const tasksTranslation: Translation { return TranslationGen.resolvePlural(count, { @@ -1025,6 +1165,7 @@ export const tasksTranslation: Translation { return `Añadir ${name}` }, + 'readOnlyView': `Read-only`, 'recentPatients': `Tus pacientes recientes`, 'recentTasks': `Tus tareas recientes`, 'rEdit': ({ name }): string => { @@ -1037,6 +1178,12 @@ export const tasksTranslation: Translation { return `Mostrar ${name}` }, + '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)...`, @@ -1058,6 +1205,7 @@ export const tasksTranslation: Translation { return TranslationGen.resolveSelect(subject, { 'patient': `Paciente`, @@ -1109,8 +1257,18 @@ export const tasksTranslation: Translation { + return TranslationGen.resolvePlural(count, { + '=1': `${count} filtre`, + 'other': `${count} filtres`, + }) + }, 'no': `Non`, 'noClosedTasks': `Aucune tâche terminée`, 'noLocationsFound': `Aucun emplacement trouvé`, + 'none': `Aucun`, 'noNotifications': `Aucune mise à jour récente`, 'noOpenTasks': `Aucune tâche ouverte`, 'noPatient': `Aucun patient`, + 'noPatientsInTaskView': `Aucun patient trouvé pour cet accès rapide de tâches.`, 'noResultsFound': `Aucun résultat trouvé`, 'notAssigned': `Non assigné`, 'notifications': `Notifications`, @@ -1257,6 +1431,12 @@ export const tasksTranslation: Translation { + return TranslationGen.resolvePlural(count, { + '=1': `${count} tri`, + 'other': `${count} tris`, + }) + }, 'nTask': ({ count }): string => { return TranslationGen.resolvePlural(count, { '=1': `${count} Tâche`, @@ -1274,6 +1454,7 @@ export const tasksTranslation: Translation { return TranslationGen.resolvePlural(count, { @@ -1325,6 +1507,7 @@ export const tasksTranslation: Translation { return `Ajouter ${name}` }, + 'readOnlyView': `Read-only`, 'recentPatients': `Vos patients récents`, 'recentTasks': `Vos tâches récentes`, 'rEdit': ({ name }): string => { @@ -1337,6 +1520,12 @@ export const tasksTranslation: Translation { return `Afficher ${name}` }, + '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)...`, @@ -1358,6 +1547,7 @@ export const tasksTranslation: Translation { return TranslationGen.resolveSelect(subject, { 'patient': `Patient`, @@ -1409,8 +1599,18 @@ export const tasksTranslation: Translation { + return TranslationGen.resolvePlural(count, { + '=1': `${count} filter`, + 'other': `${count} filters`, + }) + }, 'no': `Nee`, 'noClosedTasks': `Geen afgeronde taken`, 'noLocationsFound': `Geen locaties gevonden`, + 'none': `Geen`, 'noNotifications': `Geen recente updates`, 'noOpenTasks': `Geen open taken`, 'noPatient': `Geen patiënt`, + 'noPatientsInTaskView': `Geen patiënten gevonden voor deze taak-snelle toegang.`, 'noResultsFound': `Geen resultaten gevonden`, 'notAssigned': `Niet toegewezen`, 'notifications': `Meldingen`, @@ -1557,6 +1773,12 @@ export const tasksTranslation: Translation { + return TranslationGen.resolvePlural(count, { + '=1': `${count} sortering`, + 'other': `${count} sorteringen`, + }) + }, 'nTask': ({ count }): string => { return TranslationGen.resolvePlural(count, { '=1': `${count} Taak`, @@ -1574,6 +1796,7 @@ export const tasksTranslation: Translation { let _out: string = '' @@ -1628,6 +1852,7 @@ export const tasksTranslation: Translation { return `${name} toevoegen` }, + 'readOnlyView': `Read-only`, 'recentPatients': `Uw recente patiënten`, 'recentTasks': `Uw recente taken`, 'rEdit': ({ name }): string => { @@ -1640,6 +1865,12 @@ export const tasksTranslation: Translation { return `${name} tonen` }, + 'savedViews': `Snelle toegang`, + 'saveView': `Save view`, + 'saveViewAsNew': `Save as new view`, + 'saveViewDescription': `Name this view. Filters, sorting, search, and location 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)...`, @@ -1661,6 +1892,7 @@ export const tasksTranslation: Translation { return TranslationGen.resolveSelect(subject, { 'patient': `Patiënt`, @@ -1712,8 +1944,18 @@ export const tasksTranslation: Translation { + return TranslationGen.resolvePlural(count, { + '=1': `${count} filtro`, + 'other': `${count} filtros`, + }) + }, 'no': `Não`, 'noClosedTasks': `Nenhuma tarefa concluída`, 'noLocationsFound': `Nenhuma localização encontrada`, + 'none': `Nenhum`, 'noNotifications': `Nenhuma atualização recente`, 'noOpenTasks': `Nenhuma tarefa aberta`, 'noPatient': `Sem paciente`, + 'noPatientsInTaskView': `Nenhum paciente encontrado para este acesso rápido de tarefas.`, 'noResultsFound': `Nenhum resultado encontrado`, 'notAssigned': `Não atribuído`, 'notifications': `Notificações`, @@ -1860,6 +2118,12 @@ export const tasksTranslation: Translation { + return TranslationGen.resolvePlural(count, { + '=1': `${count} ordenação`, + 'other': `${count} ordenações`, + }) + }, 'nTask': ({ count }): string => { return TranslationGen.resolvePlural(count, { '=1': `${count} Tarefa`, @@ -1877,6 +2141,7 @@ export const tasksTranslation: Translation { return TranslationGen.resolvePlural(count, { @@ -1928,6 +2194,7 @@ export const tasksTranslation: Translation { return `Adicionar ${name}` }, + 'readOnlyView': `Read-only`, 'recentPatients': `Seus pacientes recentes`, 'recentTasks': `Suas tarefas recentes`, 'rEdit': ({ name }): string => { @@ -1940,6 +2207,12 @@ export const tasksTranslation: Translation { return `Mostrar ${name}` }, + 'savedViews': `Acesso rápido`, + 'saveView': `Save view`, + 'saveViewAsNew': `Save as new view`, + 'saveViewDescription': `Name this view. Filters, sorting, search, and location 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)...`, @@ -1961,6 +2234,7 @@ export const tasksTranslation: Translation { return TranslationGen.resolveSelect(subject, { 'patient': `Paciente`, @@ -2012,8 +2286,18 @@ export const tasksTranslation: Translation=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 +213,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 +223,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,19 +255,6 @@ "@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", @@ -336,38 +265,6 @@ "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", @@ -378,499 +275,64 @@ "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==", - "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": { - "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==", - "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" @@ -954,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", @@ -1127,9 +590,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 +664,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 +925,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 +945,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 +1102,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 +1121,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" + "tslib": "~2.6.0" + }, + "engines": { + "node": ">=16" }, "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" - }, - "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" - }, - "engines": { - "node": ">=8" - } - }, - "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==", - "dev": true, - "license": "ISC" - }, - "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==", + "version": "2.6.3", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.3.tgz", + "integrity": "sha512-xNvxJEOUiWPGhUuUdQgAJPKOOJfGnIyKySOc09XkKsgdUV/3E2zvwZYdejjmRgPCgcym1juLH3226yA7sEFJKQ==", "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" - }, - "engines": { - "node": ">=8" - } + "license": "0BSD" }, - "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==", + "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": "ISC", + "license": "MIT", "dependencies": { - "camelcase": "^5.0.0", - "decamelize": "^1.2.0" + "@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": ">=6" + "node": ">=16.0.0" + }, + "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 +1947,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 +2151,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.5", + "resolved": "https://registry.npmjs.org/@helpwave/hightide/-/hightide-0.9.5.tgz", + "integrity": "sha512-Q510q5FiI27n/Unm1o49sDSDgshpHlVsdhyvnK2f4djG4NE+qmICgvT9hQAPEfZfPPCMQpdPYmTbLKn8lmZhCg==", "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 +2165,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": { @@ -4371,6 +3610,7 @@ "integrity": "sha512-6TyEnHgd6SArQO8UO2OMTxshln3QMWBtPGrOCgs3wVEmQmwyuNtB10IZMfmYDE0riwNR1cu4q+pPcxMVtaG3TA==", "devOptional": true, "license": "Apache-2.0", + "peer": true, "dependencies": { "playwright": "1.57.0" }, @@ -4381,6 +3621,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", @@ -5172,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" }, @@ -5288,8 +4562,9 @@ "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", + "peer": true, "dependencies": { "csstype": "^3.2.2" } @@ -5359,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", @@ -5668,6 +4944,7 @@ "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", "dev": true, "license": "MIT", + "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -5686,9 +4963,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 +5261,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", @@ -6089,6 +5320,7 @@ } ], "license": "MIT", + "peer": true, "dependencies": { "baseline-browser-mapping": "^2.9.0", "caniuse-lite": "^1.0.30001759", @@ -6103,16 +5335,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 +5406,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 +5731,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 +5763,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 +5868,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", @@ -7098,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", @@ -7210,9 +6403,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 +6470,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 +6614,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 +6714,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.2", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.4.2.tgz", + "integrity": "sha512-PjDse7RzhcPkIJwy5t7KPWQSZ9cAbzQXcafsetQoD7sOJRQlGikNbx7yZp2OotDnJyrDcbyRq3Ttb18iYOqkxA==", "dev": true, "license": "ISC" }, @@ -7620,13 +6780,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 +6936,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 +6949,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", @@ -7917,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" } @@ -8227,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" }, @@ -8385,14 +7494,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 +7550,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 +8883,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 +9092,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 +9112,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 +9337,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 +9427,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", @@ -10524,6 +9556,7 @@ } ], "license": "MIT", + "peer": true, "dependencies": { "nanoid": "^3.3.8", "picocolors": "^1.1.1", @@ -10550,16 +9583,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", @@ -10608,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" } @@ -10617,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" }, @@ -10675,18 +9700,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 +9734,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 +9921,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 +9970,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 +10151,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 +10548,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", @@ -11692,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" @@ -11724,33 +10703,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 +10829,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 +10839,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 +10922,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", @@ -12102,6 +11029,7 @@ "integrity": "sha512-blAT2mjOEIi0ZzruJfIhb3nps74PRWTCz1IjglWEEpQl5XS/UNama6u2/rjFkDDouqr4L67ry+1aGIALViWjDg==", "devOptional": true, "license": "MIT", + "peer": true, "engines": { "node": ">=10.0.0" }, diff --git a/web/package.json b/web/package.json index 86e73cdb..a59eba7a 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.5", "@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", @@ -57,4 +57,4 @@ "graphql": "16.12.0", "graphql-request": "7.3.5" } -} \ No newline at end of file +} diff --git a/web/pages/location/[id].tsx b/web/pages/location/[id].tsx index 6b7aad37..79643123 100644 --- a/web/pages/location/[id].tsx +++ b/web/pages/location/[id].tsx @@ -12,6 +12,8 @@ import { type LocationType, PatientState } from '@/api/gql/generated' import { useLocationNode, usePatients, useTasks } from '@/data' import { useMemo, useState } from 'react' import { useRouter } from 'next/router' +import type { ColumnFiltersState } from '@tanstack/react-table' +import type { FilterValue } from '@helpwave/hightide' import { LocationChips } from '@/components/locations/LocationChips' import { LOCATION_PATH_SEPARATOR } from '@/utils/location' @@ -45,6 +47,16 @@ const LocationPage: NextPage = () => { const isTeamLocation = locationData?.locationNode?.kind === 'TEAM' + const viewDefaultLocationFilters = useMemo((): ColumnFiltersState => { + if (!id) return [] + const value: FilterValue = { + dataType: 'singleTag', + operator: 'equals', + parameter: { uuidValue: id }, + } + return [{ id: 'position', value }] + }, [id]) + const { data: patientsData, refetch: refetchPatients, loading: isLoadingPatients } = usePatients( { rootLocationIds: id ? [id] : undefined }, { skip: !id || isTeamLocation } @@ -76,8 +88,8 @@ const LocationPage: NextPage = () => { 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 } + assignee: task.assignees[0] + ? { id: task.assignees[0].id, name: task.assignees[0].name, avatarURL: task.assignees[0].avatarUrl, isOnline: task.assignees[0].isOnline ?? null } : undefined, assigneeTeam: task.assigneeTeam ? { id: task.assigneeTeam.id, title: task.assigneeTeam.title } @@ -111,8 +123,8 @@ const LocationPage: NextPage = () => { 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 } + assignee: task.assignees[0] + ? { id: task.assignees[0].id, name: task.assignees[0].name, avatarURL: task.assignees[0].avatarUrl, isOnline: task.assignees[0].isOnline ?? null } : undefined, assigneeTeam: task.assigneeTeam ? { id: task.assigneeTeam.id, title: task.assigneeTeam.title } @@ -196,7 +208,7 @@ const LocationPage: NextPage = () => { {translation('errorOccurred')}
) : ( - + )} @@ -210,7 +222,6 @@ const LocationPage: NextPage = () => { onRefetch={handleRefetch} showAssignee={true} loading={isTeamLocation ? isLoadingTasks : isLoadingPatients} - showAllTasksMode={isTeamLocation && showAllTasks} headerActions={ isTeamLocation ? ( + @@ -274,40 +288,38 @@ const SettingsPage: NextPage = () => {
{translation('language')} -
{translation('pThemes', { count: 1 })} - 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 d7a1ab23..c23a6936 100644 --- a/web/pages/tasks/index.tsx +++ b/web/pages/tasks/index.tsx @@ -5,36 +5,90 @@ 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 { useCallback, 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 { usePropertyDefinitions, useTasksPaginated } from '@/data' +import { getPropertyColumnIds } from '@/hooks/usePropertyColumnVisibility' +import { PropertyEntity } from '@/api/gql/generated' +import { columnFiltersToQueryFilterClauses, paginationStateToPaginationInput, sortingStateToQuerySortClauses } from '@/utils/tableStateToApi' +import { Visibility } from '@helpwave/hightide' +import { SaveViewDialog } from '@/components/views/SaveViewDialog' +import { SaveViewActionsMenu } from '@/components/views/SaveViewActionsMenu' +import { SavedViewEntityType } from '@/api/gql/generated' +import { + serializeColumnFiltersForView, + serializeSortingForView, + stringifyViewParameters, + tableViewStateMatchesBaseline +} from '@/utils/viewDefinition' +import type { ColumnFiltersState, ColumnOrderState, PaginationState, SortingState, VisibilityState } from '@tanstack/react-table' const TasksPage: NextPage = () => { const translation = useTasksTranslation() const router = useRouter() const { selectedRootLocationIds, user, myTasksCount } = useTasksContext() - const { - pagination, - setPagination, - sorting, - setSorting, - filters, - setFilters, - columnVisibility, - setColumnVisibility, - } = useStorageSyncedTableState('task-list', { - defaultSorting: useMemo(() => [ - { id: 'done', desc: false }, - { id: 'dueDate', desc: false }, - ], []), - }) + const { data: propertyDefinitionsData } = usePropertyDefinitions() + const propertyColumnIds = useMemo( + () => getPropertyColumnIds(propertyDefinitionsData, PropertyEntity.Task), + [propertyDefinitionsData] + ) + const defaultSorting = useMemo(() => [ + { id: 'done', desc: false }, + { id: 'dueDate', desc: false }, + ], []) + const [pagination, setPagination] = useState({ pageSize: 10, pageIndex: 0 }) + const [sorting, setSorting] = useState(defaultSorting) + const [filters, setFilters] = useState([]) + const [columnVisibility, setColumnVisibility] = useState({}) + const [columnOrder, setColumnOrder] = useState([]) + + const baselineFilters = useMemo((): ColumnFiltersState => [], []) + + const [isSaveViewOpen, setIsSaveViewOpen] = useState(false) + const [searchQuery, setSearchQuery] = useState('') + + const viewMatchesBaseline = useMemo( + () => tableViewStateMatchesBaseline({ + filters: filters as ColumnFiltersState, + baselineFilters, + sorting, + baselineSorting: defaultSorting, + searchQuery, + baselineSearch: '', + columnVisibility, + baselineColumnVisibility: undefined, + columnOrder, + baselineColumnOrder: undefined, + propertyColumnIds, + }), + [ + filters, + baselineFilters, + sorting, + defaultSorting, + searchQuery, + columnVisibility, + columnOrder, + propertyColumnIds, + ] + ) + const hasUnsavedViewChanges = !viewMatchesBaseline + + const handleDiscardTasksView = useCallback(() => { + setFilters(baselineFilters) + setSorting(defaultSorting) + setSearchQuery('') + setColumnVisibility({}) + setColumnOrder([]) + }, [baselineFilters, defaultSorting]) - const apiFiltering = useMemo(() => columnFiltersToFilterInput(filters, 'task'), [filters]) - const apiSorting = useMemo(() => sortingStateToSortInput(sorting, 'task'), [sorting]) + 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 +96,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 @@ -67,8 +122,8 @@ const TasksPage: NextPage = () => { 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 } + assignee: task.assignees[0] + ? { id: task.assignees[0].id, name: task.assignees[0].name, avatarURL: task.assignees[0].avatarUrl, isOnline: task.assignees[0].isOnline ?? null } : undefined, properties: task.properties ?? [], })) @@ -80,6 +135,21 @@ 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={serializeSortingForView(sorting)} + parameters={stringifyViewParameters({ + rootLocationIds: selectedRootLocationIds ?? undefined, + assigneeId: user?.id, + columnVisibility, + columnOrder, + })} + presentation="fromSystemList" + onCreated={(id) => router.push(`/view/${id}`)} + /> { onInitialTaskOpened={() => router.replace('/tasks', undefined, { shallow: true })} totalCount={totalCount} loading={tasksLoading} + searchQuery={searchQuery} + onSearchQueryChange={setSearchQuery} + saveViewSlot={( + + undefined} + onOpenSaveAsNew={() => setIsSaveViewOpen(true)} + onDiscard={handleDiscardTasksView} + /> + + )} tableState={{ pagination, setPagination, @@ -97,6 +179,8 @@ const TasksPage: NextPage = () => { setFilters, columnVisibility, setColumnVisibility, + columnOrder, + setColumnOrder, }} /> diff --git a/web/pages/view/[uid].tsx b/web/pages/view/[uid].tsx new file mode 100644 index 00000000..ca967738 --- /dev/null +++ b/web/pages/view/[uid].tsx @@ -0,0 +1,526 @@ +'use client' + +import type { NextPage } from 'next' +import { useRouter } from 'next/router' +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, 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 { 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 { appendSavedViewToMySavedViewsCache, replaceSavedViewInMySavedViewsCache } from '@/utils/savedViewsCache' +import { + deserializeColumnFiltersFromView, + deserializeSortingFromView, + 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 { useTableState } from '@/hooks/useTableState' +import { columnFiltersToQueryFilterClauses, paginationStateToPaginationInput, sortingStateToQuerySortClauses } from '@/utils/tableStateToApi' +import { Share2 } from 'lucide-react' + +type SavedTaskViewTabProps = { + viewId: string, + filterDefinition: string, + sortDefinition: string, + parameters: ReturnType, + isOwner: boolean, +} + +function SavedTaskViewTab({ + viewId, + filterDefinition, + sortDefinition, + parameters, + isOwner, +}: SavedTaskViewTabProps) { + const router = useRouter() + const { selectedRootLocationIds, user } = useTasksContext() + const defaultFilters = deserializeColumnFiltersFromView(filterDefinition) + const defaultSorting = deserializeSortingFromView(sortDefinition) + + const baselineSort = useMemo(() => [ + { id: 'done', desc: false }, + { 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, + sorting, + setSorting, + filters, + setFilters, + columnVisibility, + setColumnVisibility, + columnOrder, + setColumnOrder, + } = useTableState({ + defaultFilters, + defaultSorting: viewSortBaseline, + defaultColumnVisibility: baselineColumnVisibility, + defaultColumnOrder: baselineColumnOrder, + }) + + 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, + filterDefinition, + sortDefinition, + parameters.searchQuery, + parameters.columnVisibility, + parameters.columnOrder, + setFilters, + setSorting, + setSearchQuery, + setColumnVisibility, + setColumnOrder, + setPagination, + ]) + + 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), { + awaitRefetchQueries: true, + refetchQueries: [ + { query: getParsedDocument(SavedViewDocument), variables: { id: viewId } }, + { query: getParsedDocument(MySavedViewsDocument) }, + ], + update(cache, { data }) { + const view = data?.updateSavedView + if (view) { + replaceSavedViewInMySavedViewsCache(cache, view) + } + }, + }) + + 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 } + : undefined, + { + pagination: apiPagination, + sorts: apiSorting.length > 0 ? apiSorting : undefined, + filters: apiFilters.length > 0 ? apiFilters : undefined, + search: searchInput, + } + ) + + 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.assignees[0] + ? { id: task.assignees[0].id, name: task.assignees[0].name, avatarURL: task.assignees[0].avatarUrl, isOnline: task.assignees[0].isOnline ?? null } + : undefined, + properties: task.properties ?? [], + })) + }, [tasksData]) + + const viewParametersForSave = useMemo(() => stringifyViewParameters({ + rootLocationIds: rootIds ?? undefined, + assigneeId: assigneeId ?? undefined, + searchQuery: searchQuery || undefined, + }), [rootIds, assigneeId, searchQuery]) + + return ( + <> + 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, + }} + /> + + ) +} + +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), { + refetchQueries: [{ query: getParsedDocument(MySavedViewsDocument) }], + awaitRefetchQueries: true, + update(cache, { data }) { + const view = data?.duplicateSavedView + if (view) { + appendSavedViewToMySavedViewsCache(cache, view) + } + }, + }) + + 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.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/schema.graphql b/web/schema.graphql new file mode 100644 index 00000000..b065f404 --- /dev/null +++ b/web/schema.graphql @@ -0,0 +1,481 @@ +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 CreateSavedViewInput { + name: String! + baseEntityType: SavedViewEntityType! + filterDefinition: String! + sortDefinition: String! + parameters: String! + visibility: SavedViewVisibility! = PRIVATE +} + +input CreateTaskInput { + title: String! + patientId: ID = null + description: String = null + dueDate: DateTime = null + assigneeIds: [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! + addTaskAssignee(id: ID!, userId: ID!): TaskType! + removeTaskAssignee(id: ID!, userId: 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! + createSavedView(data: CreateSavedViewInput!): SavedView! + updateSavedView(id: ID!, data: UpdateSavedViewInput!): SavedView! + deleteSavedView(id: ID!): Boolean! + duplicateSavedView(id: ID!, name: String!): SavedView! +} + +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!]! + savedView(id: ID!): SavedView + mySavedViews: [SavedView!]! +} + +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 + boolValue: Boolean = null + dateValue: DateTime = null + dateMin: Date = null + dateMax: Date = 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! + sortDirections: [SortDirection!]! + searchable: Boolean! + filterable: 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 +} + +type SavedView { + id: ID! + name: String! + baseEntityType: SavedViewEntityType! + filterDefinition: String! + sortDefinition: String! + parameters: String! + ownerUserId: ID! + visibility: SavedViewVisibility! + createdAt: String! + updatedAt: String! + isOwner: Boolean! +} + +enum SavedViewEntityType { + TASK + PATIENT +} + +enum SavedViewVisibility { + PRIVATE + LINK_SHARED +} + +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 + assigneeTeamId: ID + patientId: ID + priority: String + estimatedTime: Int + assignees: [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 UpdateSavedViewInput { + name: String = null + filterDefinition: String = null + sortDefinition: String = null + parameters: String = null + visibility: SavedViewVisibility = null +} + +input UpdateTaskInput { + title: String = null + patientId: ID + description: String = null + done: Boolean = null + dueDate: DateTime + assigneeIds: [ID!] + 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!]! +} diff --git a/web/utils/columnOrder.ts b/web/utils/columnOrder.ts new file mode 100644 index 00000000..1f520cd4 --- /dev/null +++ b/web/utils/columnOrder.ts @@ -0,0 +1,29 @@ +import type { ColumnDef, ColumnOrderState } from '@tanstack/table-core' + +export function columnIdsFromColumnDefs(columns: ColumnDef[]): string[] { + return columns + .map((col) => { + if (typeof col.id === 'string' && col.id.length > 0) { + return col.id + } + if ('accessorKey' in col && typeof col.accessorKey === 'string') { + return col.accessorKey + } + return undefined + }) + .filter((id): id is string => id != null) +} + +export function sanitizeColumnOrderForKnownColumns( + order: ColumnOrderState, + knownColumnIdsOrdered: readonly string[] +): ColumnOrderState { + if (knownColumnIdsOrdered.length === 0) { + return [] + } + const knownSet = new Set(knownColumnIdsOrdered) + const kept = order.filter((id) => knownSet.has(id)) + const seen = new Set(kept) + const appended = knownColumnIdsOrdered.filter((id) => !seen.has(id)) + return [...kept, ...appended] +} diff --git a/web/utils/propertyColumn.tsx b/web/utils/propertyColumn.tsx index 5310125c..9cb0848e 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' @@ -49,7 +49,8 @@ function getFilterData(prop: PropertyDefinitionType) { } export function createPropertyColumn( - prop: PropertyDefinitionType + prop: PropertyDefinitionType, + hasFilter?: boolean ): ColumnDef { const columnId = `property_${prop.id}` const filterFn = getPropertyFilterFn(prop.fieldType) @@ -71,7 +72,7 @@ export function createPropertyColumn( return () }, meta: { - columnType: ColumnType.Property, + columnType: 'PROPERTY', propertyDefinitionId: prop.id, fieldType: prop.fieldType, ...(filterData && { filterData }), @@ -79,7 +80,7 @@ export function createPropertyColumn( minSize: 220, size: 220, maxSize: 300, - filterFn, + filterFn: hasFilter ? filterFn : undefined, } as ColumnDef } @@ -89,11 +90,12 @@ type PropertyDefinitionsData = { export function getPropertyColumnsForEntity( propertyDefinitionsData: PropertyDefinitionsData, - entity: PropertyEntity + entity: PropertyEntity, + hasFilter?: boolean ): ColumnDef[] { if (!propertyDefinitionsData?.propertyDefinitions) return [] const properties = propertyDefinitionsData.propertyDefinitions.filter( def => def.isActive && def.allowedEntities.includes(entity) ) - return properties.map(prop => createPropertyColumn(prop)) + return properties.map(prop => createPropertyColumn(prop, hasFilter)) } diff --git a/web/utils/propertyFilterMapping.ts b/web/utils/propertyFilterMapping.ts index a73a32e0..06a3bfe1 100644 --- a/web/utils/propertyFilterMapping.ts +++ b/web/utils/propertyFilterMapping.ts @@ -1,5 +1,5 @@ import { FieldType } from '@/api/gql/generated' -import type { TableFilterCategory } from '@helpwave/hightide' +import type { DataType } from '@helpwave/hightide' /** * Maps a FieldType to the appropriate filter function name for TanStack Table. @@ -9,7 +9,7 @@ import type { TableFilterCategory } from '@helpwave/hightide' * - date vs datetime (for proper date/time filtering) * - tags (multi-select) vs tags_single (single select) */ -export function getPropertyFilterFn(fieldType: FieldType): TableFilterCategory { +export function getPropertyFilterFn(fieldType: FieldType): DataType { switch (fieldType) { case FieldType.FieldTypeCheckbox: return 'boolean' diff --git a/web/utils/queryableFilterList.tsx b/web/utils/queryableFilterList.tsx new file mode 100644 index 00000000..f86dac85 --- /dev/null +++ b/web/utils/queryableFilterList.tsx @@ -0,0 +1,70 @@ +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 { LocationSubtreeFilterPopUp } from '@/components/tables/LocationSubtreeFilterPopUp' +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' +} + +function filterFieldDataType(field: QueryableField): DataType { + if (field.key === 'position') return 'singleTag' + return valueKindToDataType(field) +} + +export type QueryableSortListItem = Pick + +export function queryableFieldsToFilterListItems( + fields: QueryableField[], + propertyFieldTypeByDefId: Map +): FilterListItem[] { + return fields.filter(field => field.filterable).map((field): FilterListItem => { + const dataType = filterFieldDataType(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 => () + : field.key === 'position' + ? (props: FilterListPopUpBuilderProps): ReactNode => () + : undefined, + } + }) +} + +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), + })) +} diff --git a/web/utils/savedViewsCache.ts b/web/utils/savedViewsCache.ts new file mode 100644 index 00000000..65cc9be8 --- /dev/null +++ b/web/utils/savedViewsCache.ts @@ -0,0 +1,43 @@ +import type { ApolloCache } from '@apollo/client' +import { MySavedViewsDocument, type MySavedViewsQuery } from '@/api/gql/generated' +import { getParsedDocument } from '@/data/hooks/queryHelpers' + +type SavedViewRow = MySavedViewsQuery['mySavedViews'][number] + +const mySavedViewsQuery = { query: getParsedDocument(MySavedViewsDocument) } + +export function appendSavedViewToMySavedViewsCache(cache: ApolloCache, view: SavedViewRow): void { + cache.updateQuery(mySavedViewsQuery, (data) => { + if (!data) { + return data + } + if (data.mySavedViews.some((v) => v.id === view.id)) { + return data + } + return { ...data, mySavedViews: [...data.mySavedViews, view] } + }) +} + +export function replaceSavedViewInMySavedViewsCache(cache: ApolloCache, view: SavedViewRow): void { + cache.updateQuery(mySavedViewsQuery, (data) => { + if (!data) { + return data + } + const idx = data.mySavedViews.findIndex((v) => v.id === view.id) + if (idx === -1) { + return { ...data, mySavedViews: [...data.mySavedViews, view] } + } + const next = [...data.mySavedViews] + next[idx] = view + return { ...data, mySavedViews: next } + }) +} + +export function removeSavedViewFromMySavedViewsCache(cache: ApolloCache, id: string): void { + cache.updateQuery(mySavedViewsQuery, (data) => { + if (!data) { + return data + } + return { ...data, mySavedViews: data.mySavedViews.filter((v) => v.id !== id) } + }) +} diff --git a/web/utils/tableStateToApi.ts b/web/utils/tableStateToApi.ts index c7342bcf..0248b1fc 100644 --- a/web/utils/tableStateToApi.ts +++ b/web/utils/tableStateToApi.ts @@ -1,158 +1,199 @@ 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 { TableFilterValue } from '@helpwave/hightide' +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 = { - textEquals: 'TEXT_EQUALS' as FilterOperator, - textNotEquals: 'TEXT_NOT_EQUALS' as FilterOperator, - textContains: 'TEXT_CONTAINS' as FilterOperator, - textNotContains: 'TEXT_NOT_CONTAINS' as FilterOperator, - textStartsWith: 'TEXT_STARTS_WITH' as FilterOperator, - textEndsWith: 'TEXT_ENDS_WITH' as FilterOperator, - textNotWhitespace: 'TEXT_NOT_WHITESPACE' as FilterOperator, - numberEquals: 'NUMBER_EQUALS' as FilterOperator, - numberNotEquals: 'NUMBER_NOT_EQUALS' as FilterOperator, - numberGreaterThan: 'NUMBER_GREATER_THAN' as FilterOperator, - numberGreaterThanOrEqual: 'NUMBER_GREATER_THAN_OR_EQUAL' as FilterOperator, - numberLessThan: 'NUMBER_LESS_THAN' as FilterOperator, - numberLessThanOrEqual: 'NUMBER_LESS_THAN_OR_EQUAL' as FilterOperator, - numberBetween: 'NUMBER_BETWEEN' as FilterOperator, - numberNotBetween: 'NUMBER_NOT_BETWEEN' as FilterOperator, - dateEquals: 'DATE_EQUALS' as FilterOperator, - dateNotEquals: 'DATE_NOT_EQUALS' as FilterOperator, - dateGreaterThan: 'DATE_GREATER_THAN' as FilterOperator, - dateGreaterThanOrEqual: 'DATE_GREATER_THAN_OR_EQUAL' as FilterOperator, - dateLessThan: 'DATE_LESS_THAN' as FilterOperator, - dateLessThanOrEqual: 'DATE_LESS_THAN_OR_EQUAL' as FilterOperator, - dateBetween: 'DATE_BETWEEN' as FilterOperator, - dateNotBetween: 'DATE_NOT_BETWEEN' as FilterOperator, - dateTimeEquals: 'DATETIME_EQUALS' as FilterOperator, - dateTimeNotEquals: 'DATETIME_NOT_EQUALS' as FilterOperator, - dateTimeGreaterThan: 'DATETIME_GREATER_THAN' as FilterOperator, - dateTimeGreaterThanOrEqual: 'DATETIME_GREATER_THAN_OR_EQUAL' as FilterOperator, - dateTimeLessThan: 'DATETIME_LESS_THAN' as FilterOperator, - dateTimeLessThanOrEqual: 'DATETIME_LESS_THAN_OR_EQUAL' as FilterOperator, - dateTimeBetween: 'DATETIME_BETWEEN' as FilterOperator, - dateTimeNotBetween: 'DATETIME_NOT_BETWEEN' as FilterOperator, - booleanIsTrue: 'BOOLEAN_IS_TRUE' as FilterOperator, - booleanIsFalse: 'BOOLEAN_IS_FALSE' as FilterOperator, - tagsEquals: 'TAGS_EQUALS' as FilterOperator, - tagsNotEquals: 'TAGS_NOT_EQUALS' as FilterOperator, - tagsContains: 'TAGS_CONTAINS' as FilterOperator, - tagsNotContains: 'TAGS_NOT_CONTAINS' as FilterOperator, - tagsSingleEquals: 'TAGS_SINGLE_EQUALS' as FilterOperator, - tagsSingleNotEquals: 'TAGS_SINGLE_NOT_EQUALS' as FilterOperator, - tagsSingleContains: 'TAGS_SINGLE_CONTAINS' as FilterOperator, - tagsSingleNotContains: 'TAGS_SINGLE_NOT_CONTAINS' as FilterOperator, - isNull: 'IS_NULL' as FilterOperator, - isNotNull: 'IS_NOT_NULL' as FilterOperator, +const TABLE_OPERATOR_TO_QUERY: Record>> = { + text: { + 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: 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: 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: 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: QueryOperator.Eq, + isFalse: QueryOperator.Eq, + isNotUndefined: QueryOperator.IsNotNull, + isUndefined: QueryOperator.IsNull, + }, + singleTag: { + equals: QueryOperator.Eq, + notEquals: QueryOperator.Neq, + contains: QueryOperator.In, + notContains: QueryOperator.Neq, + isNotUndefined: QueryOperator.IsNotNull, + isUndefined: QueryOperator.IsNull, + }, + multiTags: { + equals: QueryOperator.AllIn, + notEquals: QueryOperator.Neq, + contains: QueryOperator.AnyIn, + notContains: QueryOperator.NoneIn, + isNotUndefined: QueryOperator.IsNotNull, + isUndefined: QueryOperator.IsNull, + }, + unknownType: { + isNotUndefined: QueryOperator.IsNotNull, + isUndefined: QueryOperator.IsNull, + }, } -function tableOperatorToApi(operator: string): FilterOperator | null { - const normalized = operator.replace(/([A-Z])/g, (m) => m.toLowerCase()) - return TABLE_OPERATOR_TO_API[normalized] ?? TABLE_OPERATOR_TO_API[operator] ?? (operator in TABLE_OPERATOR_TO_API ? (operator as FilterOperator) : null) +function tableOperatorToQuery(dataType: DataType, operator: HightideFilterOperator): QueryOperator | null { + return TABLE_OPERATOR_TO_QUERY[dataType][operator] ?? null } -function toFilterParameter(value: TableFilterValue): FilterParameter { - const p = value.parameter as Record - const param: FilterParameter = { - searchText: typeof p['searchText'] === 'string' ? p['searchText'] : undefined, - isCaseSensitive: typeof p['isCaseSensitive'] === 'boolean' ? p['isCaseSensitive'] : false, - compareValue: typeof p['compareValue'] === 'number' ? p['compareValue'] : undefined, - min: typeof p['min'] === 'number' ? p['min'] : undefined, - max: typeof p['max'] === 'number' ? p['max'] : undefined, - } - if (p['compareDate'] instanceof Date) { - param.compareDate = (p['compareDate'] as Date).toISOString().slice(0, 10) - } else if (typeof p['compareDate'] === 'string') { - param.compareDate = p['compareDate'] - } - if (p['min'] instanceof Date) param.minDate = (p['min'] as Date).toISOString().slice(0, 10) - else if (typeof p['min'] === 'string' && (p['min'] as string).length === 10) param.minDate = p['min'] as string - if (p['max'] instanceof Date) param.maxDate = (p['max'] as Date).toISOString().slice(0, 10) - else if (typeof p['max'] === 'string' && (p['max'] as string).length === 10) param.maxDate = p['max'] as string - if (p['compareDatetime'] instanceof Date) { - param.compareDateTime = (p['compareDatetime'] as Date).toISOString() - } else if (typeof p['compareDatetime'] === 'string') { - param.compareDateTime = p['compareDatetime'] - } - if (p['minDateTime'] instanceof Date) param.minDateTime = (p['minDateTime'] as Date).toISOString() - else if (typeof p['minDateTime'] === 'string') param.minDateTime = p['minDateTime'] - if (p['maxDateTime'] instanceof Date) param.maxDateTime = (p['maxDateTime'] as Date).toISOString() - else if (typeof p['maxDateTime'] === 'string') param.maxDateTime = p['maxDateTime'] - if (Array.isArray(p['searchTags'])) { - param.searchTags = (p['searchTags'] as unknown[]).filter((t): t is string => typeof t === 'string') - } - if (Array.isArray(p['searchTagsContains']) && (param.searchTags == null || param.searchTags.length === 0)) { - param.searchTags = (p['searchTagsContains'] as unknown[]).filter((t): t is string => typeof t === 'string') - } - if (param.searchTags == null && p['searchTag'] != null) { - param.searchTags = [String(p['searchTag'])] - } - if (typeof p['propertyDefinitionId'] === 'string') { - param.propertyDefinitionId = p['propertyDefinitionId'] - } - return param +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}` } -const TASK_COLUMN_TO_BACKEND: Record = { - dueDate: 'due_date', - updateDate: 'update_date', - creationDate: 'creation_date', - estimatedTime: 'estimated_time', - assigneeTeam: 'assignee_team_id', +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 isPropertyColumnId(id: string): boolean { - return id.startsWith('property_') +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 getPropertyDefinitionId(id: string): string | undefined { - if (!isPropertyColumnId(id)) return undefined - return id.replace(/^property_/, '') +function filterDateValueForDataType(value: FilterValue): string | undefined { + const parameter = value.parameter + if (value.dataType === 'dateTime') { + if (parameter.dateValue == null) return undefined + return parameter.dateValue.toISOString() + } + const day = toGraphqlDateInput(parameter.dateValue) + if (!day) return undefined + return localCalendarDateToIso(day) } -function columnIdToBackend(columnId: string, entity: 'task' | 'patient'): string { - if (entity === 'task' && TASK_COLUMN_TO_BACKEND[columnId]) { - return TASK_COLUMN_TO_BACKEND[columnId] +function toQueryFilterValue(value: FilterValue): QueryFilterValueInput { + const parameter = value.parameter + const raw = parameter as Record + const multi = parameter.uuidValues + const hasMulti = Array.isArray(multi) && multi.length > 0 + const hasSingle = parameter.uuidValue != null && String(parameter.uuidValue) !== '' + let searchTagsUnknownType: unknown[] = [] + if (!hasMulti && !hasSingle) { + if (Array.isArray(raw['searchTags']) && raw['searchTags'].length > 0) { + searchTagsUnknownType = raw['searchTags'] as unknown[] + } else if (Array.isArray(raw['searchTagsContains']) && raw['searchTagsContains'].length > 0) { + searchTagsUnknownType = raw['searchTagsContains'] as unknown[] + } else if (raw['searchTag'] != null) { + searchTagsUnknownType = [raw['searchTag']] + } + } + const searchTags: string[] = searchTagsUnknownType.map((t) => String(t)) + const base: QueryFilterValueInput = { + stringValue: parameter.stringValue, + floatValue: parameter.numberValue, + floatMin: parameter.numberMin, + floatMax: parameter.numberMax, + dateValue: filterDateValueForDataType(value), + dateMin: toGraphqlDateInput(parameter.dateMin), + dateMax: toGraphqlDateInput(parameter.dateMax), + stringValues: searchTags.length > 0 ? searchTags : undefined, + uuidValue: hasSingle ? String(parameter.uuidValue) : undefined, + uuidValues: hasMulti ? (multi as string[]) : undefined, } - return columnId + if (value.dataType === 'singleTag' && value.operator === 'equals' && searchTags.length === 1 && !hasSingle) { + base.stringValue = searchTags[0] + base.stringValues = undefined + } + 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 TableFilterValue - if (!value?.operator || !value?.parameter) continue - const apiOperator = tableOperatorToApi(value.operator) + const value = filter.value as FilterValue + if (!value?.operator || !value?.parameter || !value?.dataType) continue + 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 })) } @@ -162,3 +203,6 @@ export function paginationStateToPaginationInput(pagination: PaginationState): { pageSize: pagination.pageSize ?? 10, } } + +export { columnFiltersToQueryFilterClauses as columnFiltersToFilterInput } +export { sortingStateToQuerySortClauses as sortingStateToSortInput } diff --git a/web/utils/viewDefinition.ts b/web/utils/viewDefinition.ts new file mode 100644 index 00000000..f4b025b6 --- /dev/null +++ b/web/utils/viewDefinition.ts @@ -0,0 +1,218 @@ +import type { + ColumnFilter, + ColumnFiltersState, + ColumnOrderState, + SortingState, + VisibilityState +} from '@tanstack/react-table' +import type { DataType, FilterOperator, FilterParameter, FilterValue } from '@helpwave/hightide' + +export type ViewParameters = { + rootLocationIds?: string[], + locationId?: string, + searchQuery?: string, + assigneeId?: string, + columnVisibility?: VisibilityState, + columnOrder?: ColumnOrderState, +} + +export function hasActiveLocationFilter(filters: ColumnFiltersState): boolean { + return filters.some(f => { + if (f.id !== 'position' && f.id !== 'locationSubtree') return false + const v = f.value as FilterValue | undefined + if (!v?.parameter) return false + const p = v.parameter + if (p.uuidValue != null && String(p.uuidValue) !== '') return true + if (Array.isArray(p.uuidValues) && p.uuidValues.length > 0) return true + return false + }) +} + +export function normalizedVisibilityForViewCompare(v: VisibilityState): string { + const keys = Object.keys(v).sort() + const sorted: Record = {} + for (const k of keys) { + sorted[k] = v[k] as boolean + } + return JSON.stringify(sorted) +} + +export function visibilityMatchesViewBaseline( + current: VisibilityState, + baseline: VisibilityState | undefined +): boolean { + return normalizedVisibilityForViewCompare(current) === normalizedVisibilityForViewCompare(baseline ?? {}) +} + +export function expandVisibilityWithPropertyColumnDefaults( + v: VisibilityState, + propertyColumnIds: readonly string[] +): VisibilityState { + if (propertyColumnIds.length === 0) { + return v + } + const out: VisibilityState = { ...v } + for (const id of propertyColumnIds) { + if (!(id in out)) { + out[id] = false + } + } + return out +} + +export function visibilityMatchesViewBaselineForDirty( + current: VisibilityState, + baseline: VisibilityState | undefined, + propertyColumnIds: readonly string[] +): boolean { + const base = baseline ?? {} + return normalizedVisibilityForViewCompare( + expandVisibilityWithPropertyColumnDefaults(current, propertyColumnIds) + ) === normalizedVisibilityForViewCompare( + expandVisibilityWithPropertyColumnDefaults(base, propertyColumnIds) + ) +} + +export function normalizedColumnOrderForViewCompare(order: ColumnOrderState): string { + return JSON.stringify(order) +} + +export function columnOrderMatchesViewBaseline( + current: ColumnOrderState, + baseline: ColumnOrderState | undefined +): boolean { + return normalizedColumnOrderForViewCompare(current) === normalizedColumnOrderForViewCompare(baseline ?? []) +} + +export function columnOrderMatchesBaselineForDirty( + current: ColumnOrderState, + baseline: ColumnOrderState | undefined +): boolean { + const b = baseline ?? [] + if (b.length === 0) { + return true + } + return normalizedColumnOrderForViewCompare(current) === normalizedColumnOrderForViewCompare(b) +} + +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) +} + +/** Wire format for `filterDefinition` on saved views (JSON string). */ +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, + dateValue: filterParameter.dateValue ? filterParameter.dateValue.toISOString() : undefined, + dateMin: filterParameter.dateMin ? filterParameter.dateMin.toISOString() : undefined, + dateMax: filterParameter.dateMax ? filterParameter.dateMax.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 filterId = filter['id'] + const resolvedId = filterId === 'locationSubtree' ? 'position' : filterId + const value = filter['value'] as Record + const parameter = value['parameter'] as Record + const dateValueRaw = parameter['dateValue'] ?? parameter['compareDate'] + const dateMinRaw = parameter['dateMin'] ?? parameter['minDate'] + const dateMaxRaw = parameter['dateMax'] ?? parameter['maxDate'] + const parseStoredDate = (raw: unknown): Date | undefined => { + if (raw == null || raw === '') return undefined + const d = new Date(String(raw)) + return Number.isNaN(d.getTime()) ? undefined : d + } + const filterParameter: FilterParameter = { + stringValue: (parameter['stringValue'] ?? parameter['searchText']) as string | undefined, + numberValue: (parameter['numberValue'] ?? parameter['compareValue']) as number | undefined, + numberMin: (parameter['numberMin'] ?? parameter['minNumber']) as number | undefined, + numberMax: (parameter['numberMax'] ?? parameter['maxNumber']) as number | undefined, + booleanValue: parameter['booleanValue'] as boolean | undefined, + dateValue: parseStoredDate(dateValueRaw), + dateMin: parseStoredDate(dateMinRaw), + dateMax: parseStoredDate(dateMaxRaw), + uuidValue: parameter['uuidValue'] ?? parameter['singleOptionSearch'], + uuidValues: (parameter['uuidValues'] ?? parameter['multiOptionSearch']) as unknown[] | undefined, + } + const mappedValue: FilterValue = { + operator: value['operator'] as FilterOperator, + dataType: value['dataType'] as DataType, + parameter: filterParameter, + } + return { + ...filter, + id: resolvedId as string, + 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 [] + } +} + +export function tableViewStateMatchesBaseline(params: { + filters: ColumnFiltersState, + baselineFilters: ColumnFiltersState, + sorting: SortingState, + baselineSorting: SortingState, + searchQuery: string, + baselineSearch: string, + columnVisibility: VisibilityState, + baselineColumnVisibility: VisibilityState | undefined, + columnOrder: ColumnOrderState, + baselineColumnOrder: ColumnOrderState | undefined, + propertyColumnIds: readonly string[], +}): boolean { + const filtersMatch = + serializeColumnFiltersForView(params.filters) === serializeColumnFiltersForView(params.baselineFilters) + const sortMatch = + serializeSortingForView(params.sorting) === serializeSortingForView(params.baselineSorting) + const searchMatch = params.searchQuery === params.baselineSearch + const visMatch = visibilityMatchesViewBaselineForDirty( + params.columnVisibility, + params.baselineColumnVisibility, + params.propertyColumnIds + ) + const orderMatch = columnOrderMatchesBaselineForDirty(params.columnOrder, params.baselineColumnOrder) + return filtersMatch && sortMatch && searchMatch && visMatch && orderMatch +}