diff --git a/backend/api/inputs.py b/backend/api/inputs.py index 4936f82..f8f4891 100644 --- a/backend/api/inputs.py +++ b/backend/api/inputs.py @@ -49,6 +49,14 @@ class PatientState(Enum): DEAD = "DEAD" +@strawberry.enum +class TaskPriority(Enum): + P1 = "P1" + P2 = "P2" + P3 = "P3" + P4 = "P4" + + @strawberry.input class PropertyValueInput: definition_id: strawberry.ID @@ -104,6 +112,8 @@ class CreateTaskInput: assignee_id: strawberry.ID | None = None previous_task_ids: list[strawberry.ID] | None = None properties: list[PropertyValueInput] | None = None + priority: TaskPriority | None = None + estimated_time: int | None = None @strawberry.input @@ -116,6 +126,8 @@ class UpdateTaskInput: previous_task_ids: list[strawberry.ID] | None = None properties: list[PropertyValueInput] | None = None checksum: str | None = None + priority: TaskPriority | None = strawberry.UNSET + estimated_time: int | None = strawberry.UNSET @strawberry.input diff --git a/backend/api/resolvers/task.py b/backend/api/resolvers/task.py index fef46d2..b32ba53 100644 --- a/backend/api/resolvers/task.py +++ b/backend/api/resolvers/task.py @@ -224,6 +224,8 @@ async def create_task(self, info: Info, data: CreateTaskInput) -> TaskType: patient_id=data.patient_id, assignee_id=data.assignee_id, due_date=normalize_datetime_to_utc(data.due_date), + priority=data.priority.value if data.priority else None, + estimated_time=data.estimated_time, ) if data.properties is not None: @@ -284,6 +286,12 @@ async def update_task( else None ) + if data.priority is not strawberry.UNSET: + task.priority = data.priority.value if data.priority else None + + if data.estimated_time is not strawberry.UNSET: + task.estimated_time = data.estimated_time + if data.properties is not None: property_service = TaskMutation._get_property_service(db) await property_service.process_properties( diff --git a/backend/api/types/task.py b/backend/api/types/task.py index 6f57304..f7c445d 100644 --- a/backend/api/types/task.py +++ b/backend/api/types/task.py @@ -25,6 +25,8 @@ class TaskType: update_date: datetime | None assignee_id: strawberry.ID | None patient_id: strawberry.ID + priority: str | None + estimated_time: int | None @strawberry.field async def assignee( diff --git a/backend/database/migrations/versions/add_task_priority_and_estimated_time.py b/backend/database/migrations/versions/add_task_priority_and_estimated_time.py new file mode 100644 index 0000000..cc9d965 --- /dev/null +++ b/backend/database/migrations/versions/add_task_priority_and_estimated_time.py @@ -0,0 +1,33 @@ +"""Add priority and estimated_time to tasks. + +Revision ID: add_task_priority_time +Revises: 0de3078888ba +Create Date: 2025-12-19 00:00:00.000000 +""" + +from typing import Sequence, Union + +from alembic import op +import sqlalchemy as sa + + +revision: str = "add_task_priority_time" +down_revision: Union[str, Sequence[str], None] = "0de3078888ba" +branch_labels: Union[str, Sequence[str], None] = None +depends_on: Union[str, Sequence[str], None] = None + + +def upgrade() -> None: + op.add_column( + "tasks", + sa.Column("priority", sa.String(), nullable=True), + ) + op.add_column( + "tasks", + sa.Column("estimated_time", sa.Integer(), nullable=True), + ) + + +def downgrade() -> None: + op.drop_column("tasks", "estimated_time") + op.drop_column("tasks", "priority") diff --git a/backend/database/models/task.py b/backend/database/models/task.py index 8625e1e..79d8e64 100644 --- a/backend/database/models/task.py +++ b/backend/database/models/task.py @@ -5,7 +5,7 @@ from typing import TYPE_CHECKING from database.models.base import Base -from sqlalchemy import Boolean, Column, ForeignKey, String, Table +from sqlalchemy import Boolean, Column, ForeignKey, Integer, String, Table from sqlalchemy.orm import Mapped, mapped_column, relationship if TYPE_CHECKING: @@ -44,6 +44,8 @@ class Task(Base): nullable=True, ) patient_id: Mapped[str] = mapped_column(ForeignKey("patients.id")) + priority: Mapped[str | None] = mapped_column(String, nullable=True) + estimated_time: Mapped[int | None] = mapped_column(Integer, nullable=True) assignee: Mapped[User | None] = relationship( "User", diff --git a/web/api/gql/generated.ts b/web/api/gql/generated.ts index f6d0c5f..85059d8 100644 --- a/web/api/gql/generated.ts +++ b/web/api/gql/generated.ts @@ -51,8 +51,10 @@ export type CreateTaskInput = { assigneeId?: InputMaybe; description?: InputMaybe; dueDate?: InputMaybe; + estimatedTime?: InputMaybe; patientId: Scalars['ID']['input']; previousTaskIds?: InputMaybe>; + priority?: InputMaybe; properties?: InputMaybe>; title: Scalars['String']['input']; }; @@ -403,6 +405,13 @@ export type SubscriptionTaskUpdatedArgs = { taskId?: InputMaybe; }; +export enum TaskPriority { + P1 = 'P1', + P2 = 'P2', + P3 = 'P3', + P4 = 'P4' +} + export type TaskType = { __typename?: 'TaskType'; assignee?: Maybe; @@ -412,9 +421,11 @@ export type TaskType = { description?: Maybe; done: Scalars['Boolean']['output']; dueDate?: Maybe; + estimatedTime?: Maybe; id: Scalars['ID']['output']; patient: PatientType; patientId: Scalars['ID']['output']; + priority?: Maybe; properties: Array; title: Scalars['String']['output']; updateDate?: Maybe; @@ -454,7 +465,9 @@ export type UpdateTaskInput = { description?: InputMaybe; done?: InputMaybe; dueDate?: InputMaybe; + estimatedTime?: InputMaybe; previousTaskIds?: InputMaybe>; + priority?: InputMaybe; properties?: InputMaybe>; title?: InputMaybe; }; @@ -489,7 +502,7 @@ 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, 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 } | 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 }> }, assignee?: { __typename?: 'UserType', id: string, name: string, avatarUrl?: string | null } | null }> } | null }; export type GetOverviewDataQueryVariables = Exact<{ [key: string]: never; }>; @@ -501,7 +514,7 @@ export type GetPatientQueryVariables = Exact<{ }>; -export type GetPatientQuery = { __typename?: 'Query', patient?: { __typename?: 'PatientType', id: string, firstname: string, lastname: string, birthdate: any, sex: Sex, state: PatientState, 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, updateDate?: any | null, assignee?: { __typename?: 'UserType', id: string, name: string, avatarUrl?: string | null } | null }>, properties: Array<{ __typename?: 'PropertyValueType', textValue?: string | null, numberValue?: number | null, booleanValue?: boolean | null, dateValue?: any | null, dateTimeValue?: any | null, selectValue?: string | null, multiSelectValues?: Array | null, definition: { __typename?: 'PropertyDefinitionType', id: string, name: string, description?: string | null, fieldType: FieldType, isActive: boolean, allowedEntities: Array, options: Array } }> } | null }; +export type GetPatientQuery = { __typename?: 'Query', patient?: { __typename?: 'PatientType', id: string, firstname: string, lastname: string, birthdate: any, sex: Sex, state: PatientState, 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 } | null }>, properties: Array<{ __typename?: 'PropertyValueType', textValue?: string | null, numberValue?: number | null, booleanValue?: boolean | null, dateValue?: any | null, dateTimeValue?: any | null, selectValue?: string | null, multiSelectValues?: Array | null, definition: { __typename?: 'PropertyDefinitionType', id: string, name: string, description?: string | null, fieldType: FieldType, isActive: boolean, allowedEntities: Array, options: Array } }> } | null }; export type GetPatientsQueryVariables = Exact<{ locationId?: InputMaybe; @@ -510,14 +523,14 @@ export type GetPatientsQueryVariables = Exact<{ }>; -export type GetPatientsQuery = { __typename?: 'Query', 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, 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, creationDate: any, updateDate?: any | null, assignee?: { __typename?: 'UserType', id: string, name: string, avatarUrl?: string | null } | null }>, properties: Array<{ __typename?: 'PropertyValueType', textValue?: string | null, definition: { __typename?: 'PropertyDefinitionType', name: string } }> }> }; +export type GetPatientsQuery = { __typename?: 'Query', 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, 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, creationDate: any, updateDate?: any | null, assignee?: { __typename?: 'UserType', id: string, name: string, avatarUrl?: string | null } | null }>, properties: Array<{ __typename?: 'PropertyValueType', textValue?: string | null, definition: { __typename?: 'PropertyDefinitionType', name: string } }> }> }; 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, checksum: string, patient: { __typename?: 'PatientType', id: string, name: string }, assignee?: { __typename?: 'UserType', id: string, name: string } | null, properties: Array<{ __typename?: 'PropertyValueType', textValue?: string | null, numberValue?: number | null, booleanValue?: boolean | null, dateValue?: any | null, dateTimeValue?: any | null, selectValue?: string | null, multiSelectValues?: Array | null, definition: { __typename?: 'PropertyDefinitionType', id: string, name: string, description?: string | null, fieldType: FieldType, isActive: boolean, allowedEntities: Array, options: Array } }> } | 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, patient: { __typename?: 'PatientType', id: string, name: string }, assignee?: { __typename?: 'UserType', id: string, name: string } | null, properties: Array<{ __typename?: 'PropertyValueType', textValue?: string | null, numberValue?: number | null, booleanValue?: boolean | null, dateValue?: any | null, dateTimeValue?: any | null, selectValue?: string | null, multiSelectValues?: Array | null, definition: { __typename?: 'PropertyDefinitionType', id: string, name: string, description?: string | null, fieldType: FieldType, isActive: boolean, allowedEntities: Array, options: Array } }> } | null }; export type GetTasksQueryVariables = Exact<{ rootLocationIds?: InputMaybe | Scalars['ID']['input']>; @@ -525,7 +538,7 @@ export type GetTasksQueryVariables = Exact<{ }>; -export type GetTasksQuery = { __typename?: 'Query', tasks: Array<{ __typename?: 'TaskType', id: string, title: string, description?: string | null, done: boolean, dueDate?: any | 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 } | null }> }; +export type GetTasksQuery = { __typename?: 'Query', 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 } | null }> }; export type GetUsersQueryVariables = Exact<{ [key: string]: never; }>; @@ -683,7 +696,7 @@ export type UpdateTaskMutationVariables = Exact<{ }>; -export type UpdateTaskMutation = { __typename?: 'Mutation', updateTask: { __typename?: 'TaskType', id: string, title: string, description?: string | null, done: boolean, dueDate?: any | null, updateDate?: any | null, checksum: string, assignee?: { __typename?: 'UserType', id: string, name: string, avatarUrl?: string | null } | 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 }, assignee?: { __typename?: 'UserType', id: string, name: string, avatarUrl?: string | null } | null } }; export type AssignTaskMutationVariables = Exact<{ id: Scalars['ID']['input']; @@ -811,6 +824,8 @@ export const GetMyTasksDocument = ` description done dueDate + priority + estimatedTime creationDate updateDate patient { @@ -1016,6 +1031,8 @@ export const GetPatientDocument = ` description done dueDate + priority + estimatedTime updateDate assignee { id @@ -1169,6 +1186,8 @@ export const GetPatientsDocument = ` description done dueDate + priority + estimatedTime creationDate updateDate assignee { @@ -1211,6 +1230,8 @@ export const GetTaskDocument = ` description done dueDate + priority + estimatedTime checksum patient { id @@ -1266,6 +1287,8 @@ export const GetTasksDocument = ` description done dueDate + priority + estimatedTime creationDate updateDate patient { @@ -1819,8 +1842,14 @@ export const UpdateTaskDocument = ` description done dueDate + priority + estimatedTime updateDate checksum + patient { + id + name + } assignee { id name diff --git a/web/api/graphql/GetMyTasks.graphql b/web/api/graphql/GetMyTasks.graphql index 8ea0c6d..2342e52 100644 --- a/web/api/graphql/GetMyTasks.graphql +++ b/web/api/graphql/GetMyTasks.graphql @@ -7,6 +7,8 @@ query GetMyTasks { description done dueDate + priority + estimatedTime creationDate updateDate patient { diff --git a/web/api/graphql/GetPatient.graphql b/web/api/graphql/GetPatient.graphql index f05cf6d..258e2da 100644 --- a/web/api/graphql/GetPatient.graphql +++ b/web/api/graphql/GetPatient.graphql @@ -84,6 +84,8 @@ query GetPatient($id: ID!) { description done dueDate + priority + estimatedTime updateDate assignee { id diff --git a/web/api/graphql/GetPatients.graphql b/web/api/graphql/GetPatients.graphql index 17bc1a0..1fbff92 100644 --- a/web/api/graphql/GetPatients.graphql +++ b/web/api/graphql/GetPatients.graphql @@ -101,6 +101,8 @@ query GetPatients($locationId: ID, $rootLocationIds: [ID!], $states: [PatientSta description done dueDate + priority + estimatedTime creationDate updateDate assignee { diff --git a/web/api/graphql/GetTask.graphql b/web/api/graphql/GetTask.graphql index 3c8c2d3..12c14aa 100644 --- a/web/api/graphql/GetTask.graphql +++ b/web/api/graphql/GetTask.graphql @@ -5,6 +5,8 @@ query GetTask($id: ID!) { description done dueDate + priority + estimatedTime checksum patient { id diff --git a/web/api/graphql/GetTasks.graphql b/web/api/graphql/GetTasks.graphql index ec6f7b8..ffcff1c 100644 --- a/web/api/graphql/GetTasks.graphql +++ b/web/api/graphql/GetTasks.graphql @@ -5,6 +5,8 @@ query GetTasks($rootLocationIds: [ID!], $assigneeId: ID) { description done dueDate + priority + estimatedTime creationDate updateDate patient { diff --git a/web/api/graphql/TaskMutations.graphql b/web/api/graphql/TaskMutations.graphql index ea5e944..9e6d30f 100644 --- a/web/api/graphql/TaskMutations.graphql +++ b/web/api/graphql/TaskMutations.graphql @@ -25,8 +25,14 @@ mutation UpdateTask($id: ID!, $data: UpdateTaskInput!) { description done dueDate + priority + estimatedTime updateDate checksum + patient { + id + name + } assignee { id name diff --git a/web/components/tasks/TaskCardView.tsx b/web/components/tasks/TaskCardView.tsx index 1f7a44a..b923429 100644 --- a/web/components/tasks/TaskCardView.tsx +++ b/web/components/tasks/TaskCardView.tsx @@ -14,6 +14,8 @@ type FlexibleTask = { description?: string | null, done: boolean, dueDate?: Date | string | null, + priority?: string | null, + estimatedTime?: number | null, updateDate?: Date | string | null, patient?: { id: string, @@ -111,13 +113,24 @@ export const TaskCardView = ({ task, onToggleDone: _onToggleDone, onClick, showA
-
+ {(task as FlexibleTask).priority && ( +
)} - > - {taskName} +
+ {taskName} +
{task.assignee && (
@@ -156,12 +169,24 @@ export const TaskCardView = ({ task, onToggleDone: _onToggleDone, onClick, showA )}
)} - {dueDate && ( -
- - -
- )} +
+ {(task as FlexibleTask).estimatedTime && ( +
+ + + {(task as FlexibleTask).estimatedTime! < 60 + ? `${(task as FlexibleTask).estimatedTime}m` + : `${Math.floor((task as FlexibleTask).estimatedTime! / 60)}h ${(task as FlexibleTask).estimatedTime! % 60}m`} + +
+ )} + {dueDate && ( +
+ + +
+ )} +
) } diff --git a/web/components/tasks/TaskDetailView.tsx b/web/components/tasks/TaskDetailView.tsx index 16ebd68..dba8b16 100644 --- a/web/components/tasks/TaskDetailView.tsx +++ b/web/components/tasks/TaskDetailView.tsx @@ -1,6 +1,7 @@ -import { useEffect, useState, useMemo } from 'react' +import { useEffect, useState, useMemo, useRef } from 'react' +import { useQueryClient } from '@tanstack/react-query' import { useTasksTranslation } from '@/i18n/useTasksTranslation' -import type { CreateTaskInput, UpdateTaskInput } from '@/api/gql/generated' +import type { CreateTaskInput, UpdateTaskInput, TaskPriority } from '@/api/gql/generated' import { useAssignTaskMutation, useCreateTaskMutation, @@ -11,7 +12,8 @@ import { useGetUsersQuery, useUnassignTaskMutation, useUpdateTaskMutation, - PropertyEntity + PropertyEntity, + type GetTaskQuery } from '@/api/gql/generated' import { Button, @@ -45,6 +47,7 @@ interface TaskDetailViewProps { export const TaskDetailView = ({ taskId, onClose, onSuccess, initialPatientId }: TaskDetailViewProps) => { const translation = useTasksTranslation() const { selectedLocationId } = useTasksContext() + const queryClient = useQueryClient() const [isShowingPatientDialog, setIsShowingPatientDialog] = useState(false) const [isDeleteDialogOpen, setIsDeleteDialogOpen] = useState(false) const isEditMode = !!taskId @@ -52,6 +55,10 @@ export const TaskDetailView = ({ taskId, onClose, onSuccess, initialPatientId }: const [titleError, setTitleError] = useState(null) const [patientIdError, setPatientIdError] = useState(null) + const dirtyFieldsRef = useRef>(new Set()) + const lastServerUpdateRef = useRef(0) + const fieldUpdateTimestampsRef = useRef>(new Map()) + const { data: taskData, isLoading: isLoadingTask, refetch } = useGetTaskQuery( { id: taskId! }, { @@ -95,8 +102,47 @@ export const TaskDetailView = ({ taskId, onClose, onSuccess, initialPatientId }: }) const { mutate: updateTask } = useUpdateTaskMutation({ - onSuccess: () => { - refetch() + onSuccess: (data, variables) => { + if (data?.updateTask && taskId) { + queryClient.setQueryData( + ['GetTask', { id: taskId }], + (oldData) => { + if (!oldData?.task) { + return { task: data.updateTask } as GetTaskQuery + } + const mergedTask = { + ...oldData.task, + ...data.updateTask, + patient: data.updateTask.patient || oldData.task.patient, + assignee: data.updateTask.assignee || oldData.task.assignee, + } + if ('priority' in data.updateTask) { + mergedTask.priority = data.updateTask.priority + } else { + mergedTask.priority = oldData.task.priority + } + if ('estimatedTime' in data.updateTask) { + mergedTask.estimatedTime = data.updateTask.estimatedTime + } else { + mergedTask.estimatedTime = oldData.task.estimatedTime + } + if (oldData.task.properties) { + mergedTask.properties = oldData.task.properties + } + return { task: mergedTask } as GetTaskQuery + } + ) + const now = Date.now() + lastServerUpdateRef.current = now + if (variables?.data) { + Object.keys(variables.data).forEach(key => { + if (variables.data[key as keyof UpdateTaskInput] !== undefined) { + fieldUpdateTimestampsRef.current.set(key, now) + dirtyFieldsRef.current.delete(key) + } + }) + } + } onSuccess() } }) @@ -122,25 +168,92 @@ export const TaskDetailView = ({ taskId, onClose, onSuccess, initialPatientId }: patientId: initialPatientId || '', assigneeId: null, dueDate: null, + priority: null, + estimatedTime: null, done: false, }) useEffect(() => { - if (taskData?.task) { - setFormData({ - title: taskData.task.title, - description: taskData.task.description || '', - patientId: taskData.task.patient?.id || '', - assigneeId: taskData.task.assignee?.id || null, - dueDate: taskData.task.dueDate ? new Date(taskData.task.dueDate) : null, - done: taskData.task.done || false + if (taskData?.task && isEditMode) { + const task = taskData.task + const updateTime = Date.now() + const GRACE_PERIOD_MS = 5000 + + if (updateTime <= lastServerUpdateRef.current + 100) { + return + } + + setFormData(prev => { + const newPriority = (task.priority as TaskPriority | null) || null + const newEstimatedTime = task.estimatedTime ?? null + const newDueDate = task.dueDate ? new Date(task.dueDate) : null + + const shouldPreserveField = (fieldName: string): boolean => { + const lastUpdate = fieldUpdateTimestampsRef.current.get(fieldName) + if (lastUpdate && (updateTime - lastUpdate) < GRACE_PERIOD_MS) { + return true + } + return dirtyFieldsRef.current.has(fieldName) + } + + const hasDirtyFields = dirtyFieldsRef.current.size > 0 || Array.from(fieldUpdateTimestampsRef.current.values()).some(ts => (updateTime - ts) < GRACE_PERIOD_MS) + + if (hasDirtyFields) { + const merged: Partial = { + title: shouldPreserveField('title') ? prev.title : task.title, + description: shouldPreserveField('description') ? prev.description : (task.description || ''), + patientId: shouldPreserveField('patientId') ? prev.patientId : (task.patient?.id || ''), + assigneeId: shouldPreserveField('assigneeId') ? prev.assigneeId : (task.assignee?.id || null), + dueDate: shouldPreserveField('dueDate') ? prev.dueDate : newDueDate, + priority: shouldPreserveField('priority') ? prev.priority : newPriority, + estimatedTime: shouldPreserveField('estimatedTime') ? prev.estimatedTime : newEstimatedTime, + done: shouldPreserveField('done') ? prev.done : (task.done || false), + } + return { ...prev, ...merged } + } + + const priorityChanged = prev.priority !== newPriority + const estimatedTimeChanged = prev.estimatedTime !== newEstimatedTime + const shouldPreservePriority = shouldPreserveField('priority') + const shouldPreserveEstimatedTime = shouldPreserveField('estimatedTime') + + if ( + prev.title === task.title && + prev.description === (task.description || '') && + prev.patientId === (task.patient?.id || '') && + prev.assigneeId === (task.assignee?.id || null) && + prev.dueDate?.getTime() === newDueDate?.getTime() && + (priorityChanged ? shouldPreservePriority : prev.priority === newPriority) && + (estimatedTimeChanged ? shouldPreserveEstimatedTime : prev.estimatedTime === newEstimatedTime) && + prev.done === task.done + ) { + return prev + } + + return { + title: task.title, + description: task.description || '', + patientId: task.patient?.id || '', + assigneeId: task.assignee?.id || null, + dueDate: newDueDate, + priority: shouldPreservePriority ? prev.priority : newPriority, + estimatedTime: shouldPreserveEstimatedTime ? prev.estimatedTime : newEstimatedTime, + done: task.done || false + } }) } else if (initialPatientId && !taskId) { setFormData(prev => ({ ...prev, patientId: initialPatientId })) } - }, [taskData, initialPatientId, taskId]) - - const updateLocalState = (updates: Partial) => { + }, [taskData?.task, isEditMode, initialPatientId, taskId]) + + const updateLocalState = (updates: Partial, markDirty = false) => { + if (markDirty) { + Object.keys(updates).forEach(key => { + if (updates[key as keyof CreateTaskInput] !== undefined) { + dirtyFieldsRef.current.add(key) + } + }) + } setFormData(prev => ({ ...prev, ...updates })) } @@ -173,9 +286,43 @@ export const TaskDetailView = ({ taskId, onClose, onSuccess, initialPatientId }: if (isEditMode && taskId) { if (updates.title !== undefined && !updates.title?.trim()) return + Object.keys(updates).forEach(key => { + if (updates[key as keyof UpdateTaskInput] !== undefined) { + dirtyFieldsRef.current.add(key) + } + }) + + const now = Date.now() + Object.keys(updates).forEach(key => { + if (updates[key as keyof UpdateTaskInput] !== undefined) { + fieldUpdateTimestampsRef.current.set(key, now) + } + }) + updateTask({ id: taskId, data: updates as UpdateTaskInput + }, { + onSuccess: (data) => { + if (data?.updateTask) { + setFormData(prev => ({ + ...prev, + ...(updates.title !== undefined && { title: data.updateTask.title }), + ...(updates.description !== undefined && { description: data.updateTask.description || '' }), + ...(updates.dueDate !== undefined && { + dueDate: data.updateTask.dueDate ? new Date(data.updateTask.dueDate) : null + }), + ...(updates.priority !== undefined && { + priority: (data.updateTask.priority as TaskPriority | null) || null + }), + ...(updates.estimatedTime !== undefined && { + estimatedTime: data.updateTask.estimatedTime ?? null + }), + ...(updates.done !== undefined && { done: data.updateTask.done || false }), + patientId: data.updateTask.patient?.id || prev.patientId, + })) + } + } }) } } @@ -209,8 +356,10 @@ export const TaskDetailView = ({ taskId, onClose, onSuccess, initialPatientId }: description: formData.description, assigneeId: formData.assigneeId, dueDate: formData.dueDate, + priority: (formData.priority as TaskPriority | null) || undefined, + estimatedTime: formData.estimatedTime, properties: formData.properties - } as CreateTaskInput + } as CreateTaskInput & { priority?: TaskPriority | null, estimatedTime?: number | null } }) } @@ -259,7 +408,7 @@ export const TaskDetailView = ({ taskId, onClose, onSuccess, initialPatientId }: placeholder={translation('taskTitlePlaceholder')} required onChange={e => { - updateLocalState({ title: e.target.value }) + updateLocalState({ title: e.target.value }, true) if (isShowingError) { validateTitle(e.target.value) } @@ -340,7 +489,7 @@ export const TaskDetailView = ({ taskId, onClose, onSuccess, initialPatientId }: date={formData.dueDate ? new Date(formData.dueDate) : undefined} mode="dateTime" onValueChange={(date) => { - updateLocalState({ dueDate: date }) + updateLocalState({ dueDate: date }, true) }} onEditCompleted={(date) => { updateLocalState({ dueDate: date }) @@ -354,13 +503,56 @@ export const TaskDetailView = ({ taskId, onClose, onSuccess, initialPatientId }: )} + + {({ isShowingError: _1, setIsShowingError: _2, ...bag }) => ( + + )} + + + + {({ isShowingError: _1, setIsShowingError: _2, ...bag }) => ( + { + const value = e.target.value === '' ? null : parseInt(e.target.value, 10) + updateLocalState({ estimatedTime: isNaN(value as number) ? null : value }, true) + }} + onBlur={() => { + persistChanges({ + estimatedTime: formData.estimatedTime, + priority: formData.priority + }) + }} + /> + )} + + {({ isShowingError: _1, setIsShowingError: _2, ...bag }) => (