From 29c6692a799e0f8cd9f192bdf68840fbf450bf8f Mon Sep 17 00:00:00 2001 From: Felix Thape Date: Thu, 12 Sep 2024 13:41:01 +0200 Subject: [PATCH 01/36] chore: move service APIs to the service package --- .../tasks/lib/components/assignee_select.dart | 4 +- .../lib/components/patient_bottom_sheet.dart | 379 +++++++++--------- apps/tasks/lib/components/patient_card.dart | 2 +- .../patient_status_chip_select.dart | 2 +- apps/tasks/lib/components/subtask_list.dart | 18 +- .../lib/components/task_bottom_sheet.dart | 9 +- apps/tasks/lib/components/task_card.dart | 2 +- .../lib/components/task_expansion_tile.dart | 3 +- .../lib/components/user_bottom_sheet.dart | 9 +- apps/tasks/lib/components/user_header.dart | 6 +- .../lib/components/visibility_select.dart | 1 - apps/tasks/lib/config/config.dart | 2 +- apps/tasks/lib/dataclasses/subtask.dart | 14 - apps/tasks/lib/debug/theme_visualizer.dart | 4 +- apps/tasks/lib/main.dart | 9 +- apps/tasks/lib/screens/landing_screen.dart | 6 +- apps/tasks/lib/screens/login_screen.dart | 2 +- apps/tasks/lib/screens/main_screen.dart | 6 +- .../my_tasks_screen.dart | 3 +- .../patient_screen.dart | 4 +- apps/tasks/lib/screens/settings_screen.dart | 3 +- .../tasks/lib/screens/ward_select_screen.dart | 10 +- apps/tasks/lib/services/grpc_client_svc.dart | 76 ---- apps/tasks/lib/services/task_svc.dart | 197 --------- apps/tasks/lib/util/task_status_mapping.dart | 16 - apps/tasks/pubspec.lock | 60 +-- apps/tasks/pubspec.yaml | 4 +- packages/helpwave_service/lib/auth.dart | 5 +- .../assignee_select_controller.dart | 10 +- .../lib/src/api/tasks/controllers/index.dart | 6 + .../controllers/my_tasks_controller.dart | 11 +- .../controllers/patient_controller.dart | 7 +- .../controllers/subtask_list_controller.dart | 48 +-- .../tasks}/controllers/task_controller.dart | 6 +- .../controllers/ward_patients_controller.dart | 11 +- .../lib/src/api/tasks/data_types}/bed.dart | 2 +- .../lib/src/api/tasks/data_types/index.dart | 8 + .../src/api/tasks/data_types}/patient.dart | 4 +- .../lib/src/api/tasks/data_types}/room.dart | 2 +- .../lib/src/api/tasks/data_types/subtask.dart | 27 ++ .../lib/src/api/tasks/data_types}/task.dart | 6 +- .../api/tasks/data_types}/task_template.dart | 2 +- .../data_types}/task_template_subtask.dart | 0 .../lib/src/api/tasks/data_types}/ward.dart | 0 .../lib/src/api/tasks/index.dart | 4 + .../lib/src/api/tasks/services/bed_svc.dart | 0 .../lib/src/api/tasks/services/index.dart | 5 + .../src/api/tasks}/services/patient_svc.dart | 63 +-- .../lib/src/api/tasks}/services/room_svc.dart | 14 +- .../lib/src/api/tasks/services/task_svc.dart | 163 ++++++++ .../src/api/tasks}/services/ward_service.dart | 23 +- .../lib/src/api/tasks/tasks_api_services.dart | 42 ++ .../api/tasks/util/task_status_mapping.dart | 30 ++ .../lib/src/api/user/controllers/index.dart | 1 + .../user}/controllers/user_controller.dart | 5 +- .../lib/src/api/user/data_types/index.dart | 2 + .../api/user/data_types}/organization.dart | 0 .../lib/src/api/user/data_types}/user.dart | 0 .../lib/src/api/user/index.dart | 4 + .../lib/src/api/user/services/index.dart | 2 + .../api/user}/services/organization_svc.dart | 20 +- .../src/api/user}/services/user_service.dart | 15 +- .../lib/src/api/user/user_api_services.dart | 36 ++ .../lib/src/auth/authentication_utility.dart | 18 + .../lib/src/auth}/current_ward_svc.dart | 12 +- .../helpwave_service/lib/src/auth/index.dart | 6 + .../src/auth}/user_session_controller.dart | 2 +- .../lib/src/auth}/user_session_service.dart | 10 +- packages/helpwave_service/lib/tasks.dart | 1 + packages/helpwave_service/lib/user.dart | 1 + packages/helpwave_service/pubspec.yaml | 7 +- .../lib/src/{ => theme}/dark_theme.dart | 4 +- .../lib/src/{ => theme}/light_theme.dart | 4 +- .../lib/src/{ => theme}/theme.dart | 2 +- .../lib/src/util/context_extension.dart | 5 + .../helpwave_theme/lib/src/util/index.dart | 2 + .../material_state_color_resolver.dart | 0 packages/helpwave_theme/lib/theme.dart | 4 +- packages/helpwave_theme/lib/util.dart | 2 +- packages/helpwave_util/lib/loading_state.dart | 1 + packages/helpwave_util/lib/search.dart | 1 + .../lib/src/loading_state/type.dart | 16 + .../lib/src/search}/search_helpers.dart | 0 .../src/loading/loading_and_error_widget.dart | 18 +- .../src/loading/loading_future_builder.dart | 1 + packages/helpwave_widget/pubspec.yaml | 1 + 86 files changed, 764 insertions(+), 789 deletions(-) delete mode 100644 apps/tasks/lib/dataclasses/subtask.dart delete mode 100644 apps/tasks/lib/services/grpc_client_svc.dart delete mode 100644 apps/tasks/lib/services/task_svc.dart delete mode 100644 apps/tasks/lib/util/task_status_mapping.dart rename {apps/tasks/lib => packages/helpwave_service/lib/src/api/tasks}/controllers/assignee_select_controller.dart (86%) create mode 100644 packages/helpwave_service/lib/src/api/tasks/controllers/index.dart rename {apps/tasks/lib => packages/helpwave_service/lib/src/api/tasks}/controllers/my_tasks_controller.dart (81%) rename {apps/tasks/lib => packages/helpwave_service/lib/src/api/tasks}/controllers/patient_controller.dart (95%) rename {apps/tasks/lib => packages/helpwave_service/lib/src/api/tasks}/controllers/subtask_list_controller.dart (66%) rename {apps/tasks/lib => packages/helpwave_service/lib/src/api/tasks}/controllers/task_controller.dart (96%) rename {apps/tasks/lib => packages/helpwave_service/lib/src/api/tasks}/controllers/ward_patients_controller.dart (86%) rename {apps/tasks/lib/dataclasses => packages/helpwave_service/lib/src/api/tasks/data_types}/bed.dart (84%) create mode 100644 packages/helpwave_service/lib/src/api/tasks/data_types/index.dart rename {apps/tasks/lib/dataclasses => packages/helpwave_service/lib/src/api/tasks/data_types}/patient.dart (94%) rename {apps/tasks/lib/dataclasses => packages/helpwave_service/lib/src/api/tasks/data_types}/room.dart (94%) create mode 100644 packages/helpwave_service/lib/src/api/tasks/data_types/subtask.dart rename {apps/tasks/lib/dataclasses => packages/helpwave_service/lib/src/api/tasks/data_types}/task.dart (93%) rename {apps/tasks/lib/dataclasses => packages/helpwave_service/lib/src/api/tasks/data_types}/task_template.dart (86%) rename {apps/tasks/lib/dataclasses => packages/helpwave_service/lib/src/api/tasks/data_types}/task_template_subtask.dart (100%) rename {apps/tasks/lib/dataclasses => packages/helpwave_service/lib/src/api/tasks/data_types}/ward.dart (100%) create mode 100644 packages/helpwave_service/lib/src/api/tasks/index.dart create mode 100644 packages/helpwave_service/lib/src/api/tasks/services/bed_svc.dart create mode 100644 packages/helpwave_service/lib/src/api/tasks/services/index.dart rename {apps/tasks/lib => packages/helpwave_service/lib/src/api/tasks}/services/patient_svc.dart (77%) rename {apps/tasks/lib => packages/helpwave_service/lib/src/api/tasks}/services/room_svc.dart (72%) create mode 100644 packages/helpwave_service/lib/src/api/tasks/services/task_svc.dart rename {apps/tasks/lib => packages/helpwave_service/lib/src/api/tasks}/services/ward_service.dart (59%) create mode 100644 packages/helpwave_service/lib/src/api/tasks/tasks_api_services.dart create mode 100644 packages/helpwave_service/lib/src/api/tasks/util/task_status_mapping.dart create mode 100644 packages/helpwave_service/lib/src/api/user/controllers/index.dart rename {apps/tasks/lib => packages/helpwave_service/lib/src/api/user}/controllers/user_controller.dart (91%) create mode 100644 packages/helpwave_service/lib/src/api/user/data_types/index.dart rename {apps/tasks/lib/dataclasses => packages/helpwave_service/lib/src/api/user/data_types}/organization.dart (100%) rename {apps/tasks/lib/dataclasses => packages/helpwave_service/lib/src/api/user/data_types}/user.dart (100%) create mode 100644 packages/helpwave_service/lib/src/api/user/index.dart create mode 100644 packages/helpwave_service/lib/src/api/user/services/index.dart rename {apps/tasks/lib => packages/helpwave_service/lib/src/api/user}/services/organization_svc.dart (74%) rename {apps/tasks/lib => packages/helpwave_service/lib/src/api/user}/services/user_service.dart (59%) create mode 100644 packages/helpwave_service/lib/src/api/user/user_api_services.dart create mode 100644 packages/helpwave_service/lib/src/auth/authentication_utility.dart rename {apps/tasks/lib/services => packages/helpwave_service/lib/src/auth}/current_ward_svc.dart (95%) create mode 100644 packages/helpwave_service/lib/src/auth/index.dart rename {apps/tasks/lib/controllers => packages/helpwave_service/lib/src/auth}/user_session_controller.dart (94%) rename {apps/tasks/lib/services => packages/helpwave_service/lib/src/auth}/user_session_service.dart (87%) create mode 100644 packages/helpwave_service/lib/tasks.dart create mode 100644 packages/helpwave_service/lib/user.dart rename packages/helpwave_theme/lib/src/{ => theme}/dark_theme.dart (97%) rename packages/helpwave_theme/lib/src/{ => theme}/light_theme.dart (97%) rename packages/helpwave_theme/lib/src/{ => theme}/theme.dart (99%) create mode 100644 packages/helpwave_theme/lib/src/util/context_extension.dart create mode 100644 packages/helpwave_theme/lib/src/util/index.dart rename packages/helpwave_theme/lib/src/{ => util}/material_state_color_resolver.dart (100%) create mode 100644 packages/helpwave_util/lib/loading_state.dart create mode 100644 packages/helpwave_util/lib/search.dart create mode 100644 packages/helpwave_util/lib/src/loading_state/type.dart rename {apps/tasks/lib/util => packages/helpwave_util/lib/src/search}/search_helpers.dart (100%) diff --git a/apps/tasks/lib/components/assignee_select.dart b/apps/tasks/lib/components/assignee_select.dart index d04bd06f..311999fb 100644 --- a/apps/tasks/lib/components/assignee_select.dart +++ b/apps/tasks/lib/components/assignee_select.dart @@ -1,11 +1,11 @@ import 'package:flutter/material.dart'; import 'package:helpwave_localization/localization.dart'; +import 'package:helpwave_service/tasks.dart'; +import 'package:helpwave_service/user.dart'; import 'package:helpwave_theme/constants.dart'; import 'package:helpwave_widget/bottom_sheets.dart'; import 'package:helpwave_widget/loading.dart'; import 'package:provider/provider.dart'; -import 'package:tasks/controllers/assignee_select_controller.dart'; -import 'package:tasks/dataclasses/user.dart'; /// A [BottomSheet] for selecting a assignee class AssigneeSelect extends StatelessWidget { diff --git a/apps/tasks/lib/components/patient_bottom_sheet.dart b/apps/tasks/lib/components/patient_bottom_sheet.dart index a15015e0..12b6e91e 100644 --- a/apps/tasks/lib/components/patient_bottom_sheet.dart +++ b/apps/tasks/lib/components/patient_bottom_sheet.dart @@ -1,5 +1,6 @@ import 'package:flutter/material.dart'; import 'package:helpwave_localization/localization.dart'; +import 'package:helpwave_service/auth.dart'; import 'package:helpwave_theme/constants.dart'; import 'package:helpwave_theme/util.dart'; import 'package:helpwave_widget/bottom_sheets.dart'; @@ -10,13 +11,8 @@ import 'package:helpwave_widget/text_input.dart'; import 'package:provider/provider.dart'; import 'package:tasks/components/task_bottom_sheet.dart'; import 'package:tasks/components/task_expansion_tile.dart'; -import 'package:tasks/controllers/patient_controller.dart'; -import 'package:tasks/dataclasses/bed.dart'; -import 'package:tasks/dataclasses/patient.dart'; -import 'package:tasks/dataclasses/room.dart'; -import 'package:tasks/dataclasses/task.dart'; -import 'package:tasks/services/current_ward_svc.dart'; -import 'package:tasks/services/room_svc.dart'; +import 'package:helpwave_service/tasks.dart'; +import 'package:helpwave_util/loading_state.dart'; /// A [BottomSheet] for showing [Patient] information and [Task]s for that [Patient] class PatientBottomSheet extends StatefulWidget { @@ -83,194 +79,199 @@ class _PatientBottomSheetState extends State { return LoadingAndErrorWidget( state: patientController.state, child: Row( - mainAxisAlignment: patientController.isCreating ? MainAxisAlignment.end : MainAxisAlignment.spaceBetween, - children: patientController.isCreating - ? [ - TextButton( - style: buttonStyleBig, - onPressed: patientController.create, - child: Text(context.localization!.create), - ) - ] - : [ - SizedBox( - width: width * 0.4, - // TODO make this state checking easier and more readable - child: TextButton( - onPressed: patientController.patient.isUnassigned - ? null - : () { - patientController.unassign(); - }, - style: buttonStyleMedium.copyWith( - backgroundColor: resolveByStatesAndContextBackground( - context: context, - defaultValue: inProgressColor, - ), - foregroundColor: resolveByStatesAndContextForeground( - context: context, - ), - ), - child: Text(context.localization!.unassigne), - ), - ), - SizedBox( - width: width * 0.4, - child: TextButton( - // TODO check whether the patient is active - onPressed: patientController.patient.isDischarged ? null : () { - showDialog( - context: context, - builder: (context) => AcceptDialog(titleText: context.localization!.dischargePatient), - ).then((value) { - if (value) { - patientController.discharge(); - Navigator.of(context).pop(); - } - }); - }, - style: buttonStyleMedium.copyWith( - backgroundColor: resolveByStatesAndContextBackground( - context: context, - defaultValue: negativeColor, - ), - foregroundColor: resolveByStatesAndContextForeground( - context: context, - ), - ), - child: Text(context.localization!.discharge), - ), - ), - ], - )); - } ), - ), builder: (BuildContext context) => Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Center( - child: Consumer(builder: (context, patientController, _) { - return LoadingFutureBuilder( - future: loadRoomsWithBeds(patientController.patient.id), - // TODO use a better loading widget - loadingWidget: const SizedBox(), - thenWidgetBuilder: (context, beds) { - if (beds.isEmpty) { - return Text( - context.localization!.noFreeBeds, - style: TextStyle(color: Theme.of(context).disabledColor, fontWeight: FontWeight.bold), - ); - } - return DropdownButtonHideUnderline( - child: DropdownButton( - iconEnabledColor: Theme.of(context).colorScheme.secondary.withOpacity(0.6), - padding: EdgeInsets.zero, - isDense: true, - hint: Text( - context.localization!.assignBed, - style: TextStyle(color: Theme.of(context).colorScheme.secondary.withOpacity(0.6)), - ), - value: !patientController.patient.isUnassigned - ? RoomWithBedFlat( - room: patientController.patient.room!, bed: patientController.patient.bed!) - : null, - items: beds - .map((roomWithBed) => DropdownMenuItem( - value: roomWithBed, - child: Text( - "${roomWithBed.room.name} - ${roomWithBed.bed.name}", - style: TextStyle(color: Theme.of(context).colorScheme.primary.withOpacity(0.6)), + mainAxisAlignment: + patientController.isCreating ? MainAxisAlignment.end : MainAxisAlignment.spaceBetween, + children: patientController.isCreating + ? [ + TextButton( + style: buttonStyleBig, + onPressed: patientController.create, + child: Text(context.localization!.create), + ) + ] + : [ + SizedBox( + width: width * 0.4, + // TODO make this state checking easier and more readable + child: TextButton( + onPressed: patientController.patient.isUnassigned + ? null + : () { + patientController.unassign(); + }, + style: buttonStyleMedium.copyWith( + backgroundColor: resolveByStatesAndContextBackground( + context: context, + defaultValue: inProgressColor, + ), + foregroundColor: resolveByStatesAndContextForeground( + context: context, + ), + ), + child: Text(context.localization!.unassigne), + ), + ), + SizedBox( + width: width * 0.4, + child: TextButton( + // TODO check whether the patient is active + onPressed: patientController.patient.isDischarged + ? null + : () { + showDialog( + context: context, + builder: (context) => + AcceptDialog(titleText: context.localization!.dischargePatient), + ).then((value) { + if (value) { + patientController.discharge(); + Navigator.of(context).pop(); + } + }); + }, + style: buttonStyleMedium.copyWith( + backgroundColor: resolveByStatesAndContextBackground( + context: context, + defaultValue: negativeColor, + ), + foregroundColor: resolveByStatesAndContextForeground( + context: context, + ), + ), + child: Text(context.localization!.discharge), + ), + ), + ], + )); + }), + ), + builder: (BuildContext context) => Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Center( + child: Consumer(builder: (context, patientController, _) { + return LoadingFutureBuilder( + future: loadRoomsWithBeds(patientController.patient.id), + // TODO use a better loading widget + loadingWidget: const SizedBox(), + thenWidgetBuilder: (context, beds) { + if (beds.isEmpty) { + return Text( + context.localization!.noFreeBeds, + style: TextStyle(color: Theme.of(context).disabledColor, fontWeight: FontWeight.bold), + ); + } + return DropdownButtonHideUnderline( + child: DropdownButton( + iconEnabledColor: Theme.of(context).colorScheme.secondary.withOpacity(0.6), + padding: EdgeInsets.zero, + isDense: true, + hint: Text( + context.localization!.assignBed, + style: TextStyle(color: Theme.of(context).colorScheme.secondary.withOpacity(0.6)), ), - )) - .toList(), - onChanged: (RoomWithBedFlat? value) { - // TODO later unassign here - if (value == null) { - return; - } - patientController.changeBed(value.room, value.bed); - }, - ), - ); - }, - ); - }), - ), - Text( - context.localization!.notes, - style: const TextStyle(fontSize: fontSizeBig, fontWeight: FontWeight.bold), - ), - const SizedBox(height: distanceSmall), - Consumer( - builder: (context, patientController, _) => - patientController.state == LoadingState.loaded || patientController.isCreating - ? TextFormFieldWithTimer( - initialValue: patientController.patient.notes, - maxLines: 6, - onUpdate: patientController.changeNotes, - ) - : TextFormField(maxLines: 6), - ), - Padding( - padding: const EdgeInsets.symmetric(vertical: paddingMedium), - child: Consumer(builder: (context, patientController, _) { - Patient patient = patientController.patient; - return AddList( - maxHeight: width * 0.5, - items: [ - ...patient.unscheduledTasks, - ...patient.inProgressTasks, - ...patient.doneTasks, - ], - itemBuilder: (_, index, taskList) { - if (index == 0) { - return TaskExpansionTile( - tasks: patient.unscheduledTasks - .map((task) => TaskWithPatient.fromTaskAndPatient( - task: task, - patient: patient, - )) - .toList(), - title: context.localization!.upcoming, - color: upcomingColor, + value: !patientController.patient.isUnassigned + ? RoomWithBedFlat( + room: patientController.patient.room!, bed: patientController.patient.bed!) + : null, + items: beds + .map((roomWithBed) => DropdownMenuItem( + value: roomWithBed, + child: Text( + "${roomWithBed.room.name} - ${roomWithBed.bed.name}", + style: TextStyle(color: Theme.of(context).colorScheme.primary.withOpacity(0.6)), + ), + )) + .toList(), + onChanged: (RoomWithBedFlat? value) { + // TODO later unassign here + if (value == null) { + return; + } + patientController.changeBed(value.room, value.bed); + }, + ), ); - } - if (index == 2) { + }, + ); + }), + ), + Text( + context.localization!.notes, + style: const TextStyle(fontSize: fontSizeBig, fontWeight: FontWeight.bold), + ), + const SizedBox(height: distanceSmall), + Consumer( + builder: (context, patientController, _) => + patientController.state == LoadingState.loaded || patientController.isCreating + ? TextFormFieldWithTimer( + initialValue: patientController.patient.notes, + maxLines: 6, + onUpdate: patientController.changeNotes, + ) + : TextFormField(maxLines: 6), + ), + Padding( + padding: const EdgeInsets.symmetric(vertical: paddingMedium), + child: Consumer(builder: (context, patientController, _) { + Patient patient = patientController.patient; + return AddList( + maxHeight: width * 0.5, + items: [ + ...patient.unscheduledTasks, + ...patient.inProgressTasks, + ...patient.doneTasks, + ], + itemBuilder: (_, index, taskList) { + if (index == 0) { + return TaskExpansionTile( + tasks: patient.unscheduledTasks + .map((task) => TaskWithPatient.fromTaskAndPatient( + task: task, + patient: patient, + )) + .toList(), + title: context.localization!.upcoming, + color: upcomingColor, + ); + } + if (index == 2) { + return TaskExpansionTile( + tasks: patient.doneTasks + .map((task) => TaskWithPatient.fromTaskAndPatient( + task: task, + patient: patient, + )) + .toList(), + title: context.localization!.inProgress, + color: inProgressColor, + ); + } return TaskExpansionTile( - tasks: patient.doneTasks + tasks: patient.inProgressTasks .map((task) => TaskWithPatient.fromTaskAndPatient( - task: task, - patient: patient, - )) + task: task, + patient: patient, + )) .toList(), - title: context.localization!.inProgress, - color: inProgressColor, + title: context.localization!.done, + color: doneColor, ); - } - return TaskExpansionTile( - tasks: patient.inProgressTasks - .map((task) => TaskWithPatient.fromTaskAndPatient( - task: task, - patient: patient, - )) - .toList(), - title: context.localization!.done, - color: doneColor, - ); - }, - title: Text( - context.localization!.tasks, - style: const TextStyle(fontSize: fontSizeBig, fontWeight: FontWeight.bold), - ), - // TODO use return value to add it to task list or force a refetch - onAdd: () => context.pushModal( - context: context, - builder: (context) => TaskBottomSheet(task: Task.empty, patient: patient), - ), - ); - }), - ), - ], - ), + }, + title: Text( + context.localization!.tasks, + style: const TextStyle(fontSize: fontSizeBig, fontWeight: FontWeight.bold), + ), + // TODO use return value to add it to task list or force a refetch + onAdd: () => context.pushModal( + context: context, + builder: (context) => TaskBottomSheet(task: Task.empty, patient: patient), + ), + ); + }), + ), + ], + ), ), ); } diff --git a/apps/tasks/lib/components/patient_card.dart b/apps/tasks/lib/components/patient_card.dart index cffd161e..d26c63a3 100644 --- a/apps/tasks/lib/components/patient_card.dart +++ b/apps/tasks/lib/components/patient_card.dart @@ -1,8 +1,8 @@ import 'package:flutter/material.dart'; +import 'package:helpwave_service/tasks.dart'; import 'package:tasks/components/task_status_pill_box.dart'; import 'package:helpwave_localization/localization.dart'; import 'package:helpwave_theme/constants.dart'; -import 'package:tasks/dataclasses/patient.dart'; /// A [Widget] for displaying a card containing [Patient] information class PatientCard extends StatelessWidget { diff --git a/apps/tasks/lib/components/patient_status_chip_select.dart b/apps/tasks/lib/components/patient_status_chip_select.dart index c352d9f2..093c5c2f 100644 --- a/apps/tasks/lib/components/patient_status_chip_select.dart +++ b/apps/tasks/lib/components/patient_status_chip_select.dart @@ -1,7 +1,7 @@ import 'package:flutter/cupertino.dart'; import 'package:helpwave_localization/localization.dart'; +import 'package:helpwave_service/tasks.dart'; import 'package:helpwave_widget/content_selection.dart'; -import 'package:tasks/dataclasses/patient.dart'; enum PatientStatusChipSelectOptions { all, active, unassigned, discharged } diff --git a/apps/tasks/lib/components/subtask_list.dart b/apps/tasks/lib/components/subtask_list.dart index 5f78657b..11433f36 100644 --- a/apps/tasks/lib/components/subtask_list.dart +++ b/apps/tasks/lib/components/subtask_list.dart @@ -1,25 +1,23 @@ import 'package:flutter/material.dart'; import 'package:helpwave_localization/localization.dart'; +import 'package:helpwave_service/tasks.dart'; import 'package:helpwave_theme/constants.dart'; import 'package:helpwave_theme/util.dart'; import 'package:helpwave_widget/lists.dart'; import 'package:provider/provider.dart'; -import 'package:tasks/controllers/subtask_list_controller.dart'; -import 'package:tasks/dataclasses/subtask.dart'; -import 'package:tasks/dataclasses/task.dart'; -/// A [Widget] for displaying an updating a [List] of [SubTask]s +/// A [Widget] for displaying an updating a [List] of [Subtask]s class SubtaskList extends StatelessWidget { - /// The identifier of the [Task] to which all of these [SubTask]s belong + /// The identifier of the [Task] to which all of these [Subtask]s belong final String taskId; /// The [List] of initial subtasks - final List subtasks; + final List subtasks; /// The callback when the [subtasks] are changed /// /// Should **only** be used when [taskId == ""] - final void Function(List subtasks) onChange; + final void Function(List subtasks) onChange; const SubtaskList({ super.key, @@ -43,7 +41,7 @@ class SubtaskList extends StatelessWidget { style: const TextStyle(fontWeight: FontWeight.bold, fontSize: 16), ), onAdd: () => subtasksController - .add(SubTask(id: "", name: "Subtask ${subtasksController.subtasks.length + 1}")) + .add(Subtask(id: "", name: "Subtask ${subtasksController.subtasks.length + 1}")) .then((_) => onChange(subtasksController.subtasks)), itemBuilder: (context, _, subtask) => ListTile( contentPadding: EdgeInsets.zero, @@ -71,8 +69,8 @@ class SubtaskList extends StatelessWidget { shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(iconSizeSmall), ), - onChanged: (value) => subtasksController - .changeStatus(subTask: subtask, value: value ?? false) + onChanged: (isDone) => subtasksController + .updateSubtask(subTask: subtask.copyWith(isDone: isDone)) .then((value) => onChange(subtasksController.subtasks)), ), trailing: GestureDetector( diff --git a/apps/tasks/lib/components/task_bottom_sheet.dart b/apps/tasks/lib/components/task_bottom_sheet.dart index 84f33257..d01138eb 100644 --- a/apps/tasks/lib/components/task_bottom_sheet.dart +++ b/apps/tasks/lib/components/task_bottom_sheet.dart @@ -1,5 +1,6 @@ import 'package:flutter/material.dart'; import 'package:helpwave_localization/localization.dart'; +import 'package:helpwave_service/user.dart'; import 'package:helpwave_theme/constants.dart'; import 'package:helpwave_widget/bottom_sheets.dart'; import 'package:helpwave_widget/loading.dart'; @@ -8,13 +9,7 @@ import 'package:provider/provider.dart'; import 'package:tasks/components/assignee_select.dart'; import 'package:tasks/components/subtask_list.dart'; import 'package:tasks/components/visibility_select.dart'; -import 'package:tasks/controllers/task_controller.dart'; -import 'package:tasks/controllers/user_controller.dart'; -import 'package:tasks/dataclasses/patient.dart'; -import 'package:tasks/dataclasses/user.dart'; -import 'package:tasks/services/patient_svc.dart'; -import '../controllers/assignee_select_controller.dart'; -import '../dataclasses/task.dart'; +import 'package:helpwave_service/tasks.dart'; /// A private [Widget] similar to a [ListTile] that has an icon and then to text /// diff --git a/apps/tasks/lib/components/task_card.dart b/apps/tasks/lib/components/task_card.dart index 0ec86450..3eade9a4 100644 --- a/apps/tasks/lib/components/task_card.dart +++ b/apps/tasks/lib/components/task_card.dart @@ -1,8 +1,8 @@ import 'package:flutter/material.dart'; import 'package:helpwave_localization/localization.dart'; +import 'package:helpwave_service/tasks.dart'; import 'package:helpwave_theme/constants.dart'; import 'package:helpwave_widget/static_progress_indicator.dart'; -import 'package:tasks/dataclasses/task.dart'; /// A [Card] showing a [Task]'s information class TaskCard extends StatelessWidget { diff --git a/apps/tasks/lib/components/task_expansion_tile.dart b/apps/tasks/lib/components/task_expansion_tile.dart index ed768d44..dd03b854 100644 --- a/apps/tasks/lib/components/task_expansion_tile.dart +++ b/apps/tasks/lib/components/task_expansion_tile.dart @@ -5,8 +5,7 @@ import 'package:helpwave_widget/shapes.dart'; import 'package:provider/provider.dart'; import 'package:tasks/components/task_bottom_sheet.dart'; import 'package:tasks/components/task_card.dart'; -import '../controllers/my_tasks_controller.dart'; -import '../dataclasses/task.dart'; +import 'package:helpwave_service/tasks.dart'; /// A [ExpansionTile] for showing a [List] of [Task]s /// diff --git a/apps/tasks/lib/components/user_bottom_sheet.dart b/apps/tasks/lib/components/user_bottom_sheet.dart index a574454d..41d94aa1 100644 --- a/apps/tasks/lib/components/user_bottom_sheet.dart +++ b/apps/tasks/lib/components/user_bottom_sheet.dart @@ -5,12 +5,9 @@ import 'package:helpwave_theme/constants.dart'; import 'package:helpwave_widget/bottom_sheets.dart'; import 'package:helpwave_widget/loading.dart'; import 'package:provider/provider.dart'; -import 'package:tasks/controllers/user_session_controller.dart'; -import 'package:tasks/services/current_ward_svc.dart'; -import '../dataclasses/ward.dart'; -import '../screens/login_screen.dart'; -import '../services/user_session_service.dart'; -import '../services/ward_service.dart'; +import 'package:helpwave_service/tasks.dart'; +import 'package:helpwave_service/auth.dart'; +import 'package:tasks/screens/login_screen.dart'; /// A [BottomSheet] for showing the [User]s information class UserBottomSheet extends StatefulWidget { diff --git a/apps/tasks/lib/components/user_header.dart b/apps/tasks/lib/components/user_header.dart index 4506c38b..b66cac3a 100644 --- a/apps/tasks/lib/components/user_header.dart +++ b/apps/tasks/lib/components/user_header.dart @@ -1,13 +1,11 @@ import 'package:flutter/material.dart'; +import 'package:helpwave_service/auth.dart'; +import 'package:helpwave_service/tasks.dart'; import 'package:helpwave_theme/constants.dart'; import 'package:helpwave_widget/bottom_sheets.dart'; import 'package:provider/provider.dart'; import 'package:tasks/components/user_bottom_sheet.dart'; -import 'package:tasks/controllers/user_session_controller.dart'; -import 'package:tasks/dataclasses/organization.dart'; -import 'package:tasks/dataclasses/ward.dart'; import 'package:tasks/screens/settings_screen.dart'; -import 'package:tasks/services/current_ward_svc.dart'; /// A [AppBar] for displaying the current [User], [Organization] and [Ward] class UserHeader extends StatelessWidget implements PreferredSizeWidget { diff --git a/apps/tasks/lib/components/visibility_select.dart b/apps/tasks/lib/components/visibility_select.dart index 677e93ed..2c458b20 100644 --- a/apps/tasks/lib/components/visibility_select.dart +++ b/apps/tasks/lib/components/visibility_select.dart @@ -3,7 +3,6 @@ import 'package:helpwave_localization/localization.dart'; import 'package:helpwave_theme/constants.dart'; import 'package:helpwave_widget/bottom_sheets.dart'; import 'package:helpwave_widget/dialog.dart'; -import 'package:tasks/dataclasses/task.dart'; /// A [BottomSheet] to change the visibility class _VisibilityBottomSheet extends StatelessWidget { diff --git a/apps/tasks/lib/config/config.dart b/apps/tasks/lib/config/config.dart index 207fcd97..a0f1e628 100644 --- a/apps/tasks/lib/config/config.dart +++ b/apps/tasks/lib/config/config.dart @@ -4,7 +4,7 @@ const minimumPasswordCharacters = 6; /// Whether the development mode should be enabled /// /// Shortens the login -const bool devMode = false; +const bool devMode = true; /// The API for testing const String stagingAPIURL = "staging.api.helpwave.de"; diff --git a/apps/tasks/lib/dataclasses/subtask.dart b/apps/tasks/lib/dataclasses/subtask.dart deleted file mode 100644 index b48c6d32..00000000 --- a/apps/tasks/lib/dataclasses/subtask.dart +++ /dev/null @@ -1,14 +0,0 @@ -/// data class for [SubTask] -class SubTask { - String id; - String name; - bool isDone; - - bool get isCreating => id == ""; - - SubTask({ - required this.id, - required this.name, - this.isDone = false - }); -} diff --git a/apps/tasks/lib/debug/theme_visualizer.dart b/apps/tasks/lib/debug/theme_visualizer.dart index fe0b59bf..d042da5b 100644 --- a/apps/tasks/lib/debug/theme_visualizer.dart +++ b/apps/tasks/lib/debug/theme_visualizer.dart @@ -5,9 +5,7 @@ import 'package:helpwave_widget/loading.dart'; import 'package:provider/provider.dart'; import 'package:tasks/components/subtask_list.dart'; import 'package:tasks/components/task_card.dart'; -import 'package:tasks/dataclasses/patient.dart'; - -import '../dataclasses/task.dart'; +import 'package:helpwave_service/tasks.dart'; class ThemeVisualizer extends StatelessWidget { const ThemeVisualizer({super.key}); diff --git a/apps/tasks/lib/main.dart b/apps/tasks/lib/main.dart index f6e1808b..e5b4ad0f 100644 --- a/apps/tasks/lib/main.dart +++ b/apps/tasks/lib/main.dart @@ -1,14 +1,17 @@ import 'package:flutter/material.dart'; +import 'package:helpwave_service/auth.dart'; import 'package:provider/provider.dart'; import 'package:helpwave_localization/l10n/app_localizations.dart'; import 'package:helpwave_localization/localization.dart'; import 'package:helpwave_localization/localization_model.dart'; import 'package:helpwave_theme/theme.dart'; +import 'package:tasks/config/config.dart'; +import 'package:helpwave_service/tasks.dart'; import 'package:tasks/screens/login_screen.dart'; -import 'package:tasks/services/current_ward_svc.dart'; -import 'controllers/user_session_controller.dart'; void main() { + UserSessionService().changeMode(devMode); + TasksAPIServices.apiUrl = usedAPIURL; runApp(const MyApp()); } @@ -26,7 +29,7 @@ class MyApp extends StatelessWidget { create: (_) => LanguageModel(), ), ChangeNotifierProvider( - create: (_) => CurrentWardController(), + create: (_) => CurrentWardController(devMode: devMode), ), ChangeNotifierProvider( create: (_) => UserSessionController(), diff --git a/apps/tasks/lib/screens/landing_screen.dart b/apps/tasks/lib/screens/landing_screen.dart index ba487dd1..052622a1 100644 --- a/apps/tasks/lib/screens/landing_screen.dart +++ b/apps/tasks/lib/screens/landing_screen.dart @@ -1,10 +1,11 @@ import 'package:flutter/material.dart'; import 'package:helpwave_localization/localization.dart'; +import 'package:helpwave_service/auth.dart'; import 'package:helpwave_theme/constants.dart'; import 'package:helpwave_theme/theme.dart'; +import 'package:helpwave_theme/util.dart'; import 'package:helpwave_widget/loading.dart'; import 'package:provider/provider.dart'; -import 'package:tasks/controllers/user_session_controller.dart'; /// The Landing Screen of the Application class LandingScreen extends StatelessWidget { @@ -24,7 +25,8 @@ class LandingScreen extends StatelessWidget { } return OutlinedButton( - style: buttonStyleBig.copyWith(side: const MaterialStatePropertyAll(buttonBorderSideBig)), + style: buttonStyleBig.copyWith(side: MaterialStatePropertyAll(buttonBorderSideBig.copyWith(color: context + .theme.colorScheme.onBackground))), child: Text( context.localization!.loginSlogan, style: Theme.of(context).textTheme.labelLarge, diff --git a/apps/tasks/lib/screens/login_screen.dart b/apps/tasks/lib/screens/login_screen.dart index 8d963cb4..89ac9a6d 100644 --- a/apps/tasks/lib/screens/login_screen.dart +++ b/apps/tasks/lib/screens/login_screen.dart @@ -1,8 +1,8 @@ import 'package:flutter/cupertino.dart'; +import 'package:helpwave_service/auth.dart'; import 'package:provider/provider.dart'; import 'package:tasks/screens/landing_screen.dart'; import 'package:tasks/screens/main_screen.dart'; -import '../controllers/user_session_controller.dart'; /// A Screen for forcing the User to login or be logged in /// diff --git a/apps/tasks/lib/screens/main_screen.dart b/apps/tasks/lib/screens/main_screen.dart index 690e5d2b..1235b056 100644 --- a/apps/tasks/lib/screens/main_screen.dart +++ b/apps/tasks/lib/screens/main_screen.dart @@ -1,19 +1,19 @@ import 'dart:async'; import 'package:flutter/material.dart'; import 'package:helpwave_localization/localization.dart'; +import 'package:helpwave_service/auth.dart'; +import 'package:helpwave_service/tasks.dart'; import 'package:helpwave_theme/constants.dart'; import 'package:helpwave_theme/theme.dart'; import 'package:helpwave_widget/bottom_sheets.dart'; import 'package:helpwave_widget/animation.dart'; import 'package:provider/provider.dart'; import 'package:tasks/components/patient_bottom_sheet.dart'; +import 'package:tasks/components/task_bottom_sheet.dart'; import 'package:tasks/components/user_header.dart'; import 'package:tasks/screens/main_screen_subscreens/my_tasks_screen.dart'; import 'package:tasks/screens/main_screen_subscreens/patient_screen.dart'; import 'package:tasks/screens/ward_select_screen.dart'; -import 'package:tasks/services/current_ward_svc.dart'; -import '../components/task_bottom_sheet.dart'; -import '../dataclasses/task.dart'; /// The main screen of the app /// diff --git a/apps/tasks/lib/screens/main_screen_subscreens/my_tasks_screen.dart b/apps/tasks/lib/screens/main_screen_subscreens/my_tasks_screen.dart index 47a93010..43572a93 100644 --- a/apps/tasks/lib/screens/main_screen_subscreens/my_tasks_screen.dart +++ b/apps/tasks/lib/screens/main_screen_subscreens/my_tasks_screen.dart @@ -1,11 +1,10 @@ import 'package:flutter/material.dart'; +import 'package:helpwave_service/tasks.dart'; import 'package:helpwave_theme/constants.dart'; import 'package:provider/provider.dart'; import 'package:helpwave_localization/localization.dart'; import 'package:tasks/components/task_expansion_tile.dart'; import 'package:helpwave_widget/loading.dart'; -import '../../controllers/my_tasks_controller.dart'; -import '../../dataclasses/task.dart'; /// The Screen for showing all [Task]'s the [User] has assigned to them class MyTasksScreen extends StatefulWidget { diff --git a/apps/tasks/lib/screens/main_screen_subscreens/patient_screen.dart b/apps/tasks/lib/screens/main_screen_subscreens/patient_screen.dart index 511028a9..80ce9f6f 100644 --- a/apps/tasks/lib/screens/main_screen_subscreens/patient_screen.dart +++ b/apps/tasks/lib/screens/main_screen_subscreens/patient_screen.dart @@ -8,9 +8,7 @@ import 'package:tasks/components/patient_bottom_sheet.dart'; import 'package:tasks/components/patient_card.dart'; import 'package:tasks/components/patient_status_chip_select.dart'; import 'package:tasks/components/task_bottom_sheet.dart'; -import 'package:tasks/controllers/ward_patients_controller.dart'; -import 'package:tasks/dataclasses/patient.dart'; -import 'package:tasks/dataclasses/task.dart'; +import 'package:helpwave_service/tasks.dart'; /// A screen for showing a all [Patient]s by certain user-selectable filter properties /// diff --git a/apps/tasks/lib/screens/settings_screen.dart b/apps/tasks/lib/screens/settings_screen.dart index 6258c1e7..e943d53b 100644 --- a/apps/tasks/lib/screens/settings_screen.dart +++ b/apps/tasks/lib/screens/settings_screen.dart @@ -1,12 +1,11 @@ import 'package:flutter/material.dart'; import 'package:helpwave_localization/localization.dart'; import 'package:helpwave_localization/localization_model.dart'; +import 'package:helpwave_service/auth.dart'; import 'package:helpwave_theme/constants.dart'; import 'package:helpwave_theme/theme.dart'; import 'package:provider/provider.dart'; import 'package:tasks/screens/login_screen.dart'; -import 'package:tasks/services/user_session_service.dart'; -import 'package:tasks/services/current_ward_svc.dart'; /// Screen for settings and other app options class SettingsScreen extends StatefulWidget { diff --git a/apps/tasks/lib/screens/ward_select_screen.dart b/apps/tasks/lib/screens/ward_select_screen.dart index f0bda449..7e7248c7 100644 --- a/apps/tasks/lib/screens/ward_select_screen.dart +++ b/apps/tasks/lib/screens/ward_select_screen.dart @@ -1,16 +1,14 @@ import 'package:flutter/material.dart'; import 'package:helpwave_localization/localization.dart'; +import 'package:helpwave_service/auth.dart'; +import 'package:helpwave_service/tasks.dart'; +import 'package:helpwave_service/user.dart'; import 'package:helpwave_theme/constants.dart'; import 'package:helpwave_widget/content_selection.dart'; import 'package:provider/provider.dart'; -import 'package:tasks/dataclasses/organization.dart'; import 'package:tasks/screens/settings_screen.dart'; -import 'package:tasks/services/current_ward_svc.dart'; -import 'package:tasks/services/organization_svc.dart'; -import 'package:tasks/services/ward_service.dart'; -import '../dataclasses/ward.dart'; -/// A Screen to select the current [Organization] and [Ward] +/// A Screen to select the current [OrganizationService] and [Ward] class WardSelectScreen extends StatefulWidget { const WardSelectScreen({super.key}); diff --git a/apps/tasks/lib/services/grpc_client_svc.dart b/apps/tasks/lib/services/grpc_client_svc.dart deleted file mode 100644 index 30278c22..00000000 --- a/apps/tasks/lib/services/grpc_client_svc.dart +++ /dev/null @@ -1,76 +0,0 @@ -import 'package:grpc/grpc.dart'; -import 'package:helpwave_proto_dart/proto/services/task_svc/v1/patient_svc.pbgrpc.dart'; -import 'package:helpwave_proto_dart/proto/services/task_svc/v1/room_svc.pbgrpc.dart'; -import 'package:helpwave_proto_dart/proto/services/task_svc/v1/task_svc.pbgrpc.dart'; -import 'package:helpwave_proto_dart/proto/services/task_svc/v1/ward_svc.pbgrpc.dart'; -import 'package:helpwave_proto_dart/proto/services/user_svc/v1/organization_svc.pbgrpc.dart'; -import 'package:helpwave_proto_dart/proto/services/user_svc/v1/user_svc.pbgrpc.dart'; -import 'package:tasks/config/config.dart'; -import 'package:tasks/services/current_ward_svc.dart'; -import 'user_session_service.dart'; - -/// The Underlying GrpcService it provides other clients and the correct metadata for the requests -class GRPCClientService { - static final taskServiceChannel = ClientChannel( - usedAPIURL, - ); - static final userServiceChannel = ClientChannel( - usedAPIURL, - ); - - final UserSessionService authService = UserSessionService(); - - Map get authMetaData { - if (authService.isLoggedIn) { - return { - "Authorization": "Bearer ${UserSessionService().identity?.idToken}", - }; - } - // Maybe throw a error instead - return {}; - } - - String? get fallbackOrganizationId => - // Maybe throw a error instead for the last case - CurrentWardService().currentWard?.organizationId ?? authService.identity?.firstOrganization; - - Map getTaskServiceMetaData({String? organizationId}) { - var metaData = { - ...authMetaData, - "dapr-app-id": "task-svc", - }; - - if (organizationId != null) { - metaData["X-Organization"] = organizationId; - } else { - metaData["X-Organization"] = fallbackOrganizationId!; - } - - return metaData; - } - - Map getUserServiceMetaData({String? organizationId}) { - var metaData = { - ...authMetaData, - "dapr-app-id": "user-svc", - }; - - if (organizationId != null) { - metaData["X-Organization"] = organizationId; - } - - return metaData; - } - - static PatientServiceClient get getPatientServiceClient => PatientServiceClient(taskServiceChannel); - - static WardServiceClient get getWardServiceClient => WardServiceClient(taskServiceChannel); - - static RoomServiceClient get getRoomServiceClient => RoomServiceClient(taskServiceChannel); - - static TaskServiceClient get getTaskServiceClient => TaskServiceClient(taskServiceChannel); - - static UserServiceClient get getUserServiceClient => UserServiceClient(userServiceChannel); - - static OrganizationServiceClient get getOrganizationServiceClient => OrganizationServiceClient(userServiceChannel); -} diff --git a/apps/tasks/lib/services/task_svc.dart b/apps/tasks/lib/services/task_svc.dart deleted file mode 100644 index 642e45f4..00000000 --- a/apps/tasks/lib/services/task_svc.dart +++ /dev/null @@ -1,197 +0,0 @@ -import 'package:grpc/grpc.dart'; -import 'package:helpwave_proto_dart/google/protobuf/timestamp.pb.dart'; -import 'package:helpwave_proto_dart/proto/services/task_svc/v1/task_svc.pbgrpc.dart'; -import 'package:tasks/dataclasses/patient.dart'; -import 'package:tasks/dataclasses/subtask.dart'; -import 'package:tasks/services/grpc_client_svc.dart'; -import 'package:tasks/util/task_status_mapping.dart'; -import '../dataclasses/task.dart'; - -/// The GRPC Service for [Task]s -/// -/// Provides queries and requests that load or alter [Task] objects on the server -/// The server is defined in the underlying [GRPCClientService] -class TaskService { - /// The GRPC ServiceClient which handles GRPC - TaskServiceClient taskService = GRPCClientService.getTaskServiceClient; - - /// Loads the [Task]s by a [Patient] identifier - Future> getTasksByPatient({String? patientId}) async { - GetTasksByPatientRequest request = GetTasksByPatientRequest(patientId: patientId); - GetTasksByPatientResponse response = await taskService.getTasksByPatient( - request, - options: CallOptions(metadata: GRPCClientService().getTaskServiceMetaData()), - ); - - return response.tasks - .map((task) => Task( - id: task.id, - name: task.name, - notes: task.description, - isPublicVisible: task.public, - status: taskStatusMappingFromProto[task.status]!, - assignee: task.assignedUserId, - dueDate: task.dueAt.toDateTime(), - subtasks: task.subtasks - .map((subtask) => SubTask( - id: subtask.id, - name: subtask.name, - isDone: subtask.done, - )) - .toList(), - )) - .toList(); - } - - /// Loads the [Task]s by it's identifier - Future getTask({String? id}) async { - GetTaskRequest request = GetTaskRequest(id: id); - GetTaskResponse response = await taskService.getTask( - request, - options: CallOptions(metadata: GRPCClientService().getTaskServiceMetaData()), - ); - - return TaskWithPatient( - id: response.id, - name: response.name, - notes: response.description, - isPublicVisible: response.public, - status: taskStatusMappingFromProto[response.status]!, - assignee: response.assignedUserId, - dueDate: response.dueAt.toDateTime(), - patient: PatientMinimal(id: response.patient.id, name: response.patient.name), - subtasks: response.subtasks - .map((subtask) => SubTask( - id: subtask.id, - name: subtask.name, - isDone: subtask.done, - )) - .toList(), - ); - } - - Future createTask(TaskWithPatient task) async { - CreateTaskRequest request = CreateTaskRequest( - name: task.name, - description: task.notes, - initialStatus: taskStatusMappingToProto[task.status], - dueAt: task.dueDate != null ? Timestamp.fromDateTime(task.dueDate!) : null, - patientId: !task.patient.isCreating ? task.patient.id : null, - public: task.isPublicVisible, - ); - CreateTaskResponse response = await taskService.createTask( - request, - options: CallOptions(metadata: GRPCClientService().getTaskServiceMetaData()), - ); - - return response.id; - } - - /// Assign a [Task] to a [User] - Future assignToUser({required String taskId, required String userId}) async { - AssignTaskToUserRequest request = AssignTaskToUserRequest(id: taskId, userId: userId); - AssignTaskToUserResponse response = await taskService.assignTaskToUser( - request, - options: CallOptions(metadata: GRPCClientService().getTaskServiceMetaData()), - ); - - if (!response.isInitialized()) { - // Handle error - } - } - - /// Add a [SubTask] to a [Task] - Future addSubTask({required String taskId, required SubTask subTask}) async { - AddSubTaskRequest request = AddSubTaskRequest( - taskId: taskId, - name: subTask.name, - done: subTask.isDone, - ); - AddSubTaskResponse response = await taskService.addSubTask( - request, - options: CallOptions(metadata: GRPCClientService().getTaskServiceMetaData()), - ); - - return SubTask( - id: response.id, - name: subTask.name, - isDone: subTask.isDone, - ); - } - - /// Delete a [SubTask] by its identifier - Future deleteSubTask({required String id}) async { - RemoveSubTaskRequest request = RemoveSubTaskRequest(id: id); - RemoveSubTaskResponse response = await taskService.removeSubTask( - request, - options: CallOptions(metadata: GRPCClientService().getTaskServiceMetaData()), - ); - - return response.isInitialized(); - } - - /// Change a [SubTask]'s status to done by its identifier - Future subtaskToDone({required String id}) async { - SubTaskToDoneRequest request = SubTaskToDoneRequest(id: id); - SubTaskToDoneResponse response = await taskService.subTaskToDone( - request, - options: CallOptions(metadata: GRPCClientService().getTaskServiceMetaData()), - ); - - return response.isInitialized(); - } - - /// Change a [SubTask]'s status to todo by its identifier - Future subtaskToToDo({required String id}) async { - SubTaskToToDoRequest request = SubTaskToToDoRequest(id: id); - SubTaskToToDoResponse response = await taskService.subTaskToToDo( - request, - options: CallOptions(metadata: GRPCClientService().getTaskServiceMetaData()), - ); - - return response.isInitialized(); - } - - /// Change a [SubTask]'s status by its identifier - Future changeSubtaskStatus({ - required String id, - required isDone, - }) async { - if (isDone) { - return subtaskToDone(id: id); - } else { - return subtaskToToDo(id: id); - } - } - - /// Update a [SubTask]'s - Future updateSubTask({required SubTask subTask}) async { - UpdateSubTaskRequest request = UpdateSubTaskRequest( - id: subTask.id, - name: subTask.name, - ); - UpdateSubTaskResponse response = await taskService.updateSubTask( - request, - options: CallOptions(metadata: GRPCClientService().getTaskServiceMetaData()), - ); - - return response.isInitialized(); - } - - Future updateTask(Task task) async { - UpdateTaskRequest request = UpdateTaskRequest( - id: task.id, - name: task.name, - description: task.notes, - dueAt: task.dueDate != null ? Timestamp.fromDateTime(task.dueDate!) : null, - public: task.isPublicVisible, - ); - - UpdateTaskResponse response = await taskService.updateTask( - request, - options: CallOptions(metadata: GRPCClientService().getTaskServiceMetaData()), - ); - - return response.isInitialized(); - } -} diff --git a/apps/tasks/lib/util/task_status_mapping.dart b/apps/tasks/lib/util/task_status_mapping.dart deleted file mode 100644 index 6ee32cdf..00000000 --- a/apps/tasks/lib/util/task_status_mapping.dart +++ /dev/null @@ -1,16 +0,0 @@ -import 'package:tasks/dataclasses/task.dart' as task_lib; -import 'package:helpwave_proto_dart/proto/services/task_svc/v1/task_svc.pbenum.dart' as proto; - -Map taskStatusMappingFromProto = { - proto.TaskStatus.TASK_STATUS_TODO: task_lib.TaskStatus.todo, - proto.TaskStatus.TASK_STATUS_IN_PROGRESS: task_lib.TaskStatus.inProgress, - proto.TaskStatus.TASK_STATUS_DONE: task_lib.TaskStatus.done, - proto.TaskStatus.TASK_STATUS_UNSPECIFIED: task_lib.TaskStatus.unspecified, -}; - -Map taskStatusMappingToProto = { - task_lib.TaskStatus.todo: proto.TaskStatus.TASK_STATUS_TODO, - task_lib.TaskStatus.inProgress: proto.TaskStatus.TASK_STATUS_IN_PROGRESS, - task_lib.TaskStatus.done: proto.TaskStatus.TASK_STATUS_DONE, - task_lib.TaskStatus.unspecified: proto.TaskStatus.TASK_STATUS_UNSPECIFIED, -}; diff --git a/apps/tasks/pubspec.lock b/apps/tasks/pubspec.lock index fa0366a5..bbc15e5d 100644 --- a/apps/tasks/pubspec.lock +++ b/apps/tasks/pubspec.lock @@ -254,7 +254,7 @@ packages: source: hosted version: "1.4.1" grpc: - dependency: "direct main" + dependency: transitive description: name: grpc sha256: e93ee3bce45c134bf44e9728119102358c7cd69de7832d9a874e2e74eb8cab40 @@ -269,13 +269,13 @@ packages: source: path version: "0.0.1" helpwave_proto_dart: - dependency: "direct main" + dependency: transitive description: name: helpwave_proto_dart - sha256: "60e912fcb781e16b9b5bd6b5c27585d9481ae554be2bfc2e58c76dcfa4735d60" + sha256: "4d4b2b6d0129c7c1b678164688e2bd0a99029cc178794fd655e7c4e7aa505d58" url: "https://pub.dev" source: hosted - version: "0.39.0-aa8fd45" + version: "0.46.0-336429a" helpwave_service: dependency: "direct main" description: @@ -291,7 +291,7 @@ packages: source: path version: "0.0.1" helpwave_util: - dependency: "direct overridden" + dependency: "direct main" description: path: "../../packages/helpwave_util" relative: true @@ -376,30 +376,6 @@ packages: url: "https://pub.dev" source: hosted version: "4.8.1" - leak_tracker: - dependency: transitive - description: - name: leak_tracker - sha256: "78eb209deea09858f5269f5a5b02be4049535f568c07b275096836f01ea323fa" - url: "https://pub.dev" - source: hosted - version: "10.0.0" - leak_tracker_flutter_testing: - dependency: transitive - description: - name: leak_tracker_flutter_testing - sha256: b46c5e37c19120a8a01918cfaf293547f47269f7cb4b0058f21531c2465d6ef0 - url: "https://pub.dev" - source: hosted - version: "2.0.1" - leak_tracker_testing: - dependency: transitive - description: - name: leak_tracker_testing - sha256: a597f72a664dbd293f3bfc51f9ba69816f84dcd403cdac7066cb3f6003f3ab47 - url: "https://pub.dev" - source: hosted - version: "2.0.1" lints: dependency: transitive description: @@ -428,26 +404,26 @@ packages: dependency: transitive description: name: matcher - sha256: d2323aa2060500f906aa31a895b4030b6da3ebdcc5619d14ce1aada65cd161cb + sha256: "1803e76e6653768d64ed8ff2e1e67bea3ad4b923eb5c56a295c3e634bad5960e" url: "https://pub.dev" source: hosted - version: "0.12.16+1" + version: "0.12.16" material_color_utilities: dependency: transitive description: name: material_color_utilities - sha256: "0e0a020085b65b6083975e499759762399b4475f766c21668c4ecca34ea74e5a" + sha256: "9528f2f296073ff54cb9fee677df673ace1218163c3bc7628093e7eed5203d41" url: "https://pub.dev" source: hosted - version: "0.8.0" + version: "0.5.0" meta: dependency: transitive description: name: meta - sha256: d584fa6707a52763a52446f02cc621b077888fb63b93bbcb1143a7be5a0c0c04 + sha256: a6e590c838b18133bb482a2745ad77c5bb7715fb0451209e1a7567d416678b8e url: "https://pub.dev" source: hosted - version: "1.11.0" + version: "1.10.0" nested: dependency: transitive description: @@ -468,10 +444,10 @@ packages: dependency: transitive description: name: path - sha256: "087ce49c3f0dc39180befefc60fdb4acd8f8620e5682fe2476afd0b3688bb4af" + sha256: "8829d8a55c13fc0e37127c29fedf290c102f4e40ae94ada574091fe0ff96c917" url: "https://pub.dev" source: hosted - version: "1.9.0" + version: "1.8.3" path_provider: dependency: transitive description: @@ -773,14 +749,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.1.4" - vm_service: + web: dependency: transitive description: - name: vm_service - sha256: b3d56ff4341b8f182b96aceb2fa20e3dcb336b9f867bc0eafc0de10f1048e957 + name: web + sha256: afe077240a270dcfd2aafe77602b4113645af95d0ad31128cc02bce5ac5d5152 url: "https://pub.dev" source: hosted - version: "13.0.0" + version: "0.3.0" win32: dependency: transitive description: @@ -822,5 +798,5 @@ packages: source: hosted version: "3.1.2" sdks: - dart: ">=3.2.0-0 <4.0.0" + dart: ">=3.2.0-194.0.dev <4.0.0" flutter: ">=3.16.0" diff --git a/apps/tasks/pubspec.yaml b/apps/tasks/pubspec.yaml index 068c883b..032d9e18 100644 --- a/apps/tasks/pubspec.yaml +++ b/apps/tasks/pubspec.yaml @@ -45,8 +45,8 @@ dependencies: path: "../../packages/helpwave_widget" helpwave_service: path: "../../packages/helpwave_service" - helpwave_proto_dart: ^0.39.0-aa8fd45 - grpc: ^3.2.4 + helpwave_util: + path: "../../packages/helpwave_util" shared_preferences: ^2.0.15 logger: ^2.0.2+1 diff --git a/packages/helpwave_service/lib/auth.dart b/packages/helpwave_service/lib/auth.dart index cba87fbb..497562fd 100644 --- a/packages/helpwave_service/lib/auth.dart +++ b/packages/helpwave_service/lib/auth.dart @@ -1,4 +1 @@ -export 'package:helpwave_service/src/auth/authentication_service.dart' - show AuthenticationService; - -export 'package:helpwave_service/src/auth/identity.dart'; +export 'package:helpwave_service/src/auth/index.dart'; diff --git a/apps/tasks/lib/controllers/assignee_select_controller.dart b/packages/helpwave_service/lib/src/api/tasks/controllers/assignee_select_controller.dart similarity index 86% rename from apps/tasks/lib/controllers/assignee_select_controller.dart rename to packages/helpwave_service/lib/src/api/tasks/controllers/assignee_select_controller.dart index 19d3c5d5..a8fd4fed 100644 --- a/apps/tasks/lib/controllers/assignee_select_controller.dart +++ b/packages/helpwave_service/lib/src/api/tasks/controllers/assignee_select_controller.dart @@ -1,11 +1,9 @@ import 'package:flutter/foundation.dart'; -import 'package:helpwave_widget/loading.dart'; +import 'package:helpwave_service/auth.dart'; +import 'package:helpwave_service/src/api/user/index.dart'; +import 'package:helpwave_util/loading_state.dart'; import 'package:logger/logger.dart'; -import 'package:tasks/dataclasses/task.dart'; -import 'package:tasks/services/current_ward_svc.dart'; -import 'package:tasks/services/organization_svc.dart'; -import 'package:tasks/services/task_svc.dart'; -import '../dataclasses/user.dart'; +import 'package:helpwave_service/tasks.dart'; /// The Controller for selecting a [User] as the assignee of a [Task] class AssigneeSelectController extends ChangeNotifier { diff --git a/packages/helpwave_service/lib/src/api/tasks/controllers/index.dart b/packages/helpwave_service/lib/src/api/tasks/controllers/index.dart new file mode 100644 index 00000000..a987ab71 --- /dev/null +++ b/packages/helpwave_service/lib/src/api/tasks/controllers/index.dart @@ -0,0 +1,6 @@ +export 'patient_controller.dart'; +export 'subtask_list_controller.dart'; +export 'task_controller.dart'; +export 'assignee_select_controller.dart'; +export 'my_tasks_controller.dart'; +export 'ward_patients_controller.dart'; diff --git a/apps/tasks/lib/controllers/my_tasks_controller.dart b/packages/helpwave_service/lib/src/api/tasks/controllers/my_tasks_controller.dart similarity index 81% rename from apps/tasks/lib/controllers/my_tasks_controller.dart rename to packages/helpwave_service/lib/src/api/tasks/controllers/my_tasks_controller.dart index 1513c18a..c353476a 100644 --- a/apps/tasks/lib/controllers/my_tasks_controller.dart +++ b/packages/helpwave_service/lib/src/api/tasks/controllers/my_tasks_controller.dart @@ -1,11 +1,6 @@ import 'package:flutter/cupertino.dart'; -import 'package:helpwave_widget/loading.dart'; -import 'package:tasks/dataclasses/task.dart'; -import 'package:tasks/services/current_ward_svc.dart'; -import 'package:tasks/services/patient_svc.dart'; -import 'package:tasks/services/task_svc.dart'; - -import '../dataclasses/patient.dart'; +import 'package:helpwave_service/tasks.dart'; +import 'package:helpwave_util/loading_state.dart'; /// The Controller for [Task]s of the current [User] class MyTasksController extends ChangeNotifier { @@ -38,7 +33,7 @@ class MyTasksController extends ChangeNotifier { notifyListeners(); } - var patients = await PatientService().getPatientList(wardId: CurrentWardService().currentWard?.wardId); + var patients = await PatientService().getPatientList(); ListallPatients = patients.all; _tasks = []; diff --git a/apps/tasks/lib/controllers/patient_controller.dart b/packages/helpwave_service/lib/src/api/tasks/controllers/patient_controller.dart similarity index 95% rename from apps/tasks/lib/controllers/patient_controller.dart rename to packages/helpwave_service/lib/src/api/tasks/controllers/patient_controller.dart index bf47e763..95daca37 100644 --- a/apps/tasks/lib/controllers/patient_controller.dart +++ b/packages/helpwave_service/lib/src/api/tasks/controllers/patient_controller.dart @@ -1,9 +1,6 @@ import 'package:flutter/material.dart'; -import 'package:helpwave_widget/loading.dart'; -import 'package:tasks/services/patient_svc.dart'; -import '../dataclasses/bed.dart'; -import '../dataclasses/patient.dart'; -import '../dataclasses/room.dart'; +import 'package:helpwave_util/loading_state.dart'; +import 'package:helpwave_service/src/api/tasks/index.dart'; /// The Controller for managing [Patient]s in a Ward class PatientController extends ChangeNotifier { diff --git a/apps/tasks/lib/controllers/subtask_list_controller.dart b/packages/helpwave_service/lib/src/api/tasks/controllers/subtask_list_controller.dart similarity index 66% rename from apps/tasks/lib/controllers/subtask_list_controller.dart rename to packages/helpwave_service/lib/src/api/tasks/controllers/subtask_list_controller.dart index 66620456..f11402f2 100644 --- a/apps/tasks/lib/controllers/subtask_list_controller.dart +++ b/packages/helpwave_service/lib/src/api/tasks/controllers/subtask_list_controller.dart @@ -1,12 +1,11 @@ import 'dart:async'; import 'package:flutter/cupertino.dart'; -import 'package:helpwave_widget/loading.dart'; -import 'package:tasks/dataclasses/subtask.dart'; -import 'package:tasks/services/task_svc.dart'; +import 'package:helpwave_service/src/api/tasks/index.dart'; +import 'package:helpwave_util/loading_state.dart'; /// The Controller for managing [Subtask]s in a [Task] /// -/// Providing a [taskId] means loading and synchronising the [SubTask]s with +/// Providing a [taskId] means loading and synchronising the [Subtask]s with /// the backend while no [taskId] or a empty [String] means that the subtasks /// only used locally class SubtasksController extends ChangeNotifier { @@ -21,11 +20,11 @@ class SubtasksController extends ChangeNotifier { } /// The [Subtask]s - List _subtasks = []; + List _subtasks = []; - List get subtasks => [..._subtasks]; + List get subtasks => [..._subtasks]; - set subtasks(List value) { + set subtasks(List value) { _subtasks = value; notifyListeners(); } @@ -39,7 +38,7 @@ class SubtasksController extends ChangeNotifier { /// Only valid in case [state] == [LoadingState.error] String errorMessage = ""; - SubtasksController({this.taskId = "", List? subtasks}) { + SubtasksController({this.taskId = "", List? subtasks}) { _isCreating = taskId == ""; if (!isCreating) { load(); @@ -64,7 +63,7 @@ class SubtasksController extends ChangeNotifier { state = LoadingState.loaded; } - /// Delete the subtask by the index + /// Delete the subtask by the index.dart Future deleteByIndex(int index) async { if (index < 0 || index >= subtasks.length) { return; @@ -75,7 +74,7 @@ class SubtasksController extends ChangeNotifier { return; } state = LoadingState.loading; - await TaskService().deleteSubTask(id: subtasks[index].id).then((value) { + await TaskService().deleteSubTask(subtaskId: subtasks[index].id, taskId: taskId).then((value) { if (value) { _subtasks.removeAt(index); state = LoadingState.loaded; @@ -87,7 +86,7 @@ class SubtasksController extends ChangeNotifier { }); } - /// Delete the [SubTask] by the id + /// Delete the [Subtask] by the id Future delete(String id) async { assert(!isCreating, "delete should not be used when creating a completely new SubTask list"); int index = _subtasks.indexWhere((element) => element.id == id); @@ -96,14 +95,14 @@ class SubtasksController extends ChangeNotifier { } } - /// Add the [SubTask] - Future add(SubTask subTask) async { + /// Add the [Subtask] + Future add(Subtask subTask) async { if (isCreating) { _subtasks.add(subTask); notifyListeners(); return; } - await TaskService().addSubTask(taskId: taskId, subTask: subTask).then((value) { + await TaskService().createSubTask(taskId: taskId, subTask: subTask).then((value) { _subtasks.add(value); state = LoadingState.loaded; }).catchError((error, stackTrace) { @@ -113,27 +112,10 @@ class SubtasksController extends ChangeNotifier { }); } - Future changeStatus({required SubTask subTask, required bool value}) async { - if (subTask.isCreating) { - subTask.isDone = value; - } else { - state = LoadingState.loading; - await TaskService().changeSubtaskStatus(id: subTask.id, isDone: value).then((value) { - subTask.isDone = value; - state = LoadingState.loaded; - }).catchError((error, stackTrace) { - errorMessage = error.toString(); - state = LoadingState.error; - return null; - }); - } - notifyListeners(); - } - - Future updateSubtask({required SubTask subTask}) async { + Future updateSubtask({required Subtask subTask}) async { if (!subTask.isCreating) { state = LoadingState.loading; - await TaskService().updateSubTask(subTask: subTask).then((value) { + await TaskService().updateSubtask(subtask: subTask, taskId: taskId).then((value) { state = LoadingState.loaded; }).catchError((error, stackTrace) { // Just reload in case of an error diff --git a/apps/tasks/lib/controllers/task_controller.dart b/packages/helpwave_service/lib/src/api/tasks/controllers/task_controller.dart similarity index 96% rename from apps/tasks/lib/controllers/task_controller.dart rename to packages/helpwave_service/lib/src/api/tasks/controllers/task_controller.dart index 917961fe..f12f77f9 100644 --- a/apps/tasks/lib/controllers/task_controller.dart +++ b/packages/helpwave_service/lib/src/api/tasks/controllers/task_controller.dart @@ -1,8 +1,6 @@ import 'package:flutter/material.dart'; -import 'package:helpwave_widget/loading.dart'; -import '../dataclasses/patient.dart'; -import '../dataclasses/task.dart'; -import '../services/task_svc.dart'; +import 'package:helpwave_util/loading_state.dart'; +import 'package:helpwave_service/src/api/tasks/index.dart'; /// The Controller for managing a [TaskWithPatient] class TaskController extends ChangeNotifier { diff --git a/apps/tasks/lib/controllers/ward_patients_controller.dart b/packages/helpwave_service/lib/src/api/tasks/controllers/ward_patients_controller.dart similarity index 86% rename from apps/tasks/lib/controllers/ward_patients_controller.dart rename to packages/helpwave_service/lib/src/api/tasks/controllers/ward_patients_controller.dart index e0edea42..e18d4d01 100644 --- a/apps/tasks/lib/controllers/ward_patients_controller.dart +++ b/packages/helpwave_service/lib/src/api/tasks/controllers/ward_patients_controller.dart @@ -1,9 +1,7 @@ import 'package:flutter/cupertino.dart'; -import 'package:helpwave_widget/loading.dart'; -import 'package:tasks/dataclasses/patient.dart'; -import 'package:tasks/services/current_ward_svc.dart'; -import 'package:tasks/services/patient_svc.dart'; -import 'package:tasks/util/search_helpers.dart'; +import 'package:helpwave_service/src/api/tasks/index.dart'; +import 'package:helpwave_util/loading_state.dart'; +import 'package:helpwave_util/search.dart'; /// The Controller for managing [Patient]s in a Ward class WardPatientsController extends ChangeNotifier { @@ -77,8 +75,7 @@ class WardPatientsController extends ChangeNotifier { state = LoadingState.loading; notifyListeners(); - _patientsByAssignmentStatus = - await PatientService().getPatientList(wardId: CurrentWardService().currentWard?.wardId); + _patientsByAssignmentStatus = await PatientService().getPatientList(); state = LoadingState.loaded; notifyListeners(); } diff --git a/apps/tasks/lib/dataclasses/bed.dart b/packages/helpwave_service/lib/src/api/tasks/data_types/bed.dart similarity index 84% rename from apps/tasks/lib/dataclasses/bed.dart rename to packages/helpwave_service/lib/src/api/tasks/data_types/bed.dart index df7d3a89..a988a270 100644 --- a/apps/tasks/lib/dataclasses/bed.dart +++ b/packages/helpwave_service/lib/src/api/tasks/data_types/bed.dart @@ -1,4 +1,4 @@ -import 'package:tasks/dataclasses/patient.dart'; +import 'package:helpwave_service/src/api/tasks/index.dart'; /// data class for [Bed] class BedMinimal { diff --git a/packages/helpwave_service/lib/src/api/tasks/data_types/index.dart b/packages/helpwave_service/lib/src/api/tasks/data_types/index.dart new file mode 100644 index 00000000..7b1fe9cc --- /dev/null +++ b/packages/helpwave_service/lib/src/api/tasks/data_types/index.dart @@ -0,0 +1,8 @@ +export 'ward.dart'; +export 'room.dart'; +export 'bed.dart'; +export 'patient.dart'; +export 'task.dart'; +export 'subtask.dart'; +export 'task_template.dart'; +export 'task_template_subtask.dart'; diff --git a/apps/tasks/lib/dataclasses/patient.dart b/packages/helpwave_service/lib/src/api/tasks/data_types/patient.dart similarity index 94% rename from apps/tasks/lib/dataclasses/patient.dart rename to packages/helpwave_service/lib/src/api/tasks/data_types/patient.dart index 31baa719..0942256b 100644 --- a/apps/tasks/lib/dataclasses/patient.dart +++ b/packages/helpwave_service/lib/src/api/tasks/data_types/patient.dart @@ -1,6 +1,4 @@ -import 'package:tasks/dataclasses/bed.dart'; -import 'package:tasks/dataclasses/room.dart'; -import 'package:tasks/dataclasses/task.dart'; +import 'package:helpwave_service/src/api/tasks/index.dart'; enum PatientAssignmentStatus { active, unassigned, discharged, all } diff --git a/apps/tasks/lib/dataclasses/room.dart b/packages/helpwave_service/lib/src/api/tasks/data_types/room.dart similarity index 94% rename from apps/tasks/lib/dataclasses/room.dart rename to packages/helpwave_service/lib/src/api/tasks/data_types/room.dart index 06775428..632ca7a1 100644 --- a/apps/tasks/lib/dataclasses/room.dart +++ b/packages/helpwave_service/lib/src/api/tasks/data_types/room.dart @@ -1,4 +1,4 @@ -import 'package:tasks/dataclasses/bed.dart'; +import 'package:helpwave_service/src/api/tasks/data_types/bed.dart'; /// data class for [Room] class RoomMinimal { diff --git a/packages/helpwave_service/lib/src/api/tasks/data_types/subtask.dart b/packages/helpwave_service/lib/src/api/tasks/data_types/subtask.dart new file mode 100644 index 00000000..f81a31dd --- /dev/null +++ b/packages/helpwave_service/lib/src/api/tasks/data_types/subtask.dart @@ -0,0 +1,27 @@ +/// Data class for a [Subtask] +class Subtask { + String id; + String name; + bool isDone; + + bool get isCreating => id == ""; + + Subtask({ + required this.id, + required this.name, + this.isDone = false + }); + + /// Create a copy of the [Subtask] + Subtask copyWith({ + String? id, + String? name, + bool? isDone, + }) { + return Subtask( + id: id ?? this.id, + name: name ?? this.name, + isDone: isDone ?? this.isDone, + ); + } +} diff --git a/apps/tasks/lib/dataclasses/task.dart b/packages/helpwave_service/lib/src/api/tasks/data_types/task.dart similarity index 93% rename from apps/tasks/lib/dataclasses/task.dart rename to packages/helpwave_service/lib/src/api/tasks/data_types/task.dart index b1633960..370ff4b2 100644 --- a/apps/tasks/lib/dataclasses/task.dart +++ b/packages/helpwave_service/lib/src/api/tasks/data_types/task.dart @@ -1,6 +1,6 @@ // TODO delete later and import from protobufs -import 'package:tasks/dataclasses/patient.dart'; -import 'package:tasks/dataclasses/subtask.dart'; +import 'package:helpwave_service/src/api/tasks/data_types/patient.dart'; +import 'package:helpwave_service/src/api/tasks/data_types/subtask.dart'; enum TaskStatus { unspecified, @@ -16,7 +16,7 @@ class Task { String? assignee; String notes; TaskStatus status; - List subtasks; + List subtasks; DateTime? dueDate; DateTime? creationDate; bool isPublicVisible; diff --git a/apps/tasks/lib/dataclasses/task_template.dart b/packages/helpwave_service/lib/src/api/tasks/data_types/task_template.dart similarity index 86% rename from apps/tasks/lib/dataclasses/task_template.dart rename to packages/helpwave_service/lib/src/api/tasks/data_types/task_template.dart index 35309363..8c61a31c 100644 --- a/apps/tasks/lib/dataclasses/task_template.dart +++ b/packages/helpwave_service/lib/src/api/tasks/data_types/task_template.dart @@ -1,4 +1,4 @@ -import 'package:tasks/dataclasses/task_template_subtask.dart'; +import '../index.dart'; /// data class for [TaskTemplate] class TaskTemplate { diff --git a/apps/tasks/lib/dataclasses/task_template_subtask.dart b/packages/helpwave_service/lib/src/api/tasks/data_types/task_template_subtask.dart similarity index 100% rename from apps/tasks/lib/dataclasses/task_template_subtask.dart rename to packages/helpwave_service/lib/src/api/tasks/data_types/task_template_subtask.dart diff --git a/apps/tasks/lib/dataclasses/ward.dart b/packages/helpwave_service/lib/src/api/tasks/data_types/ward.dart similarity index 100% rename from apps/tasks/lib/dataclasses/ward.dart rename to packages/helpwave_service/lib/src/api/tasks/data_types/ward.dart diff --git a/packages/helpwave_service/lib/src/api/tasks/index.dart b/packages/helpwave_service/lib/src/api/tasks/index.dart new file mode 100644 index 00000000..bebaf463 --- /dev/null +++ b/packages/helpwave_service/lib/src/api/tasks/index.dart @@ -0,0 +1,4 @@ +export 'data_types/index.dart'; +export 'services/index.dart'; +export 'controllers/index.dart'; +export 'tasks_api_services.dart'; diff --git a/packages/helpwave_service/lib/src/api/tasks/services/bed_svc.dart b/packages/helpwave_service/lib/src/api/tasks/services/bed_svc.dart new file mode 100644 index 00000000..e69de29b diff --git a/packages/helpwave_service/lib/src/api/tasks/services/index.dart b/packages/helpwave_service/lib/src/api/tasks/services/index.dart new file mode 100644 index 00000000..78922c72 --- /dev/null +++ b/packages/helpwave_service/lib/src/api/tasks/services/index.dart @@ -0,0 +1,5 @@ +export 'ward_service.dart'; +export 'room_svc.dart'; +export 'bed_svc.dart'; +export 'patient_svc.dart'; +export 'task_svc.dart'; diff --git a/apps/tasks/lib/services/patient_svc.dart b/packages/helpwave_service/lib/src/api/tasks/services/patient_svc.dart similarity index 77% rename from apps/tasks/lib/services/patient_svc.dart rename to packages/helpwave_service/lib/src/api/tasks/services/patient_svc.dart index c0c9ede6..c0c60d94 100644 --- a/apps/tasks/lib/services/patient_svc.dart +++ b/packages/helpwave_service/lib/src/api/tasks/services/patient_svc.dart @@ -1,21 +1,16 @@ import 'package:grpc/grpc.dart'; -import 'package:helpwave_proto_dart/proto/services/task_svc/v1/patient_svc.pbgrpc.dart'; -import 'package:tasks/dataclasses/bed.dart'; -import 'package:tasks/dataclasses/patient.dart'; -import 'package:tasks/dataclasses/room.dart'; -import 'package:tasks/dataclasses/subtask.dart'; -import 'package:tasks/dataclasses/ward.dart'; -import 'package:tasks/services/grpc_client_svc.dart'; +import 'package:helpwave_proto_dart/services/tasks_svc/v1/patient_svc.pbgrpc.dart'; +import 'package:helpwave_service/src/api/tasks/index.dart'; +import 'package:helpwave_service/src/api/tasks/util/task_status_mapping.dart'; -import '../dataclasses/task.dart'; /// The GRPC Service for [Patient]s /// /// Provides queries and requests that load or alter [Patient] objects on the server -/// The server is defined in the underlying [GRPCClientService] +/// The server is defined in the underlying [TasksAPIServices] class PatientService { /// The GRPC ServiceClient which handles GRPC - PatientServiceClient patientService = GRPCClientService.getPatientServiceClient; + PatientServiceClient patientService = TasksAPIServices.patientServiceClient; // TODO consider an enum instead of an string /// Loads the [Patient]s by [Ward] and sorts them by their assignment status @@ -24,17 +19,10 @@ class PatientService { GetPatientListResponse response = await patientService.getPatientList( request, options: CallOptions( - metadata: GRPCClientService().getTaskServiceMetaData(), + metadata: TasksAPIServices().getMetaData(), ), ); - Map taskStatusMapping = { - GetPatientListResponse_TaskStatus.TASK_STATUS_TODO: TaskStatus.todo, - GetPatientListResponse_TaskStatus.TASK_STATUS_IN_PROGRESS: TaskStatus.inProgress, - GetPatientListResponse_TaskStatus.TASK_STATUS_DONE: TaskStatus.done, - GetPatientListResponse_TaskStatus.TASK_STATUS_UNSPECIFIED: TaskStatus.unspecified, - }; - List active = response.active .map( (patient) => Patient( @@ -46,11 +34,11 @@ class PatientService { id: task.id, name: task.name, notes: task.description, - status: taskStatusMapping[task.status]!, + status: GRPCTypeConverter.taskStatusFromGRPC(task.status), isPublicVisible: task.public, assignee: task.assignedUserId, subtasks: task.subtasks - .map((subtask) => SubTask( + .map((subtask) => Subtask( id: subtask.id, name: subtask.name, isDone: subtask.done, @@ -77,11 +65,11 @@ class PatientService { id: task.id, name: task.name, notes: task.description, - status: taskStatusMapping[task.status]!, + status: GRPCTypeConverter.taskStatusFromGRPC(task.status), isPublicVisible: task.public, assignee: task.assignedUserId, subtasks: task.subtasks - .map((subtask) => SubTask( + .map((subtask) => Subtask( id: subtask.id, name: subtask.name, isDone: subtask.done, @@ -106,11 +94,11 @@ class PatientService { id: task.id, name: task.name, notes: task.description, - status: taskStatusMapping[task.status]!, + status: GRPCTypeConverter.taskStatusFromGRPC(task.status), isPublicVisible: task.public, assignee: task.assignedUserId, subtasks: task.subtasks - .map((subtask) => SubTask( + .map((subtask) => Subtask( id: subtask.id, name: subtask.name, isDone: subtask.done, @@ -138,7 +126,7 @@ class PatientService { GetPatientResponse response = await patientService.getPatient( request, options: CallOptions( - metadata: GRPCClientService().getTaskServiceMetaData(), + metadata: TasksAPIServices().getMetaData(), ), ); @@ -155,17 +143,10 @@ class PatientService { GetPatientDetailsResponse response = await patientService.getPatientDetails( request, options: CallOptions( - metadata: GRPCClientService().getTaskServiceMetaData(), + metadata: TasksAPIServices().getMetaData(), ), ); - Map statusMap = { - GetPatientDetailsResponse_TaskStatus.TASK_STATUS_TODO: TaskStatus.todo, - GetPatientDetailsResponse_TaskStatus.TASK_STATUS_IN_PROGRESS: TaskStatus.inProgress, - GetPatientDetailsResponse_TaskStatus.TASK_STATUS_DONE: TaskStatus.done, - GetPatientDetailsResponse_TaskStatus.TASK_STATUS_UNSPECIFIED: TaskStatus.unspecified, - }; - return Patient( id: response.id, name: response.humanReadableIdentifier, @@ -177,10 +158,10 @@ class PatientService { name: task.name, notes: task.description, assignee: task.assignedUserId, - status: statusMap[task.status]!, + status: GRPCTypeConverter.taskStatusFromGRPC(task.status), isPublicVisible: task.public, subtasks: task.subtasks - .map((subtask) => SubTask( + .map((subtask) => Subtask( id: subtask.id, name: subtask.name, )) @@ -198,7 +179,7 @@ class PatientService { GetPatientAssignmentByWardResponse response = await patientService.getPatientAssignmentByWard( request, options: CallOptions( - metadata: GRPCClientService().getTaskServiceMetaData(), + metadata: TasksAPIServices().getMetaData(), ), ); @@ -226,7 +207,7 @@ class PatientService { ); CreatePatientResponse response = await patientService.createPatient( request, - options: CallOptions(metadata: GRPCClientService().getTaskServiceMetaData()), + options: CallOptions(metadata: TasksAPIServices().getMetaData()), ); return response.id; @@ -241,7 +222,7 @@ class PatientService { ); UpdatePatientResponse response = await patientService.updatePatient( request, - options: CallOptions(metadata: GRPCClientService().getTaskServiceMetaData()), + options: CallOptions(metadata: TasksAPIServices().getMetaData()), ); if (response.isInitialized()) { @@ -256,7 +237,7 @@ class PatientService { DischargePatientRequest request = DischargePatientRequest(id: patientId); DischargePatientResponse response = await patientService.dischargePatient( request, - options: CallOptions(metadata: GRPCClientService().getTaskServiceMetaData()), + options: CallOptions(metadata: TasksAPIServices().getMetaData()), ); if (response.isInitialized()) { @@ -270,7 +251,7 @@ class PatientService { UnassignBedRequest request = UnassignBedRequest(id: patientId); UnassignBedResponse response = await patientService.unassignBed( request, - options: CallOptions(metadata: GRPCClientService().getTaskServiceMetaData()), + options: CallOptions(metadata: TasksAPIServices().getMetaData()), ); if (response.isInitialized()) { @@ -284,7 +265,7 @@ class PatientService { AssignBedRequest request = AssignBedRequest(id: patientId, bedId: bedId); AssignBedResponse response = await patientService.assignBed( request, - options: CallOptions(metadata: GRPCClientService().getTaskServiceMetaData()), + options: CallOptions(metadata: TasksAPIServices().getMetaData()), ); if (response.isInitialized()) { diff --git a/apps/tasks/lib/services/room_svc.dart b/packages/helpwave_service/lib/src/api/tasks/services/room_svc.dart similarity index 72% rename from apps/tasks/lib/services/room_svc.dart rename to packages/helpwave_service/lib/src/api/tasks/services/room_svc.dart index 48400564..ca4f3d3b 100644 --- a/apps/tasks/lib/services/room_svc.dart +++ b/packages/helpwave_service/lib/src/api/tasks/services/room_svc.dart @@ -1,23 +1,21 @@ import 'package:grpc/grpc.dart'; -import 'package:helpwave_proto_dart/proto/services/task_svc/v1/room_svc.pbgrpc.dart'; -import 'package:tasks/dataclasses/bed.dart'; -import 'package:tasks/dataclasses/patient.dart'; -import 'package:tasks/dataclasses/room.dart'; -import 'package:tasks/services/grpc_client_svc.dart'; +import 'package:helpwave_proto_dart/services/tasks_svc/v1/room_svc.pbgrpc.dart'; +import 'package:helpwave_service/src/api/tasks/data_types/index.dart'; +import 'package:helpwave_service/src/api/tasks/tasks_api_services.dart'; /// The GRPC Service for [Room]s /// /// Provides queries and requests that load or alter [Room] objects on the server -/// The server is defined in the underlying [GRPCClientService] +/// The server is defined in the underlying [TasksAPIServices] class RoomService { /// The GRPC ServiceClient which handles GRPC - RoomServiceClient roomService = GRPCClientService.getRoomServiceClient; + RoomServiceClient roomService = TasksAPIServices.roomServiceClient; Future> getRoomOverviews({required String wardId}) async { GetRoomOverviewsByWardRequest request = GetRoomOverviewsByWardRequest(id: wardId); GetRoomOverviewsByWardResponse response = await roomService.getRoomOverviewsByWard( request, - options: CallOptions(metadata: GRPCClientService().getTaskServiceMetaData()), + options: CallOptions(metadata: TasksAPIServices().getMetaData()), ); List rooms = response.rooms diff --git a/packages/helpwave_service/lib/src/api/tasks/services/task_svc.dart b/packages/helpwave_service/lib/src/api/tasks/services/task_svc.dart new file mode 100644 index 00000000..3d2cf292 --- /dev/null +++ b/packages/helpwave_service/lib/src/api/tasks/services/task_svc.dart @@ -0,0 +1,163 @@ +import 'package:grpc/grpc.dart'; +import 'package:helpwave_proto_dart/google/protobuf/timestamp.pb.dart'; +import 'package:helpwave_service/src/api/tasks/index.dart'; +import 'package:helpwave_proto_dart/services/tasks_svc/v1/task_svc.pbgrpc.dart'; +import '../util/task_status_mapping.dart'; + +/// The GRPC Service for [Task]s +/// +/// Provides queries and requests that load or alter [Task] objects on the server +/// The server is defined in the underlying [TasksAPIServices] +class TaskService { + /// The GRPC ServiceClient which handles GRPC + TaskServiceClient taskService = TasksAPIServices.taskServiceClient; + + /// Loads the [Task]s by a [Patient] identifier + Future> getTasksByPatient({String? patientId}) async { + GetTasksByPatientRequest request = GetTasksByPatientRequest(patientId: patientId); + GetTasksByPatientResponse response = await taskService.getTasksByPatient( + request, + options: CallOptions(metadata: TasksAPIServices().getMetaData()), + ); + + return response.tasks + .map((task) => Task( + id: task.id, + name: task.name, + notes: task.description, + isPublicVisible: task.public, + status: GRPCTypeConverter.taskStatusFromGRPC(task.status), + assignee: task.assignedUserId, + dueDate: task.dueAt.toDateTime(), + subtasks: task.subtasks + .map((subtask) => Subtask( + id: subtask.id, + name: subtask.name, + isDone: subtask.done, + )) + .toList(), + )) + .toList(); + } + + /// Loads the [Task]s by it's identifier + Future getTask({String? id}) async { + GetTaskRequest request = GetTaskRequest(id: id); + GetTaskResponse response = await taskService.getTask( + request, + options: CallOptions(metadata: TasksAPIServices().getMetaData()), + ); + + return TaskWithPatient( + id: response.id, + name: response.name, + notes: response.description, + isPublicVisible: response.public, + status: GRPCTypeConverter.taskStatusFromGRPC(response.status), + assignee: response.assignedUserId, + dueDate: response.dueAt.toDateTime(), + patient: PatientMinimal(id: response.patient.id, name: response.patient.humanReadableIdentifier), + subtasks: response.subtasks + .map((subtask) => Subtask( + id: subtask.id, + name: subtask.name, + isDone: subtask.done, + )) + .toList(), + ); + } + + Future createTask(TaskWithPatient task) async { + CreateTaskRequest request = CreateTaskRequest( + name: task.name, + description: task.notes, + initialStatus: GRPCTypeConverter.taskStatusToGRPC(task.status), + dueAt: task.dueDate != null ? Timestamp.fromDateTime(task.dueDate!) : null, + patientId: !task.patient.isCreating ? task.patient.id : null, + public: task.isPublicVisible, + ); + CreateTaskResponse response = await taskService.createTask( + request, + options: CallOptions(metadata: TasksAPIServices().getMetaData()), + ); + + return response.id; + } + + /// Assign a [Task] to a [User] + Future assignToUser({required String taskId, required String userId}) async { + AssignTaskRequest request = AssignTaskRequest(taskId: taskId, userId: userId); + AssignTaskResponse response = await taskService.assignTask( + request, + options: CallOptions(metadata: TasksAPIServices().getMetaData()), + ); + + if (!response.isInitialized()) { + // Handle error + } + } + + /// Add a [Subtask] to a [Task] + Future createSubTask({required String taskId, required Subtask subTask}) async { + CreateSubtaskRequest request = CreateSubtaskRequest( + taskId: taskId, + subtask: CreateSubtaskRequest_Subtask( + name: subTask.name, + done: subTask.isDone, + )); + CreateSubtaskResponse response = await taskService.createSubtask( + request, + options: CallOptions(metadata: TasksAPIServices().getMetaData()), + ); + + return Subtask( + id: response.subtaskId, + name: subTask.name, + isDone: subTask.isDone, + ); + } + + /// Delete a [Subtask] by its identifier + Future deleteSubTask({required String subtaskId, required String taskId}) async { + DeleteSubtaskRequest request = DeleteSubtaskRequest(subtaskId: subtaskId, taskId: taskId); + DeleteSubtaskResponse response = await taskService.deleteSubtask( + request, + options: CallOptions(metadata: TasksAPIServices().getMetaData()), + ); + + return response.isInitialized(); + } + + /// Update a [Subtask]'s + Future updateSubtask({required Subtask subtask, required taskId}) async { + UpdateSubtaskRequest request = UpdateSubtaskRequest( + taskId: taskId, + subtaskId: subtask.id, + subtask: UpdateSubtaskRequest_Subtask(done: subtask.isDone, name: subtask.name), + ); + UpdateSubtaskResponse response = await taskService.updateSubtask( + request, + options: CallOptions(metadata: TasksAPIServices().getMetaData()), + ); + + return response.isInitialized(); + } + + Future updateTask(Task task) async { + UpdateTaskRequest request = UpdateTaskRequest( + id: task.id, + name: task.name, + description: task.notes, + dueAt: task.dueDate != null ? Timestamp.fromDateTime(task.dueDate!) : null, + public: task.isPublicVisible, + status: GRPCTypeConverter.taskStatusToGRPC(task.status), + ); + + UpdateTaskResponse response = await taskService.updateTask( + request, + options: CallOptions(metadata: TasksAPIServices().getMetaData()), + ); + + return response.isInitialized(); + } +} diff --git a/apps/tasks/lib/services/ward_service.dart b/packages/helpwave_service/lib/src/api/tasks/services/ward_service.dart similarity index 59% rename from apps/tasks/lib/services/ward_service.dart rename to packages/helpwave_service/lib/src/api/tasks/services/ward_service.dart index 73ddcc9c..d34ebb15 100644 --- a/apps/tasks/lib/services/ward_service.dart +++ b/packages/helpwave_service/lib/src/api/tasks/services/ward_service.dart @@ -1,28 +1,27 @@ import 'package:grpc/grpc.dart'; -import 'package:helpwave_proto_dart/proto/services/task_svc/v1/ward_svc.pbgrpc.dart'; -import 'package:tasks/dataclasses/ward.dart'; -import 'package:tasks/services/grpc_client_svc.dart'; +import 'package:helpwave_proto_dart/services/tasks_svc/v1/ward_svc.pbgrpc.dart'; +import 'package:helpwave_service/src/api/tasks/data_types/index.dart'; +import 'package:helpwave_service/src/api/tasks/tasks_api_services.dart'; -/// The GRPC Service for [Ward]s +/// The Service for [Ward]s /// /// Provides queries and requests that load or alter [Ward] objects on the server -/// The server is defined in the underlying [GRPCClientService] +/// The server is defined in the underlying [TasksAPIServices] class WardService { /// The GRPC ServiceClient which handles GRPC - WardServiceClient wardService = GRPCClientService.getWardServiceClient; + WardServiceClient wardService = TasksAPIServices.wardServiceClient; - /// Loads a [Ward] by its identifier - Future getWard({required String id}) async { + /// Loads a [WardMinimal] by its identifier + Future getWard({required String id}) async { GetWardRequest request = GetWardRequest(id: id); GetWardResponse response = await wardService.getWard( request, - options: CallOptions(metadata: GRPCClientService().getTaskServiceMetaData()), + options: CallOptions(metadata: TasksAPIServices().getMetaData()), ); - return Ward( + return WardMinimal( id: response.id, name: response.name, - organizationId: response.organizationId, ); } @@ -31,7 +30,7 @@ class WardService { GetWardOverviewsRequest request = GetWardOverviewsRequest(); GetWardOverviewsResponse response = await wardService.getWardOverviews( request, - options: CallOptions(metadata: GRPCClientService().getTaskServiceMetaData(organizationId: organizationId)), + options: CallOptions(metadata: TasksAPIServices().getMetaData(organizationId: organizationId)), ); return response.wards diff --git a/packages/helpwave_service/lib/src/api/tasks/tasks_api_services.dart b/packages/helpwave_service/lib/src/api/tasks/tasks_api_services.dart new file mode 100644 index 00000000..1c09d123 --- /dev/null +++ b/packages/helpwave_service/lib/src/api/tasks/tasks_api_services.dart @@ -0,0 +1,42 @@ +import 'package:grpc/grpc.dart'; +import 'package:helpwave_proto_dart/services/tasks_svc/v1/ward_svc.pbgrpc.dart'; +import 'package:helpwave_proto_dart/services/tasks_svc/v1/patient_svc.pbgrpc.dart'; +import 'package:helpwave_proto_dart/services/tasks_svc/v1/room_svc.pbgrpc.dart'; +import 'package:helpwave_proto_dart/services/tasks_svc/v1/task_svc.pbgrpc.dart'; +import 'package:helpwave_service/src/auth/index.dart'; + +/// The Underlying GrpcService it provides other clients and the correct metadata for the requests +class TasksAPIServices { + /// The api URL used + static String? apiUrl; + + static ClientChannel get serviceChannel { + assert(TasksAPIServices.apiUrl != null); + return ClientChannel( + TasksAPIServices.apiUrl!, + ); + } + + Map getMetaData({String? organizationId}) { + var metaData = { + ...AuthenticationUtility.authMetaData, + "dapr-app-id": "task-svc", + }; + + if (organizationId != null) { + metaData["X-Organization"] = organizationId; + } else { + metaData["X-Organization"] = AuthenticationUtility.fallbackOrganizationId!; + } + + return metaData; + } + + static PatientServiceClient get patientServiceClient => PatientServiceClient(serviceChannel); + + static WardServiceClient get wardServiceClient => WardServiceClient(serviceChannel); + + static RoomServiceClient get roomServiceClient => RoomServiceClient(serviceChannel); + + static TaskServiceClient get taskServiceClient => TaskServiceClient(serviceChannel); +} diff --git a/packages/helpwave_service/lib/src/api/tasks/util/task_status_mapping.dart b/packages/helpwave_service/lib/src/api/tasks/util/task_status_mapping.dart new file mode 100644 index 00000000..97b65819 --- /dev/null +++ b/packages/helpwave_service/lib/src/api/tasks/util/task_status_mapping.dart @@ -0,0 +1,30 @@ +import 'package:helpwave_service/src/api/tasks/data_types/task.dart' as task_lib; +import 'package:helpwave_proto_dart/services/tasks_svc/v1/types.pbenum.dart' as proto; + +class GRPCTypeConverter { + static proto.TaskStatus taskStatusToGRPC(task_lib.TaskStatus status) { + switch (status) { + case task_lib.TaskStatus.todo: + return proto.TaskStatus.TASK_STATUS_TODO; + case task_lib.TaskStatus.inProgress: + return proto.TaskStatus.TASK_STATUS_IN_PROGRESS; + case task_lib.TaskStatus.done: + return proto.TaskStatus.TASK_STATUS_DONE; + case task_lib.TaskStatus.unspecified: + return proto.TaskStatus.TASK_STATUS_UNSPECIFIED; + } + } + + static task_lib.TaskStatus taskStatusFromGRPC(proto.TaskStatus status) { + switch (status) { + case proto.TaskStatus.TASK_STATUS_TODO: + return task_lib.TaskStatus.todo; + case proto.TaskStatus.TASK_STATUS_IN_PROGRESS: + return task_lib.TaskStatus.inProgress; + case proto.TaskStatus.TASK_STATUS_DONE: + return task_lib.TaskStatus.done; + default: + return task_lib.TaskStatus.unspecified; + } + } +} diff --git a/packages/helpwave_service/lib/src/api/user/controllers/index.dart b/packages/helpwave_service/lib/src/api/user/controllers/index.dart new file mode 100644 index 00000000..dcd72ff4 --- /dev/null +++ b/packages/helpwave_service/lib/src/api/user/controllers/index.dart @@ -0,0 +1 @@ +export 'user_controller.dart'; diff --git a/apps/tasks/lib/controllers/user_controller.dart b/packages/helpwave_service/lib/src/api/user/controllers/user_controller.dart similarity index 91% rename from apps/tasks/lib/controllers/user_controller.dart rename to packages/helpwave_service/lib/src/api/user/controllers/user_controller.dart index 518ef281..dce15479 100644 --- a/apps/tasks/lib/controllers/user_controller.dart +++ b/packages/helpwave_service/lib/src/api/user/controllers/user_controller.dart @@ -1,7 +1,6 @@ import 'package:flutter/material.dart'; -import 'package:helpwave_widget/loading.dart'; -import 'package:tasks/services/user_service.dart'; -import '../dataclasses/user.dart'; +import 'package:helpwave_util/loading_state.dart'; +import '../index.dart'; /// The Controller for managing a [User] class UserController extends ChangeNotifier { diff --git a/packages/helpwave_service/lib/src/api/user/data_types/index.dart b/packages/helpwave_service/lib/src/api/user/data_types/index.dart new file mode 100644 index 00000000..7d071f5a --- /dev/null +++ b/packages/helpwave_service/lib/src/api/user/data_types/index.dart @@ -0,0 +1,2 @@ +export 'user.dart'; +export 'organization.dart'; diff --git a/apps/tasks/lib/dataclasses/organization.dart b/packages/helpwave_service/lib/src/api/user/data_types/organization.dart similarity index 100% rename from apps/tasks/lib/dataclasses/organization.dart rename to packages/helpwave_service/lib/src/api/user/data_types/organization.dart diff --git a/apps/tasks/lib/dataclasses/user.dart b/packages/helpwave_service/lib/src/api/user/data_types/user.dart similarity index 100% rename from apps/tasks/lib/dataclasses/user.dart rename to packages/helpwave_service/lib/src/api/user/data_types/user.dart diff --git a/packages/helpwave_service/lib/src/api/user/index.dart b/packages/helpwave_service/lib/src/api/user/index.dart new file mode 100644 index 00000000..243eabf0 --- /dev/null +++ b/packages/helpwave_service/lib/src/api/user/index.dart @@ -0,0 +1,4 @@ +export 'data_types/index.dart'; +export 'services/index.dart'; +export 'controllers/index.dart'; +export 'user_api_services.dart'; diff --git a/packages/helpwave_service/lib/src/api/user/services/index.dart b/packages/helpwave_service/lib/src/api/user/services/index.dart new file mode 100644 index 00000000..01052c51 --- /dev/null +++ b/packages/helpwave_service/lib/src/api/user/services/index.dart @@ -0,0 +1,2 @@ +export 'organization_svc.dart'; +export 'user_service.dart'; diff --git a/apps/tasks/lib/services/organization_svc.dart b/packages/helpwave_service/lib/src/api/user/services/organization_svc.dart similarity index 74% rename from apps/tasks/lib/services/organization_svc.dart rename to packages/helpwave_service/lib/src/api/user/services/organization_svc.dart index 9db077c9..0b4283f8 100644 --- a/apps/tasks/lib/services/organization_svc.dart +++ b/packages/helpwave_service/lib/src/api/user/services/organization_svc.dart @@ -1,23 +1,23 @@ import 'package:grpc/grpc.dart'; -import 'package:helpwave_proto_dart/proto/services/user_svc/v1/organization_svc.pbgrpc.dart'; -import 'package:tasks/dataclasses/organization.dart'; -import 'package:tasks/dataclasses/user.dart'; -import 'package:tasks/services/grpc_client_svc.dart'; +import 'package:helpwave_proto_dart/services/user_svc/v1/organization_svc.pbgrpc.dart'; +import 'package:helpwave_service/auth.dart'; +import 'package:helpwave_service/src/api/user/user_api_services.dart'; +import '../data_types/index.dart'; /// The GRPC Service for [Organization]s /// /// Provides queries and requests that load or alter [Organization] objects on the server -/// The server is defined in the underlying [GRPCClientService] +/// The server is defined in the underlying [UserAPIServices] class OrganizationService { /// The GRPC ServiceClient which handles GRPC - OrganizationServiceClient organizationService = GRPCClientService.getOrganizationServiceClient; + OrganizationServiceClient organizationService = UserAPIServices.organizationServiceClient; /// Load a Organization by its identifier Future getOrganization({required String id}) async { GetOrganizationRequest request = GetOrganizationRequest(id: id); GetOrganizationResponse response = await organizationService.getOrganization( request, - options: CallOptions(metadata: GRPCClientService().getUserServiceMetaData(organizationId: id)), + options: CallOptions(metadata: UserAPIServices.getMetaData(organizationId: id)), ); // TODO use full information of request @@ -35,8 +35,8 @@ class OrganizationService { GetOrganizationsForUserResponse response = await organizationService.getOrganizationsForUser( request, options: CallOptions( - metadata: GRPCClientService().getUserServiceMetaData( - organizationId: GRPCClientService().fallbackOrganizationId, + metadata: UserAPIServices.getMetaData( + organizationId: AuthenticationUtility.fallbackOrganizationId, ), ), ); @@ -58,7 +58,7 @@ class OrganizationService { GetMembersByOrganizationResponse response = await organizationService.getMembersByOrganization( request, options: CallOptions( - metadata: GRPCClientService().getUserServiceMetaData(organizationId: organizationId), + metadata: UserAPIServices.getMetaData(organizationId: organizationId), ), ); diff --git a/apps/tasks/lib/services/user_service.dart b/packages/helpwave_service/lib/src/api/user/services/user_service.dart similarity index 59% rename from apps/tasks/lib/services/user_service.dart rename to packages/helpwave_service/lib/src/api/user/services/user_service.dart index fedb8899..a667b4cb 100644 --- a/apps/tasks/lib/services/user_service.dart +++ b/packages/helpwave_service/lib/src/api/user/services/user_service.dart @@ -1,15 +1,16 @@ import 'package:grpc/grpc.dart'; -import 'package:helpwave_proto_dart/proto/services/user_svc/v1/user_svc.pbgrpc.dart'; -import 'package:tasks/services/grpc_client_svc.dart'; -import '../dataclasses/user.dart'; +import 'package:helpwave_proto_dart/services/user_svc/v1/user_svc.pbgrpc.dart'; +import 'package:helpwave_service/auth.dart'; +import 'package:helpwave_service/src/api/user/user_api_services.dart'; +import '../data_types/index.dart'; /// The GRPC Service for [User]s /// /// Provides queries and requests that load or alter [User] objects on the server -/// The server is defined in the underlying [GRPCClientService] +/// The server is defined in the underlying [UserAPIServices] class UserService { /// The GRPC ServiceClient which handles GRPC - UserServiceClient userService = GRPCClientService.getUserServiceClient; + UserServiceClient userService = UserAPIServices.userServiceClient; /// Loads the [User]s by it's identifier Future getUser({String? id}) async { @@ -17,8 +18,8 @@ class UserService { ReadPublicProfileResponse response = await userService.readPublicProfile( request, options: CallOptions( - metadata: GRPCClientService().getUserServiceMetaData( - organizationId: GRPCClientService().fallbackOrganizationId, + metadata: UserAPIServices.getMetaData( + organizationId: AuthenticationUtility.fallbackOrganizationId, ), ), ); diff --git a/packages/helpwave_service/lib/src/api/user/user_api_services.dart b/packages/helpwave_service/lib/src/api/user/user_api_services.dart new file mode 100644 index 00000000..695cb5f9 --- /dev/null +++ b/packages/helpwave_service/lib/src/api/user/user_api_services.dart @@ -0,0 +1,36 @@ +import 'package:grpc/grpc.dart'; +import 'package:helpwave_proto_dart/services/user_svc/v1/user_svc.pbgrpc.dart'; +import 'package:helpwave_proto_dart/services/user_svc/v1/organization_svc.pbgrpc.dart'; +import 'package:helpwave_service/src/auth/index.dart'; + +/// A bundling of all User API services which can be used and are configured +/// +/// Make sure to set the [apiURL] to use the services +class UserAPIServices { + /// The api URL used + static String? apiUrl; + + static ClientChannel get serviceChannel { + assert(UserAPIServices.apiUrl != null); + return ClientChannel( + UserAPIServices.apiUrl!, + ); + } + + static Map getMetaData({String? organizationId}) { + var metaData = { + ...AuthenticationUtility.authMetaData, + "dapr-app-id": "user-svc", + }; + + if (organizationId != null) { + metaData["X-Organization"] = organizationId; + } + + return metaData; + } + + static UserServiceClient get userServiceClient => UserServiceClient(serviceChannel); + + static OrganizationServiceClient get organizationServiceClient => OrganizationServiceClient(serviceChannel); +} diff --git a/packages/helpwave_service/lib/src/auth/authentication_utility.dart b/packages/helpwave_service/lib/src/auth/authentication_utility.dart new file mode 100644 index 00000000..488e4558 --- /dev/null +++ b/packages/helpwave_service/lib/src/auth/authentication_utility.dart @@ -0,0 +1,18 @@ +import 'package:helpwave_service/auth.dart'; + +class AuthenticationUtility { + static Map get authMetaData { + UserSessionService sessionService = UserSessionService(); + if (sessionService.isLoggedIn) { + return { + "Authorization": "Bearer ${sessionService.identity?.idToken}", + }; + } + // Maybe throw a error instead + return {}; + } + + static String? get fallbackOrganizationId => + // Maybe throw a error instead for the last case + CurrentWardService().currentWard?.organizationId ?? UserSessionService().identity?.firstOrganization; +} diff --git a/apps/tasks/lib/services/current_ward_svc.dart b/packages/helpwave_service/lib/src/auth/current_ward_svc.dart similarity index 95% rename from apps/tasks/lib/services/current_ward_svc.dart rename to packages/helpwave_service/lib/src/auth/current_ward_svc.dart index 8711d471..8413e807 100644 --- a/apps/tasks/lib/services/current_ward_svc.dart +++ b/packages/helpwave_service/lib/src/auth/current_ward_svc.dart @@ -1,10 +1,7 @@ import 'package:flutter/foundation.dart'; import 'package:shared_preferences/shared_preferences.dart'; -import 'package:tasks/config/config.dart'; -import 'package:tasks/dataclasses/organization.dart'; -import 'package:tasks/dataclasses/ward.dart'; -import 'package:tasks/services/organization_svc.dart'; -import 'package:tasks/services/ward_service.dart'; +import 'package:helpwave_service/src/api/tasks/index.dart'; +import 'package:helpwave_service/src/api/user/index.dart'; /// A readonly class for getting the CurrentWard information class CurrentWardInformation { @@ -76,6 +73,8 @@ class _CurrentWardPreferences { /// /// Changes the [CurrentWardInformation] globally class CurrentWardService extends Listenable { + bool devMode = false; // TODO remove + /// A storage for the current ward final _CurrentWardPreferences _preferences = _CurrentWardPreferences(); @@ -169,7 +168,8 @@ class CurrentWardController extends ChangeNotifier { /// Whether this Controller has been initialized bool get isInitialized => service.isInitialized; - CurrentWardController() { + CurrentWardController({bool devMode = false}) { + service.devMode = devMode; service.addListener(notifyListeners); if (!service.isInitialized) { load(); diff --git a/packages/helpwave_service/lib/src/auth/index.dart b/packages/helpwave_service/lib/src/auth/index.dart new file mode 100644 index 00000000..27d7f07d --- /dev/null +++ b/packages/helpwave_service/lib/src/auth/index.dart @@ -0,0 +1,6 @@ +export 'authentication_service.dart' show AuthenticationService; +export 'identity.dart'; +export 'user_session_controller.dart'; +export 'user_session_service.dart'; +export 'authentication_utility.dart'; +export 'current_ward_svc.dart'; diff --git a/apps/tasks/lib/controllers/user_session_controller.dart b/packages/helpwave_service/lib/src/auth/user_session_controller.dart similarity index 94% rename from apps/tasks/lib/controllers/user_session_controller.dart rename to packages/helpwave_service/lib/src/auth/user_session_controller.dart index 1d44ddf0..f7238230 100644 --- a/apps/tasks/lib/controllers/user_session_controller.dart +++ b/packages/helpwave_service/lib/src/auth/user_session_controller.dart @@ -1,5 +1,5 @@ import 'package:flutter/foundation.dart'; -import 'package:tasks/services/user_session_service.dart'; +import 'package:helpwave_service/src/auth/user_session_service.dart'; /// A Controller for providing and updating the current user session class UserSessionController extends ChangeNotifier { diff --git a/apps/tasks/lib/services/user_session_service.dart b/packages/helpwave_service/lib/src/auth/user_session_service.dart similarity index 87% rename from apps/tasks/lib/services/user_session_service.dart rename to packages/helpwave_service/lib/src/auth/user_session_service.dart index 6df1ee87..1e4a40a7 100644 --- a/apps/tasks/lib/services/user_session_service.dart +++ b/packages/helpwave_service/lib/src/auth/user_session_service.dart @@ -1,6 +1,4 @@ import 'package:helpwave_service/auth.dart'; -import 'package:tasks/config/config.dart'; -import 'package:tasks/services/current_ward_svc.dart'; /// The class for storing an managing the user session class UserSessionService { @@ -10,6 +8,9 @@ class UserSessionService { /// Whether the stored tokens have already been used for authentication bool _hasTriedTokens = false; + /// Whether this service should run in development mode + bool _devMode = false; + final AuthenticationService _authService = AuthenticationService(); static final UserSessionService _userSessionService = UserSessionService._ensureInitialized(); @@ -24,6 +25,11 @@ class UserSessionService { bool get hasTriedTokens => _hasTriedTokens; + bool get devMode => _devMode; + + /// **Only use this** once before using the service + changeMode(bool isDevMode) => _devMode = isDevMode; + /// Logs a User in by using the stored tokens /// /// Sets the [hasTriedTokens] to true diff --git a/packages/helpwave_service/lib/tasks.dart b/packages/helpwave_service/lib/tasks.dart new file mode 100644 index 00000000..fabf21ad --- /dev/null +++ b/packages/helpwave_service/lib/tasks.dart @@ -0,0 +1 @@ +export 'package:helpwave_service/src/api/tasks/index.dart'; diff --git a/packages/helpwave_service/lib/user.dart b/packages/helpwave_service/lib/user.dart new file mode 100644 index 00000000..24d6abf3 --- /dev/null +++ b/packages/helpwave_service/lib/user.dart @@ -0,0 +1 @@ +export 'package:helpwave_service/src/api/user/index.dart'; diff --git a/packages/helpwave_service/pubspec.yaml b/packages/helpwave_service/pubspec.yaml index 1639ab95..cce05101 100644 --- a/packages/helpwave_service/pubspec.yaml +++ b/packages/helpwave_service/pubspec.yaml @@ -2,6 +2,7 @@ name: helpwave_service description: A package for the helpwave services version: 0.0.1 homepage: https://github.com/helpwave/mobile-app +publish_to: none environment: sdk: '>=3.1.0' @@ -16,6 +17,10 @@ dependencies: flutter_secure_storage: 9.0.0 jose: ^0.3.4 logger: ^2.0.2+1 + helpwave_proto_dart: ^0.46.0-336429a + grpc: ^3.2.4 + helpwave_util: + path: "../helpwave_util" dev_dependencies: flutter_test: @@ -24,4 +29,4 @@ dev_dependencies: flutter: - + # flutter config diff --git a/packages/helpwave_theme/lib/src/dark_theme.dart b/packages/helpwave_theme/lib/src/theme/dark_theme.dart similarity index 97% rename from packages/helpwave_theme/lib/src/dark_theme.dart rename to packages/helpwave_theme/lib/src/theme/dark_theme.dart index 423474e1..5002ff66 100644 --- a/packages/helpwave_theme/lib/src/dark_theme.dart +++ b/packages/helpwave_theme/lib/src/theme/dark_theme.dart @@ -1,6 +1,6 @@ import 'package:flutter/material.dart'; -import 'package:helpwave_theme/src/theme.dart'; -import 'constants.dart'; +import 'package:helpwave_theme/src/theme/theme.dart'; +import '../constants.dart'; const primaryColor = Color.fromARGB(255, 255, 255, 255); const onPrimaryColor = Color.fromARGB(255, 0, 0, 0); diff --git a/packages/helpwave_theme/lib/src/light_theme.dart b/packages/helpwave_theme/lib/src/theme/light_theme.dart similarity index 97% rename from packages/helpwave_theme/lib/src/light_theme.dart rename to packages/helpwave_theme/lib/src/theme/light_theme.dart index fc57e12d..72e708cf 100644 --- a/packages/helpwave_theme/lib/src/light_theme.dart +++ b/packages/helpwave_theme/lib/src/theme/light_theme.dart @@ -1,6 +1,6 @@ import 'package:flutter/material.dart'; -import 'package:helpwave_theme/src/theme.dart'; -import 'constants.dart'; +import 'package:helpwave_theme/src/theme/theme.dart'; +import '../constants.dart'; const primaryColor = Color.fromARGB(255, 0, 0, 0); const onPrimaryColor = Color.fromARGB(255, 255, 255, 255); diff --git a/packages/helpwave_theme/lib/src/theme.dart b/packages/helpwave_theme/lib/src/theme/theme.dart similarity index 99% rename from packages/helpwave_theme/lib/src/theme.dart rename to packages/helpwave_theme/lib/src/theme/theme.dart index bb97803f..a9a27fd7 100644 --- a/packages/helpwave_theme/lib/src/theme.dart +++ b/packages/helpwave_theme/lib/src/theme/theme.dart @@ -1,6 +1,6 @@ import 'package:flutter/material.dart'; import 'package:helpwave_util/material_state.dart'; -import '../constants.dart'; +import '../../constants.dart'; // A function to map incoming colors to a theme ThemeData makeTheme({ diff --git a/packages/helpwave_theme/lib/src/util/context_extension.dart b/packages/helpwave_theme/lib/src/util/context_extension.dart new file mode 100644 index 00000000..c5841c77 --- /dev/null +++ b/packages/helpwave_theme/lib/src/util/context_extension.dart @@ -0,0 +1,5 @@ +import 'package:flutter/material.dart'; + +extension BuildContextThemeExtension on BuildContext { + ThemeData get theme => Theme.of(this); +} diff --git a/packages/helpwave_theme/lib/src/util/index.dart b/packages/helpwave_theme/lib/src/util/index.dart new file mode 100644 index 00000000..a4351565 --- /dev/null +++ b/packages/helpwave_theme/lib/src/util/index.dart @@ -0,0 +1,2 @@ +export 'context_extension.dart'; +export 'material_state_color_resolver.dart'; diff --git a/packages/helpwave_theme/lib/src/material_state_color_resolver.dart b/packages/helpwave_theme/lib/src/util/material_state_color_resolver.dart similarity index 100% rename from packages/helpwave_theme/lib/src/material_state_color_resolver.dart rename to packages/helpwave_theme/lib/src/util/material_state_color_resolver.dart diff --git a/packages/helpwave_theme/lib/theme.dart b/packages/helpwave_theme/lib/theme.dart index af2d8684..65327fb0 100644 --- a/packages/helpwave_theme/lib/theme.dart +++ b/packages/helpwave_theme/lib/theme.dart @@ -1,3 +1,3 @@ -export "package:helpwave_theme/src/light_theme.dart" show lightTheme; -export "package:helpwave_theme/src/dark_theme.dart" show darkTheme; +export 'package:helpwave_theme/src/theme/light_theme.dart' show lightTheme; +export 'package:helpwave_theme/src/theme/dark_theme.dart' show darkTheme; export "package:helpwave_theme/src/theme_model.dart"; diff --git a/packages/helpwave_theme/lib/util.dart b/packages/helpwave_theme/lib/util.dart index 38c23504..4ef38e09 100644 --- a/packages/helpwave_theme/lib/util.dart +++ b/packages/helpwave_theme/lib/util.dart @@ -1 +1 @@ -export "package:helpwave_theme/src/material_state_color_resolver.dart"; +export 'package:helpwave_theme/src/util/index.dart'; diff --git a/packages/helpwave_util/lib/loading_state.dart b/packages/helpwave_util/lib/loading_state.dart new file mode 100644 index 00000000..b47bbe00 --- /dev/null +++ b/packages/helpwave_util/lib/loading_state.dart @@ -0,0 +1 @@ +export 'package:helpwave_util/src/loading_state/type.dart'; diff --git a/packages/helpwave_util/lib/search.dart b/packages/helpwave_util/lib/search.dart new file mode 100644 index 00000000..dc90568d --- /dev/null +++ b/packages/helpwave_util/lib/search.dart @@ -0,0 +1 @@ +export 'package:helpwave_util/src/search/search_helpers.dart'; diff --git a/packages/helpwave_util/lib/src/loading_state/type.dart b/packages/helpwave_util/lib/src/loading_state/type.dart new file mode 100644 index 00000000..137a5f9b --- /dev/null +++ b/packages/helpwave_util/lib/src/loading_state/type.dart @@ -0,0 +1,16 @@ +enum LoadingState { + /// The data is initializing + initializing, + + /// The data is loaded + loaded, + + /// The data is currently loading + loading, + + /// Loading the data produced an error + error, + + /// There is no loading state, meaning ignore the LoadingState + unspecified, +} diff --git a/apps/tasks/lib/util/search_helpers.dart b/packages/helpwave_util/lib/src/search/search_helpers.dart similarity index 100% rename from apps/tasks/lib/util/search_helpers.dart rename to packages/helpwave_util/lib/src/search/search_helpers.dart diff --git a/packages/helpwave_widget/lib/src/loading/loading_and_error_widget.dart b/packages/helpwave_widget/lib/src/loading/loading_and_error_widget.dart index 51281eb2..d9b26fc7 100644 --- a/packages/helpwave_widget/lib/src/loading/loading_and_error_widget.dart +++ b/packages/helpwave_widget/lib/src/loading/loading_and_error_widget.dart @@ -1,24 +1,8 @@ import 'package:flutter/cupertino.dart'; import 'package:helpwave_theme/constants.dart'; +import 'package:helpwave_util/loading_state.dart'; import 'package:helpwave_widget/loading.dart'; -enum LoadingState { - /// The date is initializing - initializing, - - /// The date is loaded - loaded, - - /// The date is currently loading - loading, - - /// The loading produced an error - error, - - /// There is no loading state, meaning ignore the LoadingState - unspecified, -} - /// A [Widget] to show different [Widget]s depending on the [LoadingState] class LoadingAndErrorWidget extends StatelessWidget { /// The [LoadingState] is used to determine the shown [Widget] diff --git a/packages/helpwave_widget/lib/src/loading/loading_future_builder.dart b/packages/helpwave_widget/lib/src/loading/loading_future_builder.dart index 7b5f7704..ccfb938e 100644 --- a/packages/helpwave_widget/lib/src/loading/loading_future_builder.dart +++ b/packages/helpwave_widget/lib/src/loading/loading_future_builder.dart @@ -1,4 +1,5 @@ import 'package:flutter/cupertino.dart'; +import 'package:helpwave_util/loading_state.dart'; import 'package:helpwave_widget/loading.dart'; /// A Wrapper for the standard [FutureBuilder] to easily distinguish the three diff --git a/packages/helpwave_widget/pubspec.yaml b/packages/helpwave_widget/pubspec.yaml index c0e91e36..bf60e6d9 100644 --- a/packages/helpwave_widget/pubspec.yaml +++ b/packages/helpwave_widget/pubspec.yaml @@ -25,3 +25,4 @@ dev_dependencies: flutter_lints: ^2.0.0 flutter: + # flutter config From ae7722af9d4d6300e69e78d068c7973968995106 Mon Sep 17 00:00:00 2001 From: Felix Thape Date: Sun, 15 Sep 2024 18:33:00 +0200 Subject: [PATCH 02/36] feat: draft for offline mode --- .../tasks/lib/components/assignee_select.dart | 2 +- .../lib/components/task_bottom_sheet.dart | 4 +- apps/tasks/lib/main.dart | 2 +- .../tasks/lib/screens/ward_select_screen.dart | 4 +- .../src/api/offline/offline_client_store.dart | 93 ++++ .../lib/src/api/offline/util.dart | 48 ++ .../controllers/my_tasks_controller.dart | 2 +- .../tasks/controllers/task_controller.dart | 6 +- .../lib/src/api/tasks/data_types/bed.dart | 6 + .../lib/src/api/tasks/data_types/patient.dart | 37 +- .../lib/src/api/tasks/data_types/room.dart | 6 + .../lib/src/api/tasks/data_types/subtask.dart | 11 +- .../lib/src/api/tasks/data_types/task.dart | 70 ++- .../api/tasks/data_types/task_template.dart | 28 +- .../lib/src/api/tasks/index.dart | 2 +- .../offline_clients/bed_offline_client.dart | 178 +++++++ .../patient_offline_client.dart | 366 ++++++++++++++ .../offline_clients/room_offline_client.dart | 161 +++++++ .../offline_clients/task_offline_client.dart | 445 ++++++++++++++++++ .../template_offline_client.dart | 241 ++++++++++ .../offline_clients/ward_offline_client.dart | 162 +++++++ .../src/api/tasks/services/patient_svc.dart | 32 +- .../lib/src/api/tasks/services/room_svc.dart | 8 +- .../lib/src/api/tasks/services/task_svc.dart | 22 +- .../src/api/tasks/services/ward_service.dart | 10 +- ...es.dart => tasks_api_service_clients.dart} | 6 +- .../src/api/user/data_types/organization.dart | 49 +- .../lib/src/api/user/data_types/user.dart | 18 +- .../lib/src/api/user/index.dart | 2 +- .../organization_offline_client.dart | 271 +++++++++++ .../offline_clients/user_offline_client.dart | 119 +++++ .../api/user/services/organization_svc.dart | 39 +- .../src/api/user/services/user_service.dart | 11 +- ...ces.dart => user_api_service_clients.dart} | 6 +- .../lib/src/auth/current_ward_svc.dart | 8 +- packages/helpwave_util/lib/lists.dart | 1 + .../helpwave_util/lib/src/lists/range.dart | 11 + 37 files changed, 2368 insertions(+), 119 deletions(-) create mode 100644 packages/helpwave_service/lib/src/api/offline/offline_client_store.dart create mode 100644 packages/helpwave_service/lib/src/api/offline/util.dart create mode 100644 packages/helpwave_service/lib/src/api/tasks/offline_clients/bed_offline_client.dart create mode 100644 packages/helpwave_service/lib/src/api/tasks/offline_clients/patient_offline_client.dart create mode 100644 packages/helpwave_service/lib/src/api/tasks/offline_clients/room_offline_client.dart create mode 100644 packages/helpwave_service/lib/src/api/tasks/offline_clients/task_offline_client.dart create mode 100644 packages/helpwave_service/lib/src/api/tasks/offline_clients/template_offline_client.dart create mode 100644 packages/helpwave_service/lib/src/api/tasks/offline_clients/ward_offline_client.dart rename packages/helpwave_service/lib/src/api/tasks/{tasks_api_services.dart => tasks_api_service_clients.dart} (92%) create mode 100644 packages/helpwave_service/lib/src/api/user/offline_clients/organization_offline_client.dart create mode 100644 packages/helpwave_service/lib/src/api/user/offline_clients/user_offline_client.dart rename packages/helpwave_service/lib/src/api/user/{user_api_services.dart => user_api_service_clients.dart} (89%) create mode 100644 packages/helpwave_util/lib/lists.dart create mode 100644 packages/helpwave_util/lib/src/lists/range.dart diff --git a/apps/tasks/lib/components/assignee_select.dart b/apps/tasks/lib/components/assignee_select.dart index 311999fb..08de7ad8 100644 --- a/apps/tasks/lib/components/assignee_select.dart +++ b/apps/tasks/lib/components/assignee_select.dart @@ -36,7 +36,7 @@ class AssigneeSelect extends StatelessWidget { }); }, leading: CircleAvatar( - foregroundColor: Colors.blue, backgroundImage: NetworkImage(user.profile.toString())), + foregroundColor: Colors.blue, backgroundImage: NetworkImage(user.profileUrl.toString())), title: Text(user.nickName), ); }, diff --git a/apps/tasks/lib/components/task_bottom_sheet.dart b/apps/tasks/lib/components/task_bottom_sheet.dart index d01138eb..44e62720 100644 --- a/apps/tasks/lib/components/task_bottom_sheet.dart +++ b/apps/tasks/lib/components/task_bottom_sheet.dart @@ -211,7 +211,7 @@ class _TaskBottomSheetState extends State { state: taskController.state, child: ChangeNotifierProvider( create: (BuildContext context) => AssigneeSelectController( - selected: taskController.task.assignee, + selected: taskController.task.assigneeId, taskId: taskController.task.id, ), child: AssigneeSelect( @@ -228,7 +228,7 @@ class _TaskBottomSheetState extends State { state: taskController.state, child: taskController.task.hasAssignee ? ChangeNotifierProvider( - create: (context) => UserController(User.empty(id: taskController.task.assignee!)), + create: (context) => UserController(User.empty(id: taskController.task.assigneeId!)), child: Consumer( builder: (context, userController, __) => LoadingAndErrorWidget.pulsing( state: userController.state, diff --git a/apps/tasks/lib/main.dart b/apps/tasks/lib/main.dart index e5b4ad0f..5a4da007 100644 --- a/apps/tasks/lib/main.dart +++ b/apps/tasks/lib/main.dart @@ -11,7 +11,7 @@ import 'package:tasks/screens/login_screen.dart'; void main() { UserSessionService().changeMode(devMode); - TasksAPIServices.apiUrl = usedAPIURL; + TasksAPIServiceClients.apiUrl = usedAPIURL; runApp(const MyApp()); } diff --git a/apps/tasks/lib/screens/ward_select_screen.dart b/apps/tasks/lib/screens/ward_select_screen.dart index 7e7248c7..73d0018c 100644 --- a/apps/tasks/lib/screens/ward_select_screen.dart +++ b/apps/tasks/lib/screens/ward_select_screen.dart @@ -43,7 +43,7 @@ class _WardSelectScreen extends State { children: [ ListTile( // TODO change to organization name - title: Text(organization?.name ?? context.localization!.none), + title: Text(organization?.longName ?? context.localization!.none), subtitle: Text(context.localization!.organization), trailing: const Icon(Icons.arrow_forward), onTap: () => Navigator.push( @@ -55,7 +55,7 @@ class _WardSelectScreen extends State { List organizations = await OrganizationService().getOrganizationsForUser(); return organizations; }, - elementToString: (Organization t) => t.name, + elementToString: (Organization t) => t.longName, ), ), ).then((value) { diff --git a/packages/helpwave_service/lib/src/api/offline/offline_client_store.dart b/packages/helpwave_service/lib/src/api/offline/offline_client_store.dart new file mode 100644 index 00000000..3fe8d420 --- /dev/null +++ b/packages/helpwave_service/lib/src/api/offline/offline_client_store.dart @@ -0,0 +1,93 @@ +import 'package:helpwave_service/src/api/tasks/offline_clients/bed_offline_client.dart'; +import 'package:helpwave_service/src/api/tasks/offline_clients/patient_offline_client.dart'; +import 'package:helpwave_service/src/api/tasks/offline_clients/room_offline_client.dart'; +import 'package:helpwave_service/src/api/tasks/offline_clients/task_offline_client.dart'; +import 'package:helpwave_service/src/api/tasks/offline_clients/template_offline_client.dart'; +import 'package:helpwave_service/src/api/tasks/offline_clients/ward_offline_client.dart'; +import 'package:helpwave_service/src/api/user/offline_clients/organization_offline_client.dart'; +import 'package:helpwave_service/src/api/user/offline_clients/user_offline_client.dart'; +import '../../../user.dart'; + +const String profileUrl = "https://helpwave.de/favicon.ico"; + +final List initialOrganizations = [ + Organization( + id: "organization1", + shortName: "Test", + longName: "Test Organization", + avatarURL: profileUrl, + email: "test@helpwave.de", + isPersonal: false, + isVerified: true), + Organization( + id: "organization2", + shortName: "MK", + longName: "Musterklinikum", + avatarURL: profileUrl, + email: "test@helpwave.de", + isPersonal: false, + isVerified: true), +]; +final List initialUsers = [ + User( + id: "user1", + name: "Testine Test", + nickName: "Testine", + email: "test@helpwave.de", + profileUrl: Uri.parse(profileUrl), + ), + User( + id: "user2", + name: "Peter Pete", + nickName: "Peter", + email: "test@helpwave.de", + profileUrl: Uri.parse(profileUrl), + ), + User( + id: "user3", + name: "John Doe", + nickName: "John", + email: "test@helpwave.de", + profileUrl: Uri.parse(profileUrl), + ), + User( + id: "user4", + name: "Walter White", + nickName: "Walter", + email: "test@helpwave.de", + profileUrl: Uri.parse(profileUrl), + ), + User( + id: "user5", + name: "Peter Parker", + nickName: "Parker", + email: "test@helpwave.de", + profileUrl: Uri.parse(profileUrl), + ), +]; + +class OfflineClientStore { + static final OfflineClientStore _instance = OfflineClientStore._internal(); + + OfflineClientStore._internal(); + + factory OfflineClientStore() => _instance; + + final OrganizationOfflineClientStore organizationStore = OrganizationOfflineClientStore(); + final UserOfflineService userStore = UserOfflineService(); + + final WardOfflineService wardStore = WardOfflineService(); + final RoomOfflineService roomStore = RoomOfflineService(); + final BedOfflineService bedStore = BedOfflineService(); + final PatientOfflineService patientStore = PatientOfflineService(); + final TaskOfflineService taskStore = TaskOfflineService(); + final SubtaskOfflineService subtaskStore = SubtaskOfflineService(); + final TaskTemplateOfflineService taskTemplateStore = TaskTemplateOfflineService(); + final TaskTemplateSubtaskOfflineService taskTemplateSubtaskStore = TaskTemplateSubtaskOfflineService(); + + void reset() { + organizationStore.organizations = initialOrganizations; + userStore.users = initialUsers; + + } +} diff --git a/packages/helpwave_service/lib/src/api/offline/util.dart b/packages/helpwave_service/lib/src/api/offline/util.dart new file mode 100644 index 00000000..c518556c --- /dev/null +++ b/packages/helpwave_service/lib/src/api/offline/util.dart @@ -0,0 +1,48 @@ +import 'dart:async'; +import 'package:grpc/grpc.dart'; + +class MockResponseFuture implements ResponseFuture { + final Future future; + + MockResponseFuture.value(T value) : future = Future.value(value); + + MockResponseFuture.error(Object error) : future = Future.error(error); + + MockResponseFuture.future(this.future); + + @override + Stream asStream() { + return future.asStream(); + } + + @override + Future cancel() async { + // Mock Futures cannot be canceled + } + + @override + Future catchError(Function onError, {bool Function(Object error)? test}) { + return future.catchError(onError, test: test); + } + + @override + Future> get headers => Future.value({}); + + @override + Future timeout(Duration timeLimit, {FutureOr Function()? onTimeout}) { + return future.timeout(timeLimit, onTimeout: onTimeout); + } + + @override + Future> get trailers => Future.value({}); + + @override + Future whenComplete(FutureOr Function() action) { + return future.whenComplete(action); + } + + @override + Future then(FutureOr Function(T p1) onValue, {Function? onError}) { + return future.then(onValue, onError: onError); + } +} diff --git a/packages/helpwave_service/lib/src/api/tasks/controllers/my_tasks_controller.dart b/packages/helpwave_service/lib/src/api/tasks/controllers/my_tasks_controller.dart index c353476a..98de67d5 100644 --- a/packages/helpwave_service/lib/src/api/tasks/controllers/my_tasks_controller.dart +++ b/packages/helpwave_service/lib/src/api/tasks/controllers/my_tasks_controller.dart @@ -44,7 +44,7 @@ class MyTasksController extends ChangeNotifier { _tasks.add(TaskWithPatient( id: task.id, name: task.name, - assignee: task.assignee, + assignee: task.assigneeId, notes: task.notes, dueDate: task.dueDate, status: task.status, diff --git a/packages/helpwave_service/lib/src/api/tasks/controllers/task_controller.dart b/packages/helpwave_service/lib/src/api/tasks/controllers/task_controller.dart index f12f77f9..a797f9e3 100644 --- a/packages/helpwave_service/lib/src/api/tasks/controllers/task_controller.dart +++ b/packages/helpwave_service/lib/src/api/tasks/controllers/task_controller.dart @@ -79,13 +79,13 @@ class TaskController extends ChangeNotifier { /// /// Without a backend request as we expect this to be done in the [AssigneeSelectController] Future changeAssignee(String assigneeId) async { - String? old = task.assignee; + String? old = task.assigneeId; updateTask( (task) { - task.assignee = assigneeId; + task.assigneeId = assigneeId; }, (task) { - task.assignee = old; + task.assigneeId = old; }, ); } diff --git a/packages/helpwave_service/lib/src/api/tasks/data_types/bed.dart b/packages/helpwave_service/lib/src/api/tasks/data_types/bed.dart index a988a270..2bf7f84e 100644 --- a/packages/helpwave_service/lib/src/api/tasks/data_types/bed.dart +++ b/packages/helpwave_service/lib/src/api/tasks/data_types/bed.dart @@ -11,6 +11,12 @@ class BedMinimal { }); } +class BedWithRoomId extends BedMinimal { + String roomId; + + BedWithRoomId({required super.id, required super.name, required this.roomId}); +} + class BedWithMinimalPatient extends BedMinimal{ PatientMinimal? patient; diff --git a/packages/helpwave_service/lib/src/api/tasks/data_types/patient.dart b/packages/helpwave_service/lib/src/api/tasks/data_types/patient.dart index 0942256b..26b0ce61 100644 --- a/packages/helpwave_service/lib/src/api/tasks/data_types/patient.dart +++ b/packages/helpwave_service/lib/src/api/tasks/data_types/patient.dart @@ -33,13 +33,45 @@ class PatientMinimal { @override String toString() { - if(isCreating){ + if (isCreating) { return "PatientMinimal"; } return "PatientMinimal<$id, $name>"; } } +class PatientWithBedId extends PatientMinimal { + String? bedId; + bool isDischarged; + String notes; + + PatientWithBedId({ + required super.id, + required super.name, + required this.isDischarged, + required this.notes, + this.bedId, + }); + + PatientWithBedId copyWith({ + String? id, + String? name, + String? bedId, + bool? isDischarged, + String? notes, + }) { + return PatientWithBedId( + id: id ?? this.id, + name: name ?? this.name, + isDischarged: isDischarged ?? this.isDischarged, + notes: notes ?? this.notes, + bedId: bedId ?? this.bedId, + ); + } + + bool get hasBed => bedId != null; +} + /// data class for [Patient] with TaskCount class Patient extends PatientMinimal { RoomMinimal? room; @@ -54,8 +86,7 @@ class Patient extends PatientMinimal { List get unscheduledTasks => tasks.where((task) => task.status == TaskStatus.todo).toList(); - List get inProgressTasks => - tasks.where((task) => task.status == TaskStatus.inProgress).toList(); + List get inProgressTasks => tasks.where((task) => task.status == TaskStatus.inProgress).toList(); List get doneTasks => tasks.where((task) => task.status == TaskStatus.done).toList(); diff --git a/packages/helpwave_service/lib/src/api/tasks/data_types/room.dart b/packages/helpwave_service/lib/src/api/tasks/data_types/room.dart index 632ca7a1..8e943ea0 100644 --- a/packages/helpwave_service/lib/src/api/tasks/data_types/room.dart +++ b/packages/helpwave_service/lib/src/api/tasks/data_types/room.dart @@ -11,6 +11,12 @@ class RoomMinimal { }); } +class RoomWithWardId extends RoomMinimal { + String wardId; + + RoomWithWardId({required super.id, required super.name, required this.wardId}); +} + class RoomWithBeds { String id; String name; diff --git a/packages/helpwave_service/lib/src/api/tasks/data_types/subtask.dart b/packages/helpwave_service/lib/src/api/tasks/data_types/subtask.dart index f81a31dd..956d235c 100644 --- a/packages/helpwave_service/lib/src/api/tasks/data_types/subtask.dart +++ b/packages/helpwave_service/lib/src/api/tasks/data_types/subtask.dart @@ -1,25 +1,24 @@ /// Data class for a [Subtask] class Subtask { - String id; + final String id; + final String taskId; String name; bool isDone; bool get isCreating => id == ""; - Subtask({ - required this.id, - required this.name, - this.isDone = false - }); + Subtask({required this.id, required this.taskId, required this.name, this.isDone = false}); /// Create a copy of the [Subtask] Subtask copyWith({ String? id, + String? taskId, String? name, bool? isDone, }) { return Subtask( id: id ?? this.id, + taskId: taskId ?? this.taskId, name: name ?? this.name, isDone: isDone ?? this.isDone, ); diff --git a/packages/helpwave_service/lib/src/api/tasks/data_types/task.dart b/packages/helpwave_service/lib/src/api/tasks/data_types/task.dart index 370ff4b2..9b13a237 100644 --- a/packages/helpwave_service/lib/src/api/tasks/data_types/task.dart +++ b/packages/helpwave_service/lib/src/api/tasks/data_types/task.dart @@ -1,4 +1,3 @@ -// TODO delete later and import from protobufs import 'package:helpwave_service/src/api/tasks/data_types/patient.dart'; import 'package:helpwave_service/src/api/tasks/data_types/subtask.dart'; @@ -11,29 +10,28 @@ enum TaskStatus { /// data class for [Task] class Task { - String id; + final String id; String name; - String? assignee; + String? assigneeId; String notes; TaskStatus status; List subtasks; DateTime? dueDate; - DateTime? creationDate; + final DateTime? creationDate; + final String? createdBy; bool isPublicVisible; + final String patientId; - static get empty => Task(id: "", name: "name", notes: ""); + factory Task.empty(String patientId) => Task(id: "", name: "name", notes: "", patientId: patientId); final _nullID = "00000000-0000-0000-0000-000000000000"; - double get progress => subtasks.isNotEmpty - ? subtasks.where((element) => element.isDone).length / subtasks.length - : 1; + double get progress => subtasks.isNotEmpty ? subtasks.where((element) => element.isDone).length / subtasks.length : 1; /// the remaining time until a task is due /// /// **NOTE**: returns [Duration.zero] if [dueDate] is null - Duration get remainingTime => - dueDate != null ? dueDate!.difference(DateTime.now()) : Duration.zero; + Duration get remainingTime => dueDate != null ? dueDate!.difference(DateTime.now()) : Duration.zero; bool get isOverdue => remainingTime.isNegative; @@ -43,29 +41,65 @@ class Task { bool get isCreating => id == ""; - bool get hasAssignee => assignee != null && assignee != "" && assignee != _nullID; + bool get hasAssignee => assigneeId != null && assigneeId != "" && assigneeId != _nullID; Task({ required this.id, required this.name, required this.notes, - this.assignee, + this.assigneeId, this.status = TaskStatus.todo, this.subtasks = const [], this.dueDate, this.creationDate, + this.createdBy, this.isPublicVisible = false, + required this.patientId, }); + + Task copyWith({ + String? id, + String? name, + String? assigneeId, + String? notes, + TaskStatus? status, + List? subtasks, + DateTime? dueDate, + DateTime? creationDate, + String? createdBy, + bool? isPublicVisible, + String? patientId, + }) { + return Task( + id: id ?? this.id, + name: name ?? this.name, + assigneeId: assigneeId ?? this.assigneeId, + notes: notes ?? this.notes, + status: status ?? this.status, + subtasks: subtasks ?? this.subtasks, + dueDate: dueDate ?? this.dueDate, + creationDate: creationDate ?? this.creationDate, + createdBy: createdBy ?? this.createdBy, + isPublicVisible: isPublicVisible ?? this.isPublicVisible, + patientId: patientId ?? this.patientId, + ); + } } class TaskWithPatient extends Task { - PatientMinimal patient; + final PatientMinimal patient; factory TaskWithPatient.empty({ String taskId = "", PatientMinimal? patient, }) { - return TaskWithPatient(id: taskId, name: "task name", notes: "", patient: patient ?? PatientMinimal.empty()); + return TaskWithPatient( + id: taskId, + name: "task name", + notes: "", + patient: patient ?? PatientMinimal.empty(), + patientId: patient?.id ?? "", + ); } factory TaskWithPatient.fromTaskAndPatient({ @@ -82,8 +116,9 @@ class TaskWithPatient extends Task { status: task.status, dueDate: task.dueDate, creationDate: task.creationDate, - assignee: task.assignee, + assigneeId: task.assigneeId, patient: patient ?? PatientMinimal.empty(), + patientId: patient?.id ?? "", ); } @@ -91,12 +126,13 @@ class TaskWithPatient extends Task { required super.id, required super.name, required super.notes, - super.assignee, + super.assigneeId, super.status, super.subtasks, super.dueDate, super.creationDate, super.isPublicVisible, + required super.patientId, required this.patient, - }); + }) : assert(patientId == patient.id); } diff --git a/packages/helpwave_service/lib/src/api/tasks/data_types/task_template.dart b/packages/helpwave_service/lib/src/api/tasks/data_types/task_template.dart index 8c61a31c..735c1993 100644 --- a/packages/helpwave_service/lib/src/api/tasks/data_types/task_template.dart +++ b/packages/helpwave_service/lib/src/api/tasks/data_types/task_template.dart @@ -3,20 +3,42 @@ import '../index.dart'; /// data class for [TaskTemplate] class TaskTemplate { String id; - String? wardID; + String? wardId; String name; String notes; List subtasks; bool isPublicVisible; + String? createdBy; - get isWardTemplate => wardID != null; + get isWardTemplate => wardId != null; TaskTemplate({ required this.id, - this.wardID, + this.wardId, required this.name, required this.notes, this.subtasks = const [], this.isPublicVisible = false, + this.createdBy }); + + TaskTemplate copyWith({ + String? id, + String? wardId, + String? name, + String? notes, + List? subtasks, + bool? isPublicVisible, + String? createdBy, + }) { + return TaskTemplate( + id: id ?? this.id, + wardId: wardId ?? this.wardId, + name: name ?? this.name, + notes: notes ?? this.notes, + subtasks: subtasks ?? this.subtasks, + isPublicVisible: isPublicVisible ?? this.isPublicVisible, + createdBy: createdBy ?? this.createdBy, + ); + } } diff --git a/packages/helpwave_service/lib/src/api/tasks/index.dart b/packages/helpwave_service/lib/src/api/tasks/index.dart index bebaf463..44b25815 100644 --- a/packages/helpwave_service/lib/src/api/tasks/index.dart +++ b/packages/helpwave_service/lib/src/api/tasks/index.dart @@ -1,4 +1,4 @@ export 'data_types/index.dart'; export 'services/index.dart'; export 'controllers/index.dart'; -export 'tasks_api_services.dart'; +export 'tasks_api_service_clients.dart'; diff --git a/packages/helpwave_service/lib/src/api/tasks/offline_clients/bed_offline_client.dart b/packages/helpwave_service/lib/src/api/tasks/offline_clients/bed_offline_client.dart new file mode 100644 index 00000000..11f565e9 --- /dev/null +++ b/packages/helpwave_service/lib/src/api/tasks/offline_clients/bed_offline_client.dart @@ -0,0 +1,178 @@ +import 'package:grpc/grpc.dart'; +import 'package:helpwave_proto_dart/services/tasks_svc/v1/bed_svc.pbgrpc.dart'; +import 'package:helpwave_service/src/api/offline/offline_client_store.dart'; +import 'package:helpwave_service/src/api/offline/util.dart'; +import 'package:helpwave_service/src/api/tasks/data_types/bed.dart'; +import 'package:helpwave_util/lists.dart'; + +class BedUpdate { + String id; + String? name; + + BedUpdate({required this.id, required this.name}); +} + +class BedOfflineService { + List beds = []; + + BedWithRoomId? findBed(String id) { + int index = OfflineClientStore().bedStore.beds.indexWhere((value) => value.id == id); + if (index == -1) { + return null; + } + return beds[index]; + } + + List findBeds([String? roomId]) { + final valueStore = OfflineClientStore().bedStore; + if (roomId == null) { + return valueStore.beds; + } + return valueStore.beds.where((value) => value.roomId == roomId).toList(); + } + + void create(BedWithRoomId bed) { + OfflineClientStore().bedStore.beds.add(bed); + } + + void update(BedUpdate bed) { + final valueStore = OfflineClientStore().bedStore; + bool found = false; + + valueStore.beds = valueStore.beds.map((value) { + if (value.id == bed.id) { + found = true; + return BedWithRoomId(id: bed.id, name: bed.name ?? value.name, roomId: value.roomId); + } + return value; + }).toList(); + + if (!found) { + throw Exception('UpdateBed: Could not find bed with id ${bed.id}'); + } + } + + void delete(String bedId) { + final valueStore = OfflineClientStore().bedStore; + valueStore.beds = valueStore.beds.where((value) => value.id != bedId).toList(); + // TODO: Cascade delete to bed-bound templates + } +} + +class BedServicePromiseClient extends BedServiceClient { + BedServicePromiseClient(super.channel); + + @override + ResponseFuture getBed(GetBedRequest request, {CallOptions? options}) { + final bed = OfflineClientStore().bedStore.findBed(request.id); + + if (bed == null) { + throw "Bed with bed id ${request.id} not found"; + } + + final response = GetBedResponse() + ..id = bed.id + ..name = bed.name + ..roomId = bed.roomId; + + return MockResponseFuture.value(response); + } + + @override + ResponseFuture getBeds(GetBedsRequest request, {CallOptions? options}) { + final beds = OfflineClientStore().bedStore.findBeds(); + final bedsList = beds.map((bed) => GetBedsResponse_Bed(id: bed.id, name: bed.name, roomId: bed.roomId)).toList(); + + final response = GetBedsResponse(beds: bedsList); + + return MockResponseFuture.value(response); + } + + @override + ResponseFuture getBedByPatient(GetBedByPatientRequest request, {CallOptions? options}) { + final patient = OfflineClientStore().patientStore.findPatient(request.patientId); + if (patient == null) { + throw "Patient with id ${request.patientId} not found"; + } + if (!patient.hasBed) { + throw "Patient with id ${request.patientId} has no bed"; + } + final bed = OfflineClientStore().bedStore.findBed(patient.bedId!); + if (bed == null) { + throw "Inconsistent Data: Bed with id ${patient.bedId} not found"; + } + final room = OfflineClientStore().roomStore.findRoom(bed.roomId); + if (room == null) { + throw "Inconsistent Data: Room with id ${bed.roomId} not found"; + } + final response = GetBedByPatientResponse( + bed: GetBedByPatientResponse_Bed(id: bed.id, name: bed.name), + room: GetBedByPatientResponse_Room(id: room.id, name: room.name, wardId: room.wardId), + ); + return MockResponseFuture.value(response); + } + + @override + ResponseFuture getBedsByRoom(GetBedsByRoomRequest request, {CallOptions? options}) { + final beds = OfflineClientStore().bedStore.findBeds(request.roomId); + final response = + GetBedsByRoomResponse(beds: beds.map((bed) => GetBedsByRoomResponse_Bed(id: bed.id, name: bed.name))); + return MockResponseFuture.value(response); + } + + @override + ResponseFuture createBed(CreateBedRequest request, {CallOptions? options}) { + final newBed = BedWithRoomId( + id: DateTime.now().millisecondsSinceEpoch.toString(), + name: request.name, + roomId: request.roomId, + ); + + OfflineClientStore().bedStore.create(newBed); + + final response = CreateBedResponse()..id = newBed.id; + + return MockResponseFuture.value(response); + } + + @override + ResponseFuture bulkCreateBeds(BulkCreateBedsRequest request, {CallOptions? options}) { + final beds = range(0, request.amountOfBeds) + .map((index) => BedWithRoomId( + id: DateTime.now().millisecondsSinceEpoch.toString(), + name: "New Bed ${index + 1}", + roomId: request.roomId, + )) + .toList(); + + for (var bed in beds) { + OfflineClientStore().bedStore.create(bed); + } + + final response = + BulkCreateBedsResponse(beds: beds.map((bed) => BulkCreateBedsResponse_Bed(id: bed.id, name: bed.name))); + + return MockResponseFuture.value(response); + } + + @override + ResponseFuture updateBed(UpdateBedRequest request, {CallOptions? options}) { + final update = BedUpdate( + id: request.id, + name: request.name, + ); + + OfflineClientStore().bedStore.update(update); + + final response = UpdateBedResponse(); + return MockResponseFuture.value(response); + } + + @override + ResponseFuture deleteBed(DeleteBedRequest request, {CallOptions? options}) { + OfflineClientStore().bedStore.delete(request.id); + + final response = DeleteBedResponse(); + return MockResponseFuture.value(response); + } +} diff --git a/packages/helpwave_service/lib/src/api/tasks/offline_clients/patient_offline_client.dart b/packages/helpwave_service/lib/src/api/tasks/offline_clients/patient_offline_client.dart new file mode 100644 index 00000000..22a46384 --- /dev/null +++ b/packages/helpwave_service/lib/src/api/tasks/offline_clients/patient_offline_client.dart @@ -0,0 +1,366 @@ +import 'package:grpc/grpc.dart'; +import 'package:helpwave_proto_dart/services/tasks_svc/v1/patient_svc.pbgrpc.dart'; +import 'package:helpwave_service/src/api/offline/offline_client_store.dart'; +import 'package:helpwave_service/src/api/offline/util.dart'; +import 'package:helpwave_service/src/api/tasks/data_types/patient.dart'; + +class PatientUpdate { + String id; + String? name; + String? notes; + bool? isDischarged; + + PatientUpdate({required this.id, this.name, this.notes, this.isDischarged}); +} + +class PatientOfflineService { + List patients = []; + + PatientWithBedId? findPatient(String id) { + int index = OfflineClientStore().patientStore.patients.indexWhere((value) => value.id == id); + if (index == -1) { + return null; + } + return patients[index]; + } + + PatientWithBedId? findPatientByBed(String bedId) { + final valueStore = OfflineClientStore().patientStore; + return valueStore.patients.where((value) => value.bedId == bedId).firstOrNull; + } + + void create(PatientWithBedId patient) { + OfflineClientStore().patientStore.patients.add(patient); + } + + void update(PatientUpdate patientUpdate) { + final valueStore = OfflineClientStore().patientStore; + bool found = false; + + valueStore.patients = valueStore.patients.map((value) { + if (value.id == patientUpdate.id) { + found = true; + return value.copyWith( + notes: patientUpdate.notes, + name: patientUpdate.name, + isDischarged: patientUpdate.isDischarged, + ); + } + return value; + }).toList(); + + if (!found) { + throw Exception('UpdatePatient: Could not find patient with id ${patientUpdate.id}'); + } + } + + void assignBed(String patientId, String bedId) { + final valueStore = OfflineClientStore().patientStore; + final bed = OfflineClientStore().bedStore.findBed(bedId); + if (bed != null) { + throw Exception('Could not find bed with id $bedId'); + } + bool found = false; + + valueStore.patients = valueStore.patients.map((value) { + if (value.id == patientId) { + found = true; + return value.copyWith(bedId: bedId); + } + return value; + }).toList(); + + if (!found) { + throw Exception('Could not find patient with id $patientId'); + } + } + + void unassignBed(String patientId) { + final valueStore = OfflineClientStore().patientStore; + bool found = false; + + valueStore.patients = valueStore.patients.map((value) { + if (value.id == patientId) { + found = true; + final copy = value.copyWith(); + copy.bedId = null; + return copy; + } + return value; + }).toList(); + + if (!found) { + throw Exception('Could not find patient with id $patientId'); + } + } + + void delete(String patientId) { + final valueStore = OfflineClientStore().patientStore; + valueStore.patients = valueStore.patients.where((value) => value.id != patientId).toList(); + // TODO: Cascade delete to tasks + } +} + +class PatientServicePromiseClient extends PatientServiceClient { + PatientServicePromiseClient(super.channel); + + @override + ResponseFuture getPatient(GetPatientRequest request, {CallOptions? options}) { + final patient = OfflineClientStore().patientStore.findPatient(request.id); + + if (patient == null) { + throw "Patient with patient id ${request.id} not found"; + } + + final response = GetPatientResponse(id: patient.id, humanReadableIdentifier: patient.name, notes: patient.notes); + + if (patient.bedId == null) { + return MockResponseFuture.value(response); + } + final bed = OfflineClientStore().bedStore.findBed(patient.bedId!); + if (bed == null) { + return MockResponseFuture.value(response); + } + final room = OfflineClientStore().roomStore.findRoom(bed.roomId); + if (room == null) { + return MockResponseFuture.value(response); + } + response.bedId = patient.bedId!; + response.bed = GetPatientResponse_Bed(id: bed.id, name: bed.name); + response.room = GetPatientResponse_Room(id: room.id, name: room.name); + return MockResponseFuture.value(response); + } + + @override + ResponseFuture getPatientDetails(GetPatientDetailsRequest request, + {CallOptions? options}) { + final patient = OfflineClientStore().patientStore.findPatient(request.id); + + if (patient == null) { + throw "Patient with patient id ${request.id} not found"; + } + + final response = GetPatientDetailsResponse( + id: patient.id, + humanReadableIdentifier: patient.name, + notes: patient.notes, + isDischarged: patient.isDischarged, + tasks: [], // TODO tasks + ); + + if (patient.bedId == null) { + return MockResponseFuture.value(response); + } + final bed = OfflineClientStore().bedStore.findBed(patient.bedId!); + if (bed == null) { + return MockResponseFuture.value(response); + } + final room = OfflineClientStore().roomStore.findRoom(bed.roomId); + if (room == null) { + return MockResponseFuture.value(response); + } + response.bed = GetPatientDetailsResponse_Bed(id: bed.id, name: bed.name); + response.room = GetPatientDetailsResponse_Room(id: room.id, name: room.name); + return MockResponseFuture.value(response); + } + + @override + ResponseFuture getPatientByBed(GetPatientByBedRequest request, {CallOptions? options}) { + final patient = OfflineClientStore().patientStore.findPatientByBed(request.bedId); + + if (patient == null) { + throw "Patient with bed id ${request.bedId} not found"; + } + + final response = GetPatientByBedResponse( + id: patient.id, + humanReadableIdentifier: patient.name, + notes: patient.notes, + bedId: patient.bedId, + ); + return MockResponseFuture.value(response); + } + + @override + ResponseFuture getPatientList(GetPatientListRequest request, {CallOptions? options}) { + mapping(patient) { + final res = GetPatientListResponse_Patient( + id: patient.id, + notes: patient.notes, + humanReadableIdentifier: patient.name, + tasks: [], // TODO get tasks + ); + if (patient.bedId == null) { + return res; + } + final bed = OfflineClientStore().bedStore.findBed(patient.bedId!); + if (bed == null) { + return res; + } + final room = OfflineClientStore().roomStore.findRoom(bed.roomId); + if (room == null) { + return res; + } + res.bed = GetPatientListResponse_Bed(id: bed.id, name: bed.name); + res.room = GetPatientListResponse_Room(id: room.id, name: room.name); + return res; + } + + final patients = OfflineClientStore().patientStore.patients; + final active = patients.where((element) => element.hasBed).map(mapping); + final discharged = patients.where((element) => element.isDischarged).map(mapping); + final unassigned = patients.where((element) => !element.isDischarged && element.bedId == null).map(mapping); + final response = GetPatientListResponse( + active: active, + dischargedPatients: discharged, + unassignedPatients: unassigned, + ); + + return MockResponseFuture.value(response); + } + + @override + ResponseFuture getRecentPatients(GetRecentPatientsRequest request, + {CallOptions? options}) { + final patients = OfflineClientStore().patientStore.patients.where((element) => element.hasBed).map((patient) { + final res = GetRecentPatientsResponse_PatientWithRoomAndBed( + id: patient.id, + humanReadableIdentifier: patient.name, + ); + if (patient.bedId == null) { + return res; + } + final bed = OfflineClientStore().bedStore.findBed(patient.bedId!); + if (bed == null) { + return res; + } + final room = OfflineClientStore().roomStore.findRoom(bed.roomId); + if (room == null) { + return res; + } + res.bed = GetRecentPatientsResponse_Bed(id: bed.id, name: bed.name); + res.room = GetRecentPatientsResponse_Room(id: room.id, name: room.name); + return res; + }); + final response = GetRecentPatientsResponse(recentPatients: patients); + + return MockResponseFuture.value(response); + } + + @override + ResponseFuture getPatientsByWard(GetPatientsByWardRequest request, + {CallOptions? options}) { + final rooms = OfflineClientStore().roomStore.findRooms(request.wardId); + final beds = rooms.map((room) => OfflineClientStore().bedStore.findBeds(room.id)).expand((element) => element); + List patients = []; + + for (final bed in beds) { + final patient = OfflineClientStore().patientStore.findPatientByBed(bed.id); + if (patient != null) { + patients.add(GetPatientsByWardResponse_Patient( + id: patient.id, notes: patient.notes, humanReadableIdentifier: patient.name, bedId: patient.bedId)); + } + } + final response = GetPatientsByWardResponse(patients: patients); + + return MockResponseFuture.value(response); + } + + @override + ResponseFuture getPatientAssignmentByWard( + GetPatientAssignmentByWardRequest request, + {CallOptions? options}) { + final rooms = + OfflineClientStore().roomStore.findRooms(request.wardId).map((room) => GetPatientAssignmentByWardResponse_Room( + id: room.id, + name: room.name, + beds: OfflineClientStore().bedStore.findBeds(room.id).map((bed) { + final res = GetPatientAssignmentByWardResponse_Room_Bed(id: bed.id, name: bed.name); + final patient = OfflineClientStore().patientStore.findPatientByBed(bed.id); + if (patient != null) { + res.patient = GetPatientAssignmentByWardResponse_Room_Bed_Patient( + id: patient.id, + name: patient.name, + ); + } + return res; + }), + )); + final response = GetPatientAssignmentByWardResponse(rooms: rooms); + + return MockResponseFuture.value(response); + } + + @override + ResponseFuture createPatient(CreatePatientRequest request, {CallOptions? options}) { + final newPatient = PatientWithBedId( + id: DateTime.now().millisecondsSinceEpoch.toString(), + name: request.humanReadableIdentifier, + notes: request.notes, + isDischarged: false, + ); + + OfflineClientStore().patientStore.create(newPatient); + + final response = CreatePatientResponse()..id = newPatient.id; + + return MockResponseFuture.value(response); + } + + @override + ResponseFuture updatePatient(UpdatePatientRequest request, {CallOptions? options}) { + final update = PatientUpdate( + id: request.id, + name: request.hasHumanReadableIdentifier() ? request.humanReadableIdentifier : null, + notes: request.hasNotes() ? request.notes : null, + ); + + OfflineClientStore().patientStore.update(update); + + final response = UpdatePatientResponse(); + return MockResponseFuture.value(response); + } + + @override + ResponseFuture assignBed(AssignBedRequest request, {CallOptions? options}) { + OfflineClientStore().patientStore.assignBed(request.id, request.bedId); + final response = AssignBedResponse(); + return MockResponseFuture.value(response); + } + + @override + ResponseFuture unassignBed(UnassignBedRequest request, {CallOptions? options}) { + OfflineClientStore().patientStore.unassignBed(request.id); + final response = UnassignBedResponse(); + return MockResponseFuture.value(response); + } + + @override + ResponseFuture dischargePatient(DischargePatientRequest request, {CallOptions? options}) { + final update = PatientUpdate(id: request.id, isDischarged: true); + + OfflineClientStore().patientStore.update(update); + OfflineClientStore().patientStore.unassignBed(request.id); + + final response = DischargePatientResponse(); + return MockResponseFuture.value(response); + } + + @override + ResponseFuture readmitPatient(ReadmitPatientRequest request, {CallOptions? options}) { + final update = PatientUpdate(id: request.patientId, isDischarged: false); + + OfflineClientStore().patientStore.update(update); + + final response = ReadmitPatientResponse(); + return MockResponseFuture.value(response); + } + + @override + ResponseFuture deletePatient(DeletePatientRequest request, {CallOptions? options}) { + OfflineClientStore().patientStore.delete(request.id); + + final response = DeletePatientResponse(); + return MockResponseFuture.value(response); + } +} diff --git a/packages/helpwave_service/lib/src/api/tasks/offline_clients/room_offline_client.dart b/packages/helpwave_service/lib/src/api/tasks/offline_clients/room_offline_client.dart new file mode 100644 index 00000000..34405d0e --- /dev/null +++ b/packages/helpwave_service/lib/src/api/tasks/offline_clients/room_offline_client.dart @@ -0,0 +1,161 @@ +import 'package:grpc/grpc.dart'; +import 'package:helpwave_proto_dart/services/tasks_svc/v1/room_svc.pbgrpc.dart'; +import 'package:helpwave_service/src/api/offline/offline_client_store.dart'; +import 'package:helpwave_service/src/api/offline/util.dart'; +import 'package:helpwave_service/src/api/tasks/data_types/index.dart'; + +class RoomUpdate { + String id; + String? name; + + RoomUpdate({required this.id, required this.name}); +} + +class RoomOfflineService { + List rooms = []; + + RoomWithWardId? findRoom(String id) { + int index = OfflineClientStore().roomStore.rooms.indexWhere((value) => value.id == id); + if (index == -1) { + return null; + } + return rooms[index]; + } + + List findRooms([String? wardId]) { + final valueStore = OfflineClientStore().roomStore; + if (wardId == null) { + return valueStore.rooms; + } + return valueStore.rooms.where((value) => value.wardId == wardId).toList(); + } + + void create(RoomWithWardId room) { + OfflineClientStore().roomStore.rooms.add(room); + } + + void update(RoomUpdate room) { + final valueStore = OfflineClientStore().roomStore; + bool found = false; + + valueStore.rooms = valueStore.rooms.map((value) { + if (value.id == room.id) { + found = true; + return RoomWithWardId(id: room.id, name: room.name ?? value.name, wardId: value.wardId); + } + return value; + }).toList(); + + if (!found) { + throw Exception('UpdateRoom: Could not find room with id ${room.id}'); + } + } + + void delete(String roomId) { + final valueStore = OfflineClientStore().roomStore; + valueStore.rooms = valueStore.rooms.where((value) => value.id != roomId).toList(); + OfflineClientStore().bedStore.findBeds(roomId).forEach((element) { + OfflineClientStore().bedStore.delete(element.id); + }); + } +} + +class RoomServicePromiseClient extends RoomServiceClient { + RoomServicePromiseClient(super.channel); + + @override + ResponseFuture getRoom(GetRoomRequest request, {CallOptions? options}) { + final room = OfflineClientStore().roomStore.findRoom(request.id); + + if (room == null) { + throw "Room with room id ${request.id} not found"; + } + + final response = GetRoomResponse() + ..id = room.id + ..name = room.name + ..wardId = room.wardId; + + return MockResponseFuture.value(response); + } + + @override + ResponseFuture getRooms(GetRoomsRequest request, {CallOptions? options}) { + final rooms = OfflineClientStore().roomStore.findRooms(); + final roomsList = rooms.map((room) => GetRoomsResponse_Room( + id: room.id, + name: room.name, + wardId: room.wardId, + beds: OfflineClientStore().bedStore.findBeds(room.id).map((bed) => GetRoomsResponse_Room_Bed( + id: bed.id, + name: bed.name, + )), + )); + + final response = GetRoomsResponse(rooms: roomsList); + return MockResponseFuture.value(response); + } + + @override + ResponseFuture getRoomOverviewsByWard(GetRoomOverviewsByWardRequest request, + {CallOptions? options}) { + final rooms = OfflineClientStore().roomStore.findRooms(request.id); + + final response = GetRoomOverviewsByWardResponse() + ..rooms.addAll(rooms.map((room) => GetRoomOverviewsByWardResponse_Room() + ..id = room.id + ..name = room.name + ..beds.addAll(OfflineClientStore().bedStore.findBeds(room.id).map((bed) { + final response = GetRoomOverviewsByWardResponse_Room_Bed(id: bed.id, name: bed.name); + final patient = OfflineClientStore().patientStore.findPatientByBed(bed.id); + if (patient != null) { + final tasks = OfflineClientStore().taskStore.findTasks(patient.id); + response.patient = GetRoomOverviewsByWardResponse_Room_Bed_Patient( + id: patient.id, + humanReadableIdentifier: patient.name, + tasksDone: tasks.where((element) => element.status == TaskStatus.done).length, + tasksInProgress: tasks.where((element) => element.status == TaskStatus.inProgress).length, + tasksUnscheduled: tasks.where((element) => element.status == TaskStatus.todo).length, + ); + } + return response; + })))); + return MockResponseFuture.value(response); + } + + @override + ResponseFuture createRoom(CreateRoomRequest request, {CallOptions? options}) { + final newRoom = RoomWithWardId( + id: DateTime.now().millisecondsSinceEpoch.toString(), + name: request.name, + wardId: request.wardId, + ); + + OfflineClientStore().roomStore.create(newRoom); + + final response = CreateRoomResponse()..id = newRoom.id; + + return MockResponseFuture.value(response); + } + + @override + ResponseFuture updateRoom(UpdateRoomRequest request, {CallOptions? options}) { + final update = RoomUpdate( + id: request.id, + name: request.name, + ); + + OfflineClientStore().roomStore.update(update); + + final response = UpdateRoomResponse(); + return MockResponseFuture.value(response); + } + + @override + ResponseFuture deleteRoom(DeleteRoomRequest request, {CallOptions? options}) { + OfflineClientStore().roomStore.delete(request.id); + + final response = DeleteRoomResponse(); + return MockResponseFuture.value(response); + } +} diff --git a/packages/helpwave_service/lib/src/api/tasks/offline_clients/task_offline_client.dart b/packages/helpwave_service/lib/src/api/tasks/offline_clients/task_offline_client.dart new file mode 100644 index 00000000..86fb98ef --- /dev/null +++ b/packages/helpwave_service/lib/src/api/tasks/offline_clients/task_offline_client.dart @@ -0,0 +1,445 @@ +import 'package:grpc/grpc.dart'; +import 'package:helpwave_proto_dart/google/protobuf/timestamp.pb.dart'; +import 'package:helpwave_proto_dart/services/tasks_svc/v1/task_svc.pbgrpc.dart'; +import 'package:helpwave_service/src/api/offline/offline_client_store.dart'; +import 'package:helpwave_service/src/api/offline/util.dart'; +import 'package:helpwave_service/src/api/tasks/index.dart'; +import 'package:helpwave_service/src/api/tasks/util/task_status_mapping.dart'; + +class TaskUpdate { + String id; + String? name; + String? notes; + TaskStatus? status; + bool? isPublicVisible; + DateTime? dueDate; + + TaskUpdate({required this.id, this.name, this.notes, this.status, this.isPublicVisible, this.dueDate}); +} + +class SubtaskUpdate { + String id; + bool? isDone; + String? name; + + SubtaskUpdate({required this.id, this.name, this.isDone}); +} + +class TaskOfflineService { + List tasks = []; + + Task? findTask(String id) { + int index = OfflineClientStore().taskStore.tasks.indexWhere((value) => value.id == id); + if (index == -1) { + return null; + } + return tasks[index]; + } + + List findTasks([String? patientId]) { + final valueStore = OfflineClientStore().taskStore; + if (patientId == null) { + return valueStore.tasks; + } + return valueStore.tasks.where((value) => value.patientId == patientId).toList(); + } + + void create(Task task) { + OfflineClientStore().taskStore.tasks.add(task); + } + + void update(TaskUpdate taskUpdate) { + final valueStore = OfflineClientStore().taskStore; + bool found = false; + + valueStore.tasks = valueStore.tasks.map((value) { + if (value.id == taskUpdate.id) { + found = true; + return value.copyWith( + name: taskUpdate.name, + notes: taskUpdate.notes, + status: taskUpdate.status, + isPublicVisible: taskUpdate.isPublicVisible, + dueDate: taskUpdate.dueDate, + ); + } + return value; + }).toList(); + + if (!found) { + throw Exception('UpdateTask: Could not find task with id ${taskUpdate.id}'); + } + } + + void removeDueDate(String taskId) { + final valueStore = OfflineClientStore().taskStore; + bool found = false; + + valueStore.tasks = valueStore.tasks.map((value) { + if (value.id == taskId) { + found = true; + final copy = value.copyWith(); + copy.dueDate = null; + return copy; + } + return value; + }).toList(); + + if (!found) { + throw Exception('Could not find task with id $taskId'); + } + } + + assignUser(String taskId, String assigneeId) { + final user = OfflineClientStore().userStore.find(assigneeId); + if (user == null) { + throw "Could not find user with id $assigneeId"; + } + final valueStore = OfflineClientStore().taskStore; + bool found = false; + + valueStore.tasks = valueStore.tasks.map((value) { + if (value.id == taskId) { + found = true; + return value.copyWith(assigneeId: assigneeId); + } + return value; + }).toList(); + + if (!found) { + throw Exception('Could not find task with id $taskId'); + } + } + + unassignUser(String taskId, String assigneeId) { + final valueStore = OfflineClientStore().taskStore; + bool found = false; + + valueStore.tasks = valueStore.tasks.map((value) { + if (value.id == taskId) { + found = true; + final copy = value.copyWith(); + copy.assigneeId = null; + return copy; + } + return value; + }).toList(); + + if (!found) { + throw Exception('Could not find task with id $taskId'); + } + } + + void delete(String taskId) { + final valueStore = OfflineClientStore().taskStore; + valueStore.tasks = valueStore.tasks.where((value) => value.id != taskId).toList(); + OfflineClientStore().subtaskStore.findSubtasks(taskId).forEach((subtask) { + OfflineClientStore().subtaskStore.delete(subtask.id); + }); + } +} + +class SubtaskOfflineService { + List subtasks = []; + + Subtask? findSubtask(String id) { + int index = OfflineClientStore().subtaskStore.subtasks.indexWhere((value) => value.id == id); + if (index == -1) { + return null; + } + return subtasks[index]; + } + + List findSubtasks([String? taskId]) { + final valueStore = OfflineClientStore().subtaskStore; + if (taskId == null) { + return valueStore.subtasks; + } + return valueStore.subtasks.where((value) => value.taskId == taskId).toList(); + } + + void create(Subtask subtask) { + OfflineClientStore().subtaskStore.subtasks.add(subtask); + } + + void update(SubtaskUpdate subtaskUpdate) { + final valueStore = OfflineClientStore().subtaskStore; + bool found = false; + + valueStore.subtasks = valueStore.subtasks.map((value) { + if (value.id == subtaskUpdate.id) { + found = true; + return value.copyWith(name: subtaskUpdate.name, isDone: subtaskUpdate.isDone); + } + return value; + }).toList(); + + if (!found) { + throw Exception('UpdateSubtask: Could not find subtask with id ${subtaskUpdate.id}'); + } + } + + void delete(String subtaskId) { + final valueStore = OfflineClientStore().subtaskStore; + valueStore.subtasks = valueStore.subtasks.where((value) => value.id != subtaskId).toList(); + } +} + +class TaskServicePromiseClient extends TaskServiceClient { + TaskServicePromiseClient(super.channel); + + @override + ResponseFuture getTask(GetTaskRequest request, {CallOptions? options}) { + final task = OfflineClientStore().taskStore.findTask(request.id); + + if (task == null) { + throw "Task with task id ${request.id} not found"; + } + + final patient = OfflineClientStore().patientStore.findPatient(task.patientId); + if (patient == null) { + throw "Inconsistency error: Patient with patient id ${task.patientId} not found"; + } + + final subtasks = OfflineClientStore() + .subtaskStore + .findSubtasks(task.id) + .map((subtask) => GetTaskResponse_SubTask(id: subtask.id, name: subtask.name, done: subtask.isDone)); + + final response = GetTaskResponse( + id: task.id, + name: task.name, + description: task.notes, + status: GRPCTypeConverter.taskStatusToGRPC(task.status), + dueAt: task.dueDate == null ? null : Timestamp.fromDateTime(task.dueDate!), + createdBy: task.createdBy, + createdAt: task.creationDate == null ? null : Timestamp.fromDateTime(task.creationDate!), + public: task.isPublicVisible, + assignedUserId: task.assigneeId, + patient: GetTaskResponse_Patient(id: patient.id, humanReadableIdentifier: patient.name), + subtasks: subtasks); + + return MockResponseFuture.value(response); + } + + @override + ResponseFuture getTasksByPatient(GetTasksByPatientRequest request, + {CallOptions? options}) { + final tasks = + OfflineClientStore().taskStore.findTasks(request.patientId).map((task) => GetTasksByPatientResponse_Task( + id: task.id, + name: task.name, + description: task.notes, + status: GRPCTypeConverter.taskStatusToGRPC(task.status), + dueAt: task.dueDate == null ? null : Timestamp.fromDateTime(task.dueDate!), + createdBy: task.createdBy, + createdAt: task.creationDate == null ? null : Timestamp.fromDateTime(task.creationDate!), + public: task.isPublicVisible, + assignedUserId: task.assigneeId, + patientId: request.patientId, + subtasks: OfflineClientStore() + .subtaskStore + .findSubtasks(task.id) + .map((subtask) => GetTasksByPatientResponse_Task_SubTask( + id: subtask.id, + name: subtask.name, + done: subtask.isDone, + )), + )); + + final response = GetTasksByPatientResponse(tasks: tasks); + + return MockResponseFuture.value(response); + } + + @override + ResponseFuture getAssignedTasks(GetAssignedTasksRequest request, {CallOptions? options}) { + final user = OfflineClientStore().userStore.users[0]; + final tasks = OfflineClientStore().taskStore.findTasks().where((task) => task.assigneeId == user.id).map((task) { + final res = GetAssignedTasksResponse_Task( + id: task.id, + name: task.name, + description: task.notes, + status: GRPCTypeConverter.taskStatusToGRPC(task.status), + dueAt: task.dueDate == null ? null : Timestamp.fromDateTime(task.dueDate!), + createdBy: task.createdBy, + createdAt: task.creationDate == null ? null : Timestamp.fromDateTime(task.creationDate!), + public: task.isPublicVisible, + assignedUserId: task.assigneeId, + subtasks: OfflineClientStore() + .subtaskStore + .findSubtasks(task.id) + .map((subtask) => GetAssignedTasksResponse_Task_SubTask( + id: subtask.id, + name: subtask.name, + done: subtask.isDone, + )), + ); + final patient = OfflineClientStore().patientStore.findPatient(task.patientId); + if (patient == null) { + throw "Inconsistency error: patient with id ${task.patientId} not found"; + } + res.patient = GetAssignedTasksResponse_Task_Patient(id: patient.id, humanReadableIdentifier: patient.name); + return res; + }); + + final response = GetAssignedTasksResponse(tasks: tasks); + + return MockResponseFuture.value(response); + } + + @override + ResponseFuture getTasksByPatientSortedByStatus( + GetTasksByPatientSortedByStatusRequest request, + {CallOptions? options}) { + mapping(task) => GetTasksByPatientSortedByStatusResponse_Task( + id: task.id, + name: task.name, + description: task.notes, + dueAt: task.dueDate == null ? null : Timestamp.fromDateTime(task.dueDate!), + createdBy: task.createdBy, + createdAt: task.creationDate == null ? null : Timestamp.fromDateTime(task.creationDate!), + public: task.isPublicVisible, + assignedUserId: task.assigneeId, + patientId: request.patientId, + subtasks: OfflineClientStore() + .subtaskStore + .findSubtasks(task.id) + .map((subtask) => GetTasksByPatientSortedByStatusResponse_Task_SubTask( + id: subtask.id, + name: subtask.name, + done: subtask.isDone, + )), + ); + + final tasks = OfflineClientStore().taskStore.findTasks(request.patientId); + + final response = GetTasksByPatientSortedByStatusResponse( + done: tasks.where((element) => element.status == TaskStatus.done).map(mapping), + inProgress: tasks.where((element) => element.status == TaskStatus.inProgress).map(mapping), + todo: tasks.where((element) => element.status == TaskStatus.todo).map(mapping), + ); + + return MockResponseFuture.value(response); + } + + @override + ResponseFuture createTask(CreateTaskRequest request, {CallOptions? options}) { + final patient = OfflineClientStore().patientStore.findPatient(request.patientId); + if (patient == null) { + throw "Patient with id ${request.patientId} not found"; + } + final newTask = Task( + id: DateTime.now().millisecondsSinceEpoch.toString(), + name: request.name, + notes: request.description, + patientId: request.patientId, + creationDate: DateTime.now(), + createdBy: OfflineClientStore().userStore.users[0].id, + dueDate: request.hasDueAt() ? request.dueAt.toDateTime() : null, + status: GRPCTypeConverter.taskStatusFromGRPC(request.initialStatus), + isPublicVisible: request.public, + assigneeId: request.assignedUserId, + ); + + OfflineClientStore().taskStore.create(newTask); + for (var subtask in request.subtasks) { + OfflineClientStore().subtaskStore.create(Subtask( + id: DateTime.now().millisecondsSinceEpoch.toString(), + taskId: newTask.id, + name: subtask.name, + isDone: subtask.done, + )); + } + + final response = CreateTaskResponse()..id = newTask.id; + + return MockResponseFuture.value(response); + } + + @override + ResponseFuture updateTask(UpdateTaskRequest request, {CallOptions? options}) { + final update = TaskUpdate( + id: request.id, + name: request.name, + status: GRPCTypeConverter.taskStatusFromGRPC(request.status), + isPublicVisible: request.public, + notes: request.description, + dueDate: request.hasDueAt() ? request.dueAt.toDateTime() : null, + ); + + OfflineClientStore().taskStore.update(update); + + final response = UpdateTaskResponse(); + return MockResponseFuture.value(response); + } + + @override + ResponseFuture assignTask(AssignTaskRequest request, {CallOptions? options}) { + OfflineClientStore().taskStore.assignUser(request.taskId, request.userId); + final response = AssignTaskResponse(); + return MockResponseFuture.value(response); + } + + @override + ResponseFuture unassignTask(UnassignTaskRequest request, {CallOptions? options}) { + OfflineClientStore().taskStore.unassignUser(request.taskId, request.userId); + final response = UnassignTaskResponse(); + return MockResponseFuture.value(response); + } + + @override + ResponseFuture removeTaskDueDate(RemoveTaskDueDateRequest request, + {CallOptions? options}) { + OfflineClientStore().taskStore.removeDueDate(request.taskId); + final response = RemoveTaskDueDateResponse(); + return MockResponseFuture.value(response); + } + + @override + ResponseFuture deleteTask(DeleteTaskRequest request, {CallOptions? options}) { + OfflineClientStore().taskStore.delete(request.id); + + final response = DeleteTaskResponse(); + return MockResponseFuture.value(response); + } + + @override + ResponseFuture createSubtask(CreateSubtaskRequest request, {CallOptions? options}) { + final task = OfflineClientStore().taskStore.findTask(request.taskId); + if (task == null) { + throw "Task with id ${request.taskId} not found"; + } + final subtask = Subtask( + id: DateTime.now().millisecondsSinceEpoch.toString(), + taskId: request.taskId, + name: request.subtask.name, + isDone: request.subtask.done); + + OfflineClientStore().subtaskStore.create(subtask); + final response = CreateSubtaskResponse()..subtaskId = subtask.id; + return MockResponseFuture.value(response); + } + + @override + ResponseFuture updateSubtask(UpdateSubtaskRequest request, {CallOptions? options}) { + final requestSubtask = request.subtask; + final update = SubtaskUpdate( + id: request.subtaskId, + isDone: requestSubtask.hasDone() ? requestSubtask.done : null, + name: requestSubtask.hasName() ? requestSubtask.name : null, + ); + OfflineClientStore().subtaskStore.update(update); + + final response = UpdateSubtaskResponse(); + return MockResponseFuture.value(response); + } + + @override + ResponseFuture deleteSubtask(DeleteSubtaskRequest request, {CallOptions? options}) { + OfflineClientStore().subtaskStore.delete(request.subtaskId); + + final response = DeleteSubtaskResponse(); + return MockResponseFuture.value(response); + } +} diff --git a/packages/helpwave_service/lib/src/api/tasks/offline_clients/template_offline_client.dart b/packages/helpwave_service/lib/src/api/tasks/offline_clients/template_offline_client.dart new file mode 100644 index 00000000..3479128a --- /dev/null +++ b/packages/helpwave_service/lib/src/api/tasks/offline_clients/template_offline_client.dart @@ -0,0 +1,241 @@ +import 'package:grpc/grpc.dart'; +import 'package:helpwave_proto_dart/services/tasks_svc/v1/task_template_svc.pbgrpc.dart'; +import 'package:helpwave_service/src/api/offline/offline_client_store.dart'; +import 'package:helpwave_service/src/api/offline/util.dart'; +import 'package:helpwave_service/src/api/tasks/data_types/index.dart'; + +class TaskTemplateUpdate { + String id; + String? name; + String? notes; + + TaskTemplateUpdate({required this.id, this.name, this.notes}); +} + +class TaskSubtaskTemplateUpdate { + String id; + String? name; + + TaskSubtaskTemplateUpdate({required this.id, this.name}); +} + +class TaskTemplateOfflineService { + List taskTemplates = []; + + TaskTemplate? findTaskTemplate(String id) { + int index = taskTemplates.indexWhere((value) => value.id == id); + if (index == -1) { + return null; + } + return taskTemplates[index]; + } + + List findTaskTemplates([String? wardId]) { + if (wardId == null) { + return taskTemplates; + } + return taskTemplates.where((value) => value.wardId == wardId).toList(); + } + + void create(TaskTemplate taskTemplate) { + taskTemplates.add(taskTemplate); + } + + void update(TaskTemplateUpdate taskTemplateUpdate) { + bool found = false; + + taskTemplates = taskTemplates.map((value) { + if (value.id == taskTemplateUpdate.id) { + found = true; + return value.copyWith( + name: taskTemplateUpdate.name, + notes: taskTemplateUpdate.notes, + ); + } + return value; + }).toList(); + + if (!found) { + throw Exception('UpdateTaskTemplate: Could not find task with id ${taskTemplateUpdate.id}'); + } + } + + void delete(String taskId) { + taskTemplates = taskTemplates.where((value) => value.id != taskId).toList(); + OfflineClientStore().taskTemplateSubtaskStore.findTemplateSubtasks(taskId).forEach((templateSubtask) { + OfflineClientStore().taskTemplateSubtaskStore.delete(templateSubtask.id); + }); + } +} + +class TaskTemplateSubtaskOfflineService { + List taskTemplateSubtasks = []; + + Subtask? findTemplateSubtask(String id) { + int index = taskTemplateSubtasks.indexWhere((value) => value.id == id); + if (index == -1) { + return null; + } + return taskTemplateSubtasks[index]; + } + + List findTemplateSubtasks([String? taskTemplateId]) { + if (taskTemplateId == null) { + return taskTemplateSubtasks; + } + return taskTemplateSubtasks.where((value) => value.taskId == taskTemplateId).toList(); + } + + void create(Subtask templateSubtask) { + taskTemplateSubtasks.add(templateSubtask); + } + + void update(TaskSubtaskTemplateUpdate templateSubtaskUpdate) { + bool found = false; + + taskTemplateSubtasks = taskTemplateSubtasks.map((value) { + if (value.id == templateSubtaskUpdate.id) { + found = true; + return value.copyWith(name: templateSubtaskUpdate.name); + } + return value; + }).toList(); + + if (!found) { + throw Exception('UpdateTemplateSubtask: Could not find templateSubtask with id ${templateSubtaskUpdate.id}'); + } + } + + void delete(String templateSubtaskId) { + taskTemplateSubtasks = taskTemplateSubtasks.where((value) => value.id != templateSubtaskId).toList(); + } +} + +class TaskTemplateServicePromiseClient extends TaskTemplateServiceClient { + TaskTemplateServicePromiseClient(super.channel); + + @override + ResponseFuture getAllTaskTemplates(GetAllTaskTemplatesRequest request, + {CallOptions? options}) { + final user = OfflineClientStore().userStore.users[0]; + final templates = OfflineClientStore().taskTemplateStore.taskTemplates.where((template) { + if (request.hasWardId() && template.wardId != request.wardId) { + return false; + } + if (request.hasCreatedBy() && template.createdBy != request.createdBy) { + return false; + } + if (request.privateOnly && template.createdBy != user.id) { + return false; + } + return true; + }); + + final response = GetAllTaskTemplatesResponse( + templates: templates.map( + (template) => GetAllTaskTemplatesResponse_TaskTemplate( + id: template.id, + name: template.name, + createdBy: template.createdBy, + description: template.notes, + isPublic: template.isPublicVisible, + subtasks: OfflineClientStore() + .taskTemplateSubtaskStore + .findTemplateSubtasks(template.id) + .map((taskTemplateSubtask) => GetAllTaskTemplatesResponse_TaskTemplate_SubTask( + id: taskTemplateSubtask.id, + name: taskTemplateSubtask.name, + taskTemplateId: taskTemplateSubtask.taskId, + ))), + )); + return MockResponseFuture.value(response); + } + + @override + ResponseFuture createTaskTemplate(CreateTaskTemplateRequest request, + {CallOptions? options}) { + final newTaskTemplate = TaskTemplate( + id: DateTime.now().millisecondsSinceEpoch.toString(), + name: request.name, + notes: request.description, + wardId: request.hasWardId() ? request.wardId : null, + createdBy: OfflineClientStore().userStore.users[0].id, + ); + + OfflineClientStore().taskTemplateStore.create(newTaskTemplate); + for (var templateSubtask in request.subtasks) { + OfflineClientStore().taskTemplateSubtaskStore.create(Subtask( + id: DateTime.now().millisecondsSinceEpoch.toString(), + taskId: newTaskTemplate.id, + name: templateSubtask.name, + isDone: false, + )); + } + + final response = CreateTaskTemplateResponse()..id = newTaskTemplate.id; + + return MockResponseFuture.value(response); + } + + @override + ResponseFuture updateTaskTemplate(UpdateTaskTemplateRequest request, + {CallOptions? options}) { + final update = TaskTemplateUpdate( + id: request.id, + name: request.hasName() ? request.name : null, + ); + + OfflineClientStore().taskTemplateStore.update(update); + + final response = UpdateTaskTemplateResponse(); + return MockResponseFuture.value(response); + } + + @override + ResponseFuture deleteTaskTemplate(DeleteTaskTemplateRequest request, + {CallOptions? options}) { + OfflineClientStore().taskStore.delete(request.id); + + final response = DeleteTaskTemplateResponse(); + return MockResponseFuture.value(response); + } + + @override + ResponseFuture createTaskTemplateSubTask(CreateTaskTemplateSubTaskRequest request, + {CallOptions? options}) { + final task = OfflineClientStore().taskTemplateStore.findTaskTemplate(request.taskTemplateId); + if (task == null) { + throw "TaskTemplate with id ${request.taskTemplateId} not found"; + } + final templateSubtask = Subtask( + id: DateTime.now().millisecondsSinceEpoch.toString(), + taskId: request.taskTemplateId, + name: request.name, + isDone: false, + ); + + OfflineClientStore().taskTemplateSubtaskStore.create(templateSubtask); + final response = CreateTaskTemplateSubTaskResponse()..id = templateSubtask.id; + return MockResponseFuture.value(response); + } + + @override + ResponseFuture updateTaskTemplateSubTask(UpdateTaskTemplateSubTaskRequest request, {CallOptions? options}) { + final update = TaskSubtaskTemplateUpdate( + id: request.subtaskId, + name: request.hasName() ? request.name : null, + ); + OfflineClientStore().taskTemplateSubtaskStore.update(update); + + final response = UpdateTaskTemplateSubTaskResponse(); + return MockResponseFuture.value(response); + } + + @override + ResponseFuture deleteTaskTemplateSubTask(DeleteTaskTemplateSubTaskRequest request, {CallOptions? options}) { + OfflineClientStore().taskTemplateSubtaskStore.delete(request.id); + + final response = DeleteTaskTemplateSubTaskResponse(); + return MockResponseFuture.value(response); + } +} diff --git a/packages/helpwave_service/lib/src/api/tasks/offline_clients/ward_offline_client.dart b/packages/helpwave_service/lib/src/api/tasks/offline_clients/ward_offline_client.dart new file mode 100644 index 00000000..b2d5648d --- /dev/null +++ b/packages/helpwave_service/lib/src/api/tasks/offline_clients/ward_offline_client.dart @@ -0,0 +1,162 @@ +import 'package:grpc/grpc.dart'; +import 'package:helpwave_proto_dart/services/tasks_svc/v1/ward_svc.pbgrpc.dart'; +import 'package:helpwave_service/src/api/offline/offline_client_store.dart'; +import 'package:helpwave_service/src/api/offline/util.dart'; +import 'package:helpwave_service/src/api/tasks/index.dart'; + +class WardUpdate { + String id; + String? name; + + WardUpdate({required this.id, required this.name}); +} + +class WardOfflineService { + List wards = []; + + Ward? findWard(String id) { + int index = OfflineClientStore().wardStore.wards.indexWhere((value) => value.id == id); + if (index == -1) { + return null; + } + return wards[index]; + } + + List findWards([String? organizationId]) { + final valueStore = OfflineClientStore().wardStore; + if (organizationId == null) { + return valueStore.wards; + } + return valueStore.wards.where((value) => value.organizationId == organizationId).toList(); + } + + void create(Ward ward) { + OfflineClientStore().wardStore.wards.add(ward); + } + + void update(WardUpdate ward) { + final valueStore = OfflineClientStore().wardStore; + bool found = false; + + valueStore.wards = valueStore.wards.map((value) { + if (value.id == ward.id) { + found = true; + return Ward(id: ward.id, name: ward.name ?? value.name, organizationId: value.organizationId); + } + return value; + }).toList(); + + if (!found) { + throw Exception('UpdateWard: Could not find ward with id ${ward.id}'); + } + } + + void delete(String wardId) { + final valueStore = OfflineClientStore().wardStore; + valueStore.wards = valueStore.wards.where((value) => value.id != wardId).toList(); + OfflineClientStore().roomStore.findRooms(wardId).forEach((element) { + OfflineClientStore().roomStore.delete(element.id); + }); + // TODO delete ward templates + } +} + +class WardServicePromiseClient extends WardServiceClient { + WardServicePromiseClient(super.channel); + + @override + ResponseFuture getWard(GetWardRequest request, {CallOptions? options}) { + final ward = OfflineClientStore().wardStore.findWard(request.id); + + if (ward == null) { + throw "Ward with ward id ${request.id} not found"; + } + + final response = GetWardResponse() + ..id = ward.id + ..name = ward.name; + + return MockResponseFuture.value(response); + } + + @override + ResponseFuture getWardDetails(GetWardDetailsRequest request, {CallOptions? options}) { + final ward = OfflineClientStore().wardStore.findWard(request.id); + + if (ward == null) { + throw "Ward with ward id ${request.id} not found"; + } + + final rooms = OfflineClientStore().roomStore.findRooms(ward.id).map((room) { + final beds = OfflineClientStore() + .bedStore + .findBeds(room.id) + .map((bed) => GetWardDetailsResponse_Bed(id: bed.id, name: bed.name)) + .toList(); + + return GetWardDetailsResponse_Room() + ..id = room.id + ..name = room.name + ..beds.addAll(beds); + }).toList(); + + final response = GetWardDetailsResponse( + id: ward.id, + name: ward.name, + rooms: rooms, + // TODO taskTemplates: + ); + + return MockResponseFuture.value(response); + } + + @override + ResponseFuture getWards(GetWardsRequest request, {CallOptions? options}) { + final wards = OfflineClientStore().wardStore.findWards(); + final wardsList = wards + .map((ward) => GetWardsResponse_Ward() + ..id = ward.id + ..name = ward.name) + .toList(); + + final response = GetWardsResponse()..wards.addAll(wardsList); + + return MockResponseFuture.value(response); + } + + @override + ResponseFuture createWard(CreateWardRequest request, {CallOptions? options}) { + final newWard = Ward( + id: DateTime.now().millisecondsSinceEpoch.toString(), + name: request.name, + organizationId: 'organization', // TODO: Check organization + ); + + OfflineClientStore().wardStore.create(newWard); + + final response = CreateWardResponse()..id = newWard.id; + + return MockResponseFuture.value(response); + } + + @override + ResponseFuture updateWard(UpdateWardRequest request, {CallOptions? options}) { + final update = WardUpdate( + id: request.id, + name: request.name, + ); + + OfflineClientStore().wardStore.update(update); + + final response = UpdateWardResponse(); + return MockResponseFuture.value(response); + } + + @override + ResponseFuture deleteWard(DeleteWardRequest request, {CallOptions? options}) { + OfflineClientStore().wardStore.delete(request.id); + + final response = DeleteWardResponse(); + return MockResponseFuture.value(response); + } +} diff --git a/packages/helpwave_service/lib/src/api/tasks/services/patient_svc.dart b/packages/helpwave_service/lib/src/api/tasks/services/patient_svc.dart index c0c60d94..acfc6674 100644 --- a/packages/helpwave_service/lib/src/api/tasks/services/patient_svc.dart +++ b/packages/helpwave_service/lib/src/api/tasks/services/patient_svc.dart @@ -7,10 +7,10 @@ import 'package:helpwave_service/src/api/tasks/util/task_status_mapping.dart'; /// The GRPC Service for [Patient]s /// /// Provides queries and requests that load or alter [Patient] objects on the server -/// The server is defined in the underlying [TasksAPIServices] +/// The server is defined in the underlying [TasksAPIServiceClients] class PatientService { /// The GRPC ServiceClient which handles GRPC - PatientServiceClient patientService = TasksAPIServices.patientServiceClient; + PatientServiceClient patientService = TasksAPIServiceClients.patientServiceClient; // TODO consider an enum instead of an string /// Loads the [Patient]s by [Ward] and sorts them by their assignment status @@ -19,7 +19,7 @@ class PatientService { GetPatientListResponse response = await patientService.getPatientList( request, options: CallOptions( - metadata: TasksAPIServices().getMetaData(), + metadata: TasksAPIServiceClients().getMetaData(), ), ); @@ -36,14 +36,16 @@ class PatientService { notes: task.description, status: GRPCTypeConverter.taskStatusFromGRPC(task.status), isPublicVisible: task.public, - assignee: task.assignedUserId, + assigneeId: task.assignedUserId, subtasks: task.subtasks .map((subtask) => Subtask( id: subtask.id, name: subtask.name, isDone: subtask.done, + taskId: task.id )) .toList(), + patientId: patient.id, // TODO due and creation date )) .toList(), @@ -67,7 +69,7 @@ class PatientService { notes: task.description, status: GRPCTypeConverter.taskStatusFromGRPC(task.status), isPublicVisible: task.public, - assignee: task.assignedUserId, + assigneeId: task.assignedUserId, subtasks: task.subtasks .map((subtask) => Subtask( id: subtask.id, @@ -96,7 +98,7 @@ class PatientService { notes: task.description, status: GRPCTypeConverter.taskStatusFromGRPC(task.status), isPublicVisible: task.public, - assignee: task.assignedUserId, + assigneeId: task.assignedUserId, subtasks: task.subtasks .map((subtask) => Subtask( id: subtask.id, @@ -126,7 +128,7 @@ class PatientService { GetPatientResponse response = await patientService.getPatient( request, options: CallOptions( - metadata: TasksAPIServices().getMetaData(), + metadata: TasksAPIServiceClients().getMetaData(), ), ); @@ -143,7 +145,7 @@ class PatientService { GetPatientDetailsResponse response = await patientService.getPatientDetails( request, options: CallOptions( - metadata: TasksAPIServices().getMetaData(), + metadata: TasksAPIServiceClients().getMetaData(), ), ); @@ -157,7 +159,7 @@ class PatientService { id: task.id, name: task.name, notes: task.description, - assignee: task.assignedUserId, + assigneeId: task.assignedUserId, status: GRPCTypeConverter.taskStatusFromGRPC(task.status), isPublicVisible: task.public, subtasks: task.subtasks @@ -179,7 +181,7 @@ class PatientService { GetPatientAssignmentByWardResponse response = await patientService.getPatientAssignmentByWard( request, options: CallOptions( - metadata: TasksAPIServices().getMetaData(), + metadata: TasksAPIServiceClients().getMetaData(), ), ); @@ -207,7 +209,7 @@ class PatientService { ); CreatePatientResponse response = await patientService.createPatient( request, - options: CallOptions(metadata: TasksAPIServices().getMetaData()), + options: CallOptions(metadata: TasksAPIServiceClients().getMetaData()), ); return response.id; @@ -222,7 +224,7 @@ class PatientService { ); UpdatePatientResponse response = await patientService.updatePatient( request, - options: CallOptions(metadata: TasksAPIServices().getMetaData()), + options: CallOptions(metadata: TasksAPIServiceClients().getMetaData()), ); if (response.isInitialized()) { @@ -237,7 +239,7 @@ class PatientService { DischargePatientRequest request = DischargePatientRequest(id: patientId); DischargePatientResponse response = await patientService.dischargePatient( request, - options: CallOptions(metadata: TasksAPIServices().getMetaData()), + options: CallOptions(metadata: TasksAPIServiceClients().getMetaData()), ); if (response.isInitialized()) { @@ -251,7 +253,7 @@ class PatientService { UnassignBedRequest request = UnassignBedRequest(id: patientId); UnassignBedResponse response = await patientService.unassignBed( request, - options: CallOptions(metadata: TasksAPIServices().getMetaData()), + options: CallOptions(metadata: TasksAPIServiceClients().getMetaData()), ); if (response.isInitialized()) { @@ -265,7 +267,7 @@ class PatientService { AssignBedRequest request = AssignBedRequest(id: patientId, bedId: bedId); AssignBedResponse response = await patientService.assignBed( request, - options: CallOptions(metadata: TasksAPIServices().getMetaData()), + options: CallOptions(metadata: TasksAPIServiceClients().getMetaData()), ); if (response.isInitialized()) { diff --git a/packages/helpwave_service/lib/src/api/tasks/services/room_svc.dart b/packages/helpwave_service/lib/src/api/tasks/services/room_svc.dart index ca4f3d3b..859a9d97 100644 --- a/packages/helpwave_service/lib/src/api/tasks/services/room_svc.dart +++ b/packages/helpwave_service/lib/src/api/tasks/services/room_svc.dart @@ -1,21 +1,21 @@ import 'package:grpc/grpc.dart'; import 'package:helpwave_proto_dart/services/tasks_svc/v1/room_svc.pbgrpc.dart'; import 'package:helpwave_service/src/api/tasks/data_types/index.dart'; -import 'package:helpwave_service/src/api/tasks/tasks_api_services.dart'; +import 'package:helpwave_service/src/api/tasks/tasks_api_service_clients.dart'; /// The GRPC Service for [Room]s /// /// Provides queries and requests that load or alter [Room] objects on the server -/// The server is defined in the underlying [TasksAPIServices] +/// The server is defined in the underlying [TasksAPIServiceClients] class RoomService { /// The GRPC ServiceClient which handles GRPC - RoomServiceClient roomService = TasksAPIServices.roomServiceClient; + RoomServiceClient roomService = TasksAPIServiceClients.roomServiceClient; Future> getRoomOverviews({required String wardId}) async { GetRoomOverviewsByWardRequest request = GetRoomOverviewsByWardRequest(id: wardId); GetRoomOverviewsByWardResponse response = await roomService.getRoomOverviewsByWard( request, - options: CallOptions(metadata: TasksAPIServices().getMetaData()), + options: CallOptions(metadata: TasksAPIServiceClients().getMetaData()), ); List rooms = response.rooms diff --git a/packages/helpwave_service/lib/src/api/tasks/services/task_svc.dart b/packages/helpwave_service/lib/src/api/tasks/services/task_svc.dart index 3d2cf292..3c9969ae 100644 --- a/packages/helpwave_service/lib/src/api/tasks/services/task_svc.dart +++ b/packages/helpwave_service/lib/src/api/tasks/services/task_svc.dart @@ -7,17 +7,17 @@ import '../util/task_status_mapping.dart'; /// The GRPC Service for [Task]s /// /// Provides queries and requests that load or alter [Task] objects on the server -/// The server is defined in the underlying [TasksAPIServices] +/// The server is defined in the underlying [TasksAPIServiceClients] class TaskService { /// The GRPC ServiceClient which handles GRPC - TaskServiceClient taskService = TasksAPIServices.taskServiceClient; + TaskServiceClient taskService = TasksAPIServiceClients.taskServiceClient; /// Loads the [Task]s by a [Patient] identifier Future> getTasksByPatient({String? patientId}) async { GetTasksByPatientRequest request = GetTasksByPatientRequest(patientId: patientId); GetTasksByPatientResponse response = await taskService.getTasksByPatient( request, - options: CallOptions(metadata: TasksAPIServices().getMetaData()), + options: CallOptions(metadata: TasksAPIServiceClients().getMetaData()), ); return response.tasks @@ -27,7 +27,7 @@ class TaskService { notes: task.description, isPublicVisible: task.public, status: GRPCTypeConverter.taskStatusFromGRPC(task.status), - assignee: task.assignedUserId, + assigneeId: task.assignedUserId, dueDate: task.dueAt.toDateTime(), subtasks: task.subtasks .map((subtask) => Subtask( @@ -45,7 +45,7 @@ class TaskService { GetTaskRequest request = GetTaskRequest(id: id); GetTaskResponse response = await taskService.getTask( request, - options: CallOptions(metadata: TasksAPIServices().getMetaData()), + options: CallOptions(metadata: TasksAPIServiceClients().getMetaData()), ); return TaskWithPatient( @@ -78,7 +78,7 @@ class TaskService { ); CreateTaskResponse response = await taskService.createTask( request, - options: CallOptions(metadata: TasksAPIServices().getMetaData()), + options: CallOptions(metadata: TasksAPIServiceClients().getMetaData()), ); return response.id; @@ -89,7 +89,7 @@ class TaskService { AssignTaskRequest request = AssignTaskRequest(taskId: taskId, userId: userId); AssignTaskResponse response = await taskService.assignTask( request, - options: CallOptions(metadata: TasksAPIServices().getMetaData()), + options: CallOptions(metadata: TasksAPIServiceClients().getMetaData()), ); if (!response.isInitialized()) { @@ -107,7 +107,7 @@ class TaskService { )); CreateSubtaskResponse response = await taskService.createSubtask( request, - options: CallOptions(metadata: TasksAPIServices().getMetaData()), + options: CallOptions(metadata: TasksAPIServiceClients().getMetaData()), ); return Subtask( @@ -122,7 +122,7 @@ class TaskService { DeleteSubtaskRequest request = DeleteSubtaskRequest(subtaskId: subtaskId, taskId: taskId); DeleteSubtaskResponse response = await taskService.deleteSubtask( request, - options: CallOptions(metadata: TasksAPIServices().getMetaData()), + options: CallOptions(metadata: TasksAPIServiceClients().getMetaData()), ); return response.isInitialized(); @@ -137,7 +137,7 @@ class TaskService { ); UpdateSubtaskResponse response = await taskService.updateSubtask( request, - options: CallOptions(metadata: TasksAPIServices().getMetaData()), + options: CallOptions(metadata: TasksAPIServiceClients().getMetaData()), ); return response.isInitialized(); @@ -155,7 +155,7 @@ class TaskService { UpdateTaskResponse response = await taskService.updateTask( request, - options: CallOptions(metadata: TasksAPIServices().getMetaData()), + options: CallOptions(metadata: TasksAPIServiceClients().getMetaData()), ); return response.isInitialized(); diff --git a/packages/helpwave_service/lib/src/api/tasks/services/ward_service.dart b/packages/helpwave_service/lib/src/api/tasks/services/ward_service.dart index d34ebb15..60156291 100644 --- a/packages/helpwave_service/lib/src/api/tasks/services/ward_service.dart +++ b/packages/helpwave_service/lib/src/api/tasks/services/ward_service.dart @@ -1,22 +1,22 @@ import 'package:grpc/grpc.dart'; import 'package:helpwave_proto_dart/services/tasks_svc/v1/ward_svc.pbgrpc.dart'; import 'package:helpwave_service/src/api/tasks/data_types/index.dart'; -import 'package:helpwave_service/src/api/tasks/tasks_api_services.dart'; +import 'package:helpwave_service/src/api/tasks/tasks_api_service_clients.dart'; /// The Service for [Ward]s /// /// Provides queries and requests that load or alter [Ward] objects on the server -/// The server is defined in the underlying [TasksAPIServices] +/// The server is defined in the underlying [TasksAPIServiceClients] class WardService { /// The GRPC ServiceClient which handles GRPC - WardServiceClient wardService = TasksAPIServices.wardServiceClient; + WardServiceClient wardService = TasksAPIServiceClients.wardServiceClient; /// Loads a [WardMinimal] by its identifier Future getWard({required String id}) async { GetWardRequest request = GetWardRequest(id: id); GetWardResponse response = await wardService.getWard( request, - options: CallOptions(metadata: TasksAPIServices().getMetaData()), + options: CallOptions(metadata: TasksAPIServiceClients().getMetaData()), ); return WardMinimal( @@ -30,7 +30,7 @@ class WardService { GetWardOverviewsRequest request = GetWardOverviewsRequest(); GetWardOverviewsResponse response = await wardService.getWardOverviews( request, - options: CallOptions(metadata: TasksAPIServices().getMetaData(organizationId: organizationId)), + options: CallOptions(metadata: TasksAPIServiceClients().getMetaData(organizationId: organizationId)), ); return response.wards diff --git a/packages/helpwave_service/lib/src/api/tasks/tasks_api_services.dart b/packages/helpwave_service/lib/src/api/tasks/tasks_api_service_clients.dart similarity index 92% rename from packages/helpwave_service/lib/src/api/tasks/tasks_api_services.dart rename to packages/helpwave_service/lib/src/api/tasks/tasks_api_service_clients.dart index 1c09d123..8a578a29 100644 --- a/packages/helpwave_service/lib/src/api/tasks/tasks_api_services.dart +++ b/packages/helpwave_service/lib/src/api/tasks/tasks_api_service_clients.dart @@ -6,14 +6,14 @@ import 'package:helpwave_proto_dart/services/tasks_svc/v1/task_svc.pbgrpc.dart'; import 'package:helpwave_service/src/auth/index.dart'; /// The Underlying GrpcService it provides other clients and the correct metadata for the requests -class TasksAPIServices { +class TasksAPIServiceClients { /// The api URL used static String? apiUrl; static ClientChannel get serviceChannel { - assert(TasksAPIServices.apiUrl != null); + assert(TasksAPIServiceClients.apiUrl != null); return ClientChannel( - TasksAPIServices.apiUrl!, + TasksAPIServiceClients.apiUrl!, ); } diff --git a/packages/helpwave_service/lib/src/api/user/data_types/organization.dart b/packages/helpwave_service/lib/src/api/user/data_types/organization.dart index 0d3a6e96..ca0caf6c 100644 --- a/packages/helpwave_service/lib/src/api/user/data_types/organization.dart +++ b/packages/helpwave_service/lib/src/api/user/data_types/organization.dart @@ -1,17 +1,54 @@ -/// data class for [Organization] -class Organization { +class OrganizationMinimal { String id; - String name; String shortName; + String longName; - Organization({ + OrganizationMinimal({ required this.id, - required this.name, required this.shortName, + required this.longName, + }); +} + +/// data class for [Organization] +class Organization extends OrganizationMinimal { + String avatarURL; + String email; + bool isPersonal; + bool isVerified; + + Organization({ + required super.id, + required super.shortName, + required super.longName, + required this.avatarURL, + required this.email, + required this.isPersonal, + required this.isVerified, }); + Organization copyWith({ + String? shortName, + String? longName, + String? avatarURL, + String? email, + bool? isPersonal, + bool? isVerified, + }) { + return Organization( + id: id, + // `id` is not changeable + shortName: shortName ?? this.shortName, + longName: longName ?? this.longName, + avatarURL: avatarURL ?? this.avatarURL, + email: email ?? this.email, + isPersonal: isPersonal ?? this.isPersonal, + isVerified: isVerified ?? this.isVerified, + ); + } + @override String toString() { - return "{id: $id, name: $name, shortName: $shortName}"; + return "{id: $id, name: $longName, shortName: $shortName}"; } } diff --git a/packages/helpwave_service/lib/src/api/user/data_types/user.dart b/packages/helpwave_service/lib/src/api/user/data_types/user.dart index 54733fc5..0abd1c09 100644 --- a/packages/helpwave_service/lib/src/api/user/data_types/user.dart +++ b/packages/helpwave_service/lib/src/api/user/data_types/user.dart @@ -3,21 +3,29 @@ class User { String id; String name; String nickName; - Uri profile; - - // TODO add email + String email; + Uri profileUrl; User({ required this.id, required this.name, required this.nickName, - required this.profile, + required this.email, + required this.profileUrl, }); - factory User.empty({String id = ""}) => User(id: id, name: "", nickName: "", profile: Uri.base); + factory User.empty({String id = ""}) => User(id: id, name: "", nickName: "", email: "", profileUrl: Uri.base); bool get isCreating => id == ""; + User copyWith({String? name, String? nickName, Uri? profileUrl, String? email}) => User( + id: id, + name: name ?? this.name, + nickName: nickName ?? this.nickName, + profileUrl: profileUrl ?? this.profileUrl, + email: email ?? this.email, + ); + @override bool operator ==(Object other) { if (identical(this, other)) return true; diff --git a/packages/helpwave_service/lib/src/api/user/index.dart b/packages/helpwave_service/lib/src/api/user/index.dart index 243eabf0..8bfb7f1d 100644 --- a/packages/helpwave_service/lib/src/api/user/index.dart +++ b/packages/helpwave_service/lib/src/api/user/index.dart @@ -1,4 +1,4 @@ export 'data_types/index.dart'; export 'services/index.dart'; export 'controllers/index.dart'; -export 'user_api_services.dart'; +export 'user_api_service_clients.dart'; diff --git a/packages/helpwave_service/lib/src/api/user/offline_clients/organization_offline_client.dart b/packages/helpwave_service/lib/src/api/user/offline_clients/organization_offline_client.dart new file mode 100644 index 00000000..f5173556 --- /dev/null +++ b/packages/helpwave_service/lib/src/api/user/offline_clients/organization_offline_client.dart @@ -0,0 +1,271 @@ +import 'package:grpc/grpc.dart'; +import 'package:helpwave_proto_dart/services/user_svc/v1/organization_svc.pbgrpc.dart'; +import 'package:helpwave_service/src/api/offline/offline_client_store.dart'; +import 'package:helpwave_service/src/api/offline/util.dart'; +import 'package:helpwave_service/user.dart'; + +class OrganizationUpdate { + String id; + String shortName; + String longName; + String email; + bool isPersonal; + String avatarURL; + + OrganizationUpdate({ + required this.id, + required this.shortName, + required this.longName, + required this.email, + required this.isPersonal, + required this.avatarURL, + }); +} + +class OrganizationOfflineClientStore { + List organizations = []; + + Organization? find(String id) { + int index = organizations.indexWhere((org) => org.id == id); + if (index == -1) { + return null; + } + return organizations[index]; + } + + List findOrganizations() { + return organizations; + } + + void create(Organization organization) { + organizations.add(organization); + } + + void update(OrganizationUpdate organizationUpdate) { + bool found = false; + organizations = organizations.map((org) { + if (org.id == organizationUpdate.id) { + found = true; + return org.copyWith( + shortName: organizationUpdate.shortName, + longName: organizationUpdate.longName, + avatarURL: organizationUpdate.avatarURL, + email: organizationUpdate.email, + isPersonal: organizationUpdate.isPersonal); + } + return org; + }).toList(); + + if (!found) { + throw Exception("UpdateOrganization: Could not find organization with id ${organizationUpdate.id}"); + } + } + + void delete(String organizationId) { + organizations.removeWhere((org) => org.id == organizationId); + // Assuming WardOfflineService handles related deletions (Wards, etc.) + // WardOfflineService.deleteByOrganization(organizationId); + } +} + +class OrganizationOfflineClient extends OrganizationServiceClient { + OrganizationOfflineClient(super.channel); + + @override + ResponseFuture createOrganization(CreateOrganizationRequest request, + {CallOptions? options}) { + final newOrganization = Organization( + id: DateTime.now().millisecondsSinceEpoch.toString(), + shortName: request.shortName, + longName: request.longName, + avatarURL: 'https://helpwave.de/favicon.ico', + email: request.contactEmail, + isPersonal: request.isPersonal, + isVerified: true, + ); + + OfflineClientStore().organizationStore.create(newOrganization); + + return MockResponseFuture.value(CreateOrganizationResponse()..id = newOrganization.id); + } + + @override + ResponseFuture createOrganizationForUser(CreateOrganizationForUserRequest request, + {CallOptions? options}) { + final newOrganization = Organization( + id: DateTime.now().millisecondsSinceEpoch.toString(), + shortName: request.shortName, + longName: request.longName, + avatarURL: 'https://helpwave.de/favicon.ico', + email: request.contactEmail, + isPersonal: request.isPersonal, + isVerified: true, + ); + + OfflineClientStore().organizationStore.create(newOrganization); + + return MockResponseFuture.value(CreateOrganizationForUserResponse()..id = newOrganization.id); + } + + @override + ResponseFuture getOrganization(GetOrganizationRequest request, {CallOptions? options}) { + final organization = OfflineClientStore().organizationStore.find(request.id); + + if (organization == null) { + throw Exception("GetOrganization: Could not find organization with id ${request.id}"); + } + + final members = + OfflineClientStore().userStore.findUsers().map((user) => GetOrganizationMember()..userId = user.id).toList(); + + return MockResponseFuture.value(GetOrganizationResponse() + ..id = organization.id + ..shortName = organization.shortName + ..longName = organization.longName + ..avatarUrl = organization.avatarURL + ..contactEmail = organization.email + ..isPersonal = organization.isPersonal + ..members.addAll(members)); + } + + @override + ResponseFuture getOrganizationsByUser(GetOrganizationsByUserRequest request, + {CallOptions? options}) { + final organizations = OfflineClientStore().organizationStore.findOrganizations().map((org) { + final members = OfflineClientStore() + .userStore + .findUsers() + .map((user) => GetOrganizationsByUserResponse_Organization_Member()..userId = user.id) + .toList(); + + return GetOrganizationsByUserResponse_Organization() + ..id = org.id + ..shortName = org.shortName + ..longName = org.longName + ..contactEmail = org.email + ..avatarUrl = org.avatarURL + ..members.addAll(members) + ..isPersonal = org.isPersonal; + }).toList(); + + return MockResponseFuture.value(GetOrganizationsByUserResponse()..organizations.addAll(organizations)); + } + + @override + ResponseFuture getOrganizationsForUser(GetOrganizationsForUserRequest request, {CallOptions? options}) { + final organizations = OfflineClientStore().organizationStore.findOrganizations().map((org) { + final members = OfflineClientStore() + .userStore + .findUsers() + .map((user) => GetOrganizationsForUserResponse_Organization_Member()..userId = user.id) + .toList(); + + return GetOrganizationsForUserResponse_Organization() + ..id = org.id + ..shortName = org.shortName + ..longName = org.longName + ..contactEmail = org.email + ..avatarUrl = org.avatarURL + ..members.addAll(members) + ..isPersonal = org.isPersonal; + }).toList(); + + return MockResponseFuture.value(GetOrganizationsForUserResponse()..organizations.addAll(organizations)); + + } + + @override + ResponseFuture updateOrganization(UpdateOrganizationRequest request, + {CallOptions? options}) { + final update = OrganizationUpdate( + id: request.id, + shortName: request.shortName, + longName: request.longName, + email: request.contactEmail, + avatarURL: request.avatarUrl, + isPersonal: request.isPersonal, + ); + + try { + OfflineClientStore().organizationStore.update(update); + return MockResponseFuture.value(UpdateOrganizationResponse()); + } catch (e) { + return MockResponseFuture.error(e); + } + } + + @override + ResponseFuture deleteOrganization(DeleteOrganizationRequest request, + {CallOptions? options}) { + try { + OfflineClientStore().organizationStore.delete(request.id); + return MockResponseFuture.value(DeleteOrganizationResponse()); + } catch (e) { + return MockResponseFuture.error(e); + } + } + + // Other missing methods + + @override + ResponseFuture getMembersByOrganization(GetMembersByOrganizationRequest request, + {CallOptions? options}) { + final organization = OfflineClientStore().organizationStore.find(request.id); + + if (organization == null) { + throw Exception("GetMembersByOrganization: Could not find organization with id ${request.id}"); + } + + final members = OfflineClientStore() + .userStore + .findUsers() + .map((user) => GetMembersByOrganizationResponse_Member() + ..userId = user.id + ..email = user.email + ..nickname = user.nickName + ..avatarUrl = user.profileUrl.toString()) + .toList(); + + return MockResponseFuture.value(GetMembersByOrganizationResponse()..members.addAll(members)); + } + + @override + ResponseFuture getInvitationsByOrganization(GetInvitationsByOrganizationRequest request, {CallOptions? options}) { + return MockResponseFuture.value(GetInvitationsByOrganizationResponse()); + } + + @override + ResponseFuture getInvitationsByUser(GetInvitationsByUserRequest request, {CallOptions? options}) { + return MockResponseFuture.value(GetInvitationsByUserResponse()); + } + + @override + ResponseFuture inviteMember(InviteMemberRequest request, {CallOptions? options}) { + throw UnimplementedError('Not implemented yet'); + } + + @override + ResponseFuture revokeInvitation(RevokeInvitationRequest request, {CallOptions? options}) { + throw UnimplementedError('Not implemented yet'); + } + + @override + ResponseFuture acceptInvitation(AcceptInvitationRequest request, {CallOptions? options}) { + throw UnimplementedError('Not implemented yet'); + } + + @override + ResponseFuture declineInvitation(DeclineInvitationRequest request, {CallOptions? options}) { + throw UnimplementedError('Not implemented yet'); + } + + @override + ResponseFuture addMember(AddMemberRequest request, {CallOptions? options}) { + throw UnimplementedError('Not implemented yet'); + } + + @override + ResponseFuture removeMember(RemoveMemberRequest request, {CallOptions? options}) { + throw UnimplementedError('Not implemented yet'); + } +} diff --git a/packages/helpwave_service/lib/src/api/user/offline_clients/user_offline_client.dart b/packages/helpwave_service/lib/src/api/user/offline_clients/user_offline_client.dart new file mode 100644 index 00000000..10839d27 --- /dev/null +++ b/packages/helpwave_service/lib/src/api/user/offline_clients/user_offline_client.dart @@ -0,0 +1,119 @@ +import 'package:grpc/grpc_web.dart'; +import 'package:helpwave_service/src/api/offline/offline_client_store.dart'; +import 'package:helpwave_service/src/api/offline/util.dart'; +import 'package:helpwave_service/src/api/user/index.dart'; +import 'package:helpwave_proto_dart/services/user_svc/v1/user_svc.pbgrpc.dart'; + +class UserUpdate { + final String id; + + UserUpdate({required this.id}); +} + +class UserOfflineService { + List users = []; + + User? find(String id) { + int index = users.indexWhere((user) => user.id == id); + if (index == -1) { + return null; + } + return users[index]; + } + + List findUsers() { + return users; + } + + void create(User user) { + users.add(user); + } + + void update(UserUpdate user) { + bool found = false; + + users = users.map((u) { + if (u.id == user.id) { + found = true; + return u.copyWith(); + } + return u; + }).toList(); + + if (!found) { + throw Exception('UpdateUser: Could not find user with id ${user.id}'); + } + } + + void delete(String userId) { + users.removeWhere((u) => u.id == userId); + } +} + +class UserOfflineClient extends UserServiceClient { + UserOfflineClient(super.channel); + + @override + ResponseFuture readPublicProfile(ReadPublicProfileRequest request, + {CallOptions? options}) { + final user = OfflineClientStore().userStore.find(request.id); + if (user == null) { + return MockResponseFuture.error(Exception('ReadPublicProfile: Could not find user with id ${request.id}')); + } + final response = ReadPublicProfileResponse() + ..id = user.id + ..name = user.name + ..nickname = user.nickName + ..avatarUrl = user.profileUrl.toString(); + return MockResponseFuture.value(response); + } + + @override + ResponseFuture readSelf(ReadSelfRequest request, {CallOptions? options}) { + final user = OfflineClientStore().userStore.users[0]; + + final organizations = OfflineClientStore() + .organizationStore + .findOrganizations() + .map((org) => ReadSelfOrganization()..id = org.id) + .toList(); + + final response = ReadSelfResponse() + ..id = user.id + ..name = user.name + ..nickname = user.nickName + ..avatarUrl = user.profileUrl.toString() + ..organizations.addAll(organizations); + + return MockResponseFuture.value(response); + } + + @override + ResponseFuture createUser(CreateUserRequest request, {CallOptions? options}) { + final newUser = User( + id: DateTime.now().millisecondsSinceEpoch.toString(), + name: request.name, + nickName: request.nickname, + email: request.email, + profileUrl: Uri.parse('https://helpwave.de/favicon.ico'), + ); + + OfflineClientStore().userStore.create(newUser); + + final response = CreateUserResponse()..id = newUser.id; + return MockResponseFuture.value(response); + } + + @override + ResponseFuture updateUser(UpdateUserRequest request, {CallOptions? options}) { + final update = UserUpdate(id: request.id); + + try { + OfflineClientStore().userStore.update(update); + final response = UpdateUserResponse(); + return MockResponseFuture.value(response); + } catch (e) { + return MockResponseFuture.error(e); + } + } +} diff --git a/packages/helpwave_service/lib/src/api/user/services/organization_svc.dart b/packages/helpwave_service/lib/src/api/user/services/organization_svc.dart index 0b4283f8..0775410f 100644 --- a/packages/helpwave_service/lib/src/api/user/services/organization_svc.dart +++ b/packages/helpwave_service/lib/src/api/user/services/organization_svc.dart @@ -1,31 +1,33 @@ import 'package:grpc/grpc.dart'; import 'package:helpwave_proto_dart/services/user_svc/v1/organization_svc.pbgrpc.dart'; import 'package:helpwave_service/auth.dart'; -import 'package:helpwave_service/src/api/user/user_api_services.dart'; +import 'package:helpwave_service/src/api/user/user_api_service_clients.dart'; import '../data_types/index.dart'; /// The GRPC Service for [Organization]s /// /// Provides queries and requests that load or alter [Organization] objects on the server -/// The server is defined in the underlying [UserAPIServices] +/// The server is defined in the underlying [UserAPIServiceClients] class OrganizationService { /// The GRPC ServiceClient which handles GRPC - OrganizationServiceClient organizationService = UserAPIServices.organizationServiceClient; + OrganizationServiceClient organizationService = UserAPIServiceClients.organizationServiceClient; /// Load a Organization by its identifier Future getOrganization({required String id}) async { GetOrganizationRequest request = GetOrganizationRequest(id: id); GetOrganizationResponse response = await organizationService.getOrganization( request, - options: CallOptions(metadata: UserAPIServices.getMetaData(organizationId: id)), + options: CallOptions(metadata: UserAPIServiceClients.getMetaData(organizationId: id)), ); - // TODO use full information of request Organization organization = Organization( - id: response.id, - name: response.longName, - shortName: response.shortName, - ); + id: response.id, + longName: response.longName, + shortName: response.shortName, + avatarURL: response.avatarUrl, + email: response.contactEmail, + isVerified: true, + isPersonal: response.isPersonal); return organization; } @@ -35,19 +37,21 @@ class OrganizationService { GetOrganizationsForUserResponse response = await organizationService.getOrganizationsForUser( request, options: CallOptions( - metadata: UserAPIServices.getMetaData( + metadata: UserAPIServiceClients.getMetaData( organizationId: AuthenticationUtility.fallbackOrganizationId, ), ), ); List organizations = response.organizations - // TODO use full information of request .map((organization) => Organization( - id: organization.id, - name: organization.longName, - shortName: organization.shortName, - )) + id: organization.id, + longName: organization.id, + shortName: organization.shortName, + avatarURL: organization.avatarUrl, + email: organization.contactEmail, + isVerified: true, + isPersonal: organization.isPersonal)) .toList(); return organizations; } @@ -58,7 +62,7 @@ class OrganizationService { GetMembersByOrganizationResponse response = await organizationService.getMembersByOrganization( request, options: CallOptions( - metadata: UserAPIServices.getMetaData(organizationId: organizationId), + metadata: UserAPIServiceClients.getMetaData(organizationId: organizationId), ), ); @@ -67,7 +71,8 @@ class OrganizationService { id: member.userId, name: member.nickname, // TODO replace this nickName: member.nickname, - profile: Uri.parse(member.avatarUrl), + email: member.email, + profileUrl: Uri.parse(member.avatarUrl), )) .toList(); return users; diff --git a/packages/helpwave_service/lib/src/api/user/services/user_service.dart b/packages/helpwave_service/lib/src/api/user/services/user_service.dart index a667b4cb..52468816 100644 --- a/packages/helpwave_service/lib/src/api/user/services/user_service.dart +++ b/packages/helpwave_service/lib/src/api/user/services/user_service.dart @@ -1,16 +1,16 @@ import 'package:grpc/grpc.dart'; import 'package:helpwave_proto_dart/services/user_svc/v1/user_svc.pbgrpc.dart'; import 'package:helpwave_service/auth.dart'; -import 'package:helpwave_service/src/api/user/user_api_services.dart'; +import 'package:helpwave_service/src/api/user/user_api_service_clients.dart'; import '../data_types/index.dart'; /// The GRPC Service for [User]s /// /// Provides queries and requests that load or alter [User] objects on the server -/// The server is defined in the underlying [UserAPIServices] +/// The server is defined in the underlying [UserAPIServiceClients] class UserService { /// The GRPC ServiceClient which handles GRPC - UserServiceClient userService = UserAPIServices.userServiceClient; + UserServiceClient userService = UserAPIServiceClients.userServiceClient; /// Loads the [User]s by it's identifier Future getUser({String? id}) async { @@ -18,7 +18,7 @@ class UserService { ReadPublicProfileResponse response = await userService.readPublicProfile( request, options: CallOptions( - metadata: UserAPIServices.getMetaData( + metadata: UserAPIServiceClients.getMetaData( organizationId: AuthenticationUtility.fallbackOrganizationId, ), ), @@ -28,7 +28,8 @@ class UserService { id: response.id, name: response.name, nickName: response.nickname, - profile: Uri.parse(response.avatarUrl), + email: "no-email", // TODO replace this + profileUrl: Uri.parse(response.avatarUrl), ); } } diff --git a/packages/helpwave_service/lib/src/api/user/user_api_services.dart b/packages/helpwave_service/lib/src/api/user/user_api_service_clients.dart similarity index 89% rename from packages/helpwave_service/lib/src/api/user/user_api_services.dart rename to packages/helpwave_service/lib/src/api/user/user_api_service_clients.dart index 695cb5f9..5e1f8954 100644 --- a/packages/helpwave_service/lib/src/api/user/user_api_services.dart +++ b/packages/helpwave_service/lib/src/api/user/user_api_service_clients.dart @@ -6,14 +6,14 @@ import 'package:helpwave_service/src/auth/index.dart'; /// A bundling of all User API services which can be used and are configured /// /// Make sure to set the [apiURL] to use the services -class UserAPIServices { +class UserAPIServiceClients { /// The api URL used static String? apiUrl; static ClientChannel get serviceChannel { - assert(UserAPIServices.apiUrl != null); + assert(UserAPIServiceClients.apiUrl != null); return ClientChannel( - UserAPIServices.apiUrl!, + UserAPIServiceClients.apiUrl!, ); } diff --git a/packages/helpwave_service/lib/src/auth/current_ward_svc.dart b/packages/helpwave_service/lib/src/auth/current_ward_svc.dart index 8413e807..bca9ab3f 100644 --- a/packages/helpwave_service/lib/src/auth/current_ward_svc.dart +++ b/packages/helpwave_service/lib/src/auth/current_ward_svc.dart @@ -9,7 +9,7 @@ class CurrentWardInformation { final WardMinimal ward; /// The identifier of the organization - final Organization organization; + final OrganizationMinimal organization; String get wardId => ward.id; @@ -17,11 +17,11 @@ class CurrentWardInformation { String get organizationId => organization.id; - String get organizationName => "${organization.name} (${organization.shortName})"; + String get organizationName => "${organization.longName} (${organization.shortName})"; bool get isEmpty => wardId == "" || organizationId == ""; - bool get hasFullInformation => ward.name != "" && organization.name != ""; + bool get hasFullInformation => ward.name != "" && organization.longName != ""; CurrentWardInformation(this.ward, this.organization); @@ -62,7 +62,7 @@ class _CurrentWardPreferences { if (wardId != null && organizationId != null) { return CurrentWardInformation( WardMinimal(id: wardId, name: ""), - Organization(id: organizationId, name: "", shortName: ""), + OrganizationMinimal(id: organizationId, longName: "", shortName: ""), ); } return null; diff --git a/packages/helpwave_util/lib/lists.dart b/packages/helpwave_util/lib/lists.dart new file mode 100644 index 00000000..da052351 --- /dev/null +++ b/packages/helpwave_util/lib/lists.dart @@ -0,0 +1 @@ +export 'package:helpwave_util/src/lists/range.dart'; diff --git a/packages/helpwave_util/lib/src/lists/range.dart b/packages/helpwave_util/lib/src/lists/range.dart new file mode 100644 index 00000000..4119bb3a --- /dev/null +++ b/packages/helpwave_util/lib/src/lists/range.dart @@ -0,0 +1,11 @@ +List range(int start, int end, [int step = 1]) { + if (step == 0) { + throw ArgumentError('Step cannot be zero.'); + } + + List result = []; + for (int i = start; (step > 0 ? i < end : i > end); i += step) { + result.add(i); + } + return result; +} From f97d8c6b41e511062c2c0444dc95056843fdcbf4 Mon Sep 17 00:00:00 2001 From: Felix Thape Date: Sun, 15 Sep 2024 20:10:27 +0200 Subject: [PATCH 03/36] fix: fix some errors --- .../controllers/my_tasks_controller.dart | 3 +- .../tasks/controllers/task_controller.dart | 2 +- .../lib/src/api/tasks/data_types/task.dart | 1 + .../src/api/tasks/services/patient_svc.dart | 151 ++++++------------ .../lib/src/api/tasks/services/task_svc.dart | 44 ++--- 5 files changed, 78 insertions(+), 123 deletions(-) diff --git a/packages/helpwave_service/lib/src/api/tasks/controllers/my_tasks_controller.dart b/packages/helpwave_service/lib/src/api/tasks/controllers/my_tasks_controller.dart index 98de67d5..05c0b4e5 100644 --- a/packages/helpwave_service/lib/src/api/tasks/controllers/my_tasks_controller.dart +++ b/packages/helpwave_service/lib/src/api/tasks/controllers/my_tasks_controller.dart @@ -44,11 +44,12 @@ class MyTasksController extends ChangeNotifier { _tasks.add(TaskWithPatient( id: task.id, name: task.name, - assignee: task.assigneeId, + assigneeId: task.assigneeId, notes: task.notes, dueDate: task.dueDate, status: task.status, subtasks: task.subtasks, + patientId: patient.id, patient: patient, )); } diff --git a/packages/helpwave_service/lib/src/api/tasks/controllers/task_controller.dart b/packages/helpwave_service/lib/src/api/tasks/controllers/task_controller.dart index a797f9e3..c52081d0 100644 --- a/packages/helpwave_service/lib/src/api/tasks/controllers/task_controller.dart +++ b/packages/helpwave_service/lib/src/api/tasks/controllers/task_controller.dart @@ -141,7 +141,7 @@ class TaskController extends ChangeNotifier { /// Only usable when creating Future changePatient(PatientMinimal patient) async { assert(task.isCreating, "Only use TaskController.changePatient, when you create a new task."); - task.patient = patient; + task = task.copyWith(patient); notifyListeners(); } diff --git a/packages/helpwave_service/lib/src/api/tasks/data_types/task.dart b/packages/helpwave_service/lib/src/api/tasks/data_types/task.dart index 9b13a237..a8b09b82 100644 --- a/packages/helpwave_service/lib/src/api/tasks/data_types/task.dart +++ b/packages/helpwave_service/lib/src/api/tasks/data_types/task.dart @@ -131,6 +131,7 @@ class TaskWithPatient extends Task { super.subtasks, super.dueDate, super.creationDate, + super.createdBy, super.isPublicVisible, required super.patientId, required this.patient, diff --git a/packages/helpwave_service/lib/src/api/tasks/services/patient_svc.dart b/packages/helpwave_service/lib/src/api/tasks/services/patient_svc.dart index acfc6674..39237b04 100644 --- a/packages/helpwave_service/lib/src/api/tasks/services/patient_svc.dart +++ b/packages/helpwave_service/lib/src/api/tasks/services/patient_svc.dart @@ -3,7 +3,6 @@ import 'package:helpwave_proto_dart/services/tasks_svc/v1/patient_svc.pbgrpc.dar import 'package:helpwave_service/src/api/tasks/index.dart'; import 'package:helpwave_service/src/api/tasks/util/task_status_mapping.dart'; - /// The GRPC Service for [Patient]s /// /// Provides queries and requests that load or alter [Patient] objects on the server @@ -23,96 +22,41 @@ class PatientService { ), ); - List active = response.active - .map( - (patient) => Patient( - id: patient.id, - name: patient.humanReadableIdentifier, - isDischarged: response.dischargedPatients.contains(patient), - tasks: patient.tasks - .map((task) => Task( - id: task.id, - name: task.name, - notes: task.description, - status: GRPCTypeConverter.taskStatusFromGRPC(task.status), - isPublicVisible: task.public, - assigneeId: task.assignedUserId, - subtasks: task.subtasks - .map((subtask) => Subtask( - id: subtask.id, - name: subtask.name, - isDone: subtask.done, - taskId: task.id - )) - .toList(), - patientId: patient.id, - // TODO due and creation date - )) - .toList(), - notes: patient.notes, - bed: BedMinimal(id: patient.bed.id, name: patient.bed.name), - room: RoomMinimal(id: patient.room.id, name: patient.room.name), - ), - ) - .toList(); - - List unassigned = response.unassignedPatients - .map( - (patient) => Patient( - id: patient.id, - name: patient.humanReadableIdentifier, - isDischarged: response.dischargedPatients.contains(patient), - tasks: patient.tasks - .map((task) => Task( - id: task.id, - name: task.name, - notes: task.description, - status: GRPCTypeConverter.taskStatusFromGRPC(task.status), - isPublicVisible: task.public, - assigneeId: task.assignedUserId, - subtasks: task.subtasks - .map((subtask) => Subtask( - id: subtask.id, - name: subtask.name, - isDone: subtask.done, - )) - .toList(), - // TODO due and creation date - )) - .toList(), - notes: patient.notes, - ), - ) - .toList(); + mapping(GetPatientListResponse_Patient patient) { + final res = Patient( + id: patient.id, + name: patient.humanReadableIdentifier, + isDischarged: response.dischargedPatients.contains(patient), + tasks: patient.tasks + .map((task) => Task( + id: task.id, + name: task.name, + notes: task.description, + status: GRPCTypeConverter.taskStatusFromGRPC(task.status), + isPublicVisible: task.public, + assigneeId: task.assignedUserId, + subtasks: task.subtasks + .map((subtask) => + Subtask(id: subtask.id, name: subtask.name, isDone: subtask.done, taskId: task.id)) + .toList(), + patientId: patient.id, + // TODO due and creation date + )) + .toList(), + notes: patient.notes, + bed: BedMinimal(id: patient.bed.id, name: patient.bed.name), + room: RoomMinimal(id: patient.room.id, name: patient.room.name), + ); + if (patient.hasBed() && patient.hasRoom()) { + res.bed = BedMinimal(id: patient.bed.id, name: patient.bed.name); + res.room = RoomMinimal(id: patient.room.id, name: patient.room.name); + } + return res; + } - List discharged = response.dischargedPatients - .map( - (patient) => Patient( - id: patient.id, - name: patient.humanReadableIdentifier, - isDischarged: true, - tasks: patient.tasks - .map((task) => Task( - id: task.id, - name: task.name, - notes: task.description, - status: GRPCTypeConverter.taskStatusFromGRPC(task.status), - isPublicVisible: task.public, - assigneeId: task.assignedUserId, - subtasks: task.subtasks - .map((subtask) => Subtask( - id: subtask.id, - name: subtask.name, - isDone: subtask.done, - )) - .toList(), - // TODO due and creation date - )) - .toList(), - notes: patient.notes, - ), - ) - .toList(); + final active = response.active.map(mapping).toList(); + final unassigned = response.unassignedPatients.map(mapping).toList(); + final discharged = response.dischargedPatients.map(mapping).toList(); return PatientsByAssignmentStatus( all: active + unassigned + discharged, @@ -156,19 +100,20 @@ class PatientService { isDischarged: response.isDischarged, tasks: response.tasks .map((task) => Task( - id: task.id, - name: task.name, - notes: task.description, - assigneeId: task.assignedUserId, - status: GRPCTypeConverter.taskStatusFromGRPC(task.status), - isPublicVisible: task.public, - subtasks: task.subtasks - .map((subtask) => Subtask( - id: subtask.id, - name: subtask.name, - )) - .toList(), - )) + id: task.id, + name: task.name, + notes: task.description, + assigneeId: task.assignedUserId, + status: GRPCTypeConverter.taskStatusFromGRPC(task.status), + isPublicVisible: task.public, + subtasks: task.subtasks + .map((subtask) => Subtask( + id: subtask.id, + taskId: task.id, + name: subtask.name, + )) + .toList(), + patientId: response.id)) .toList(), bed: response.hasBed() ? BedMinimal(id: response.bed.id, name: response.bed.name) : null, room: response.hasRoom() ? RoomMinimal(id: response.room.id, name: response.room.name) : null, diff --git a/packages/helpwave_service/lib/src/api/tasks/services/task_svc.dart b/packages/helpwave_service/lib/src/api/tasks/services/task_svc.dart index 3c9969ae..9a4a0e1a 100644 --- a/packages/helpwave_service/lib/src/api/tasks/services/task_svc.dart +++ b/packages/helpwave_service/lib/src/api/tasks/services/task_svc.dart @@ -22,21 +22,24 @@ class TaskService { return response.tasks .map((task) => Task( - id: task.id, - name: task.name, - notes: task.description, - isPublicVisible: task.public, - status: GRPCTypeConverter.taskStatusFromGRPC(task.status), - assigneeId: task.assignedUserId, - dueDate: task.dueAt.toDateTime(), - subtasks: task.subtasks - .map((subtask) => Subtask( - id: subtask.id, - name: subtask.name, - isDone: subtask.done, - )) - .toList(), - )) + id: task.id, + name: task.name, + notes: task.description, + isPublicVisible: task.public, + status: GRPCTypeConverter.taskStatusFromGRPC(task.status), + assigneeId: task.assignedUserId, + dueDate: task.dueAt.toDateTime(), + subtasks: task.subtasks + .map((subtask) => Subtask( + id: subtask.id, + taskId: task.id, + name: subtask.name, + isDone: subtask.done, + )) + .toList(), + patientId: task.patientId, + createdBy: task.createdBy, + creationDate: task.createdAt.toDateTime())) .toList(); } @@ -53,17 +56,21 @@ class TaskService { name: response.name, notes: response.description, isPublicVisible: response.public, - status: GRPCTypeConverter.taskStatusFromGRPC(response.status), - assignee: response.assignedUserId, + status: GRPCTypeConverter.taskStatusFromGRPC(response.status), + assigneeId: response.assignedUserId, dueDate: response.dueAt.toDateTime(), patient: PatientMinimal(id: response.patient.id, name: response.patient.humanReadableIdentifier), subtasks: response.subtasks .map((subtask) => Subtask( id: subtask.id, + taskId: response.id, name: subtask.name, isDone: subtask.done, )) .toList(), + patientId: response.patient.id, + createdBy: response.createdBy, + creationDate: response.createdAt.toDateTime(), ); } @@ -71,7 +78,7 @@ class TaskService { CreateTaskRequest request = CreateTaskRequest( name: task.name, description: task.notes, - initialStatus: GRPCTypeConverter.taskStatusToGRPC(task.status), + initialStatus: GRPCTypeConverter.taskStatusToGRPC(task.status), dueAt: task.dueDate != null ? Timestamp.fromDateTime(task.dueDate!) : null, patientId: !task.patient.isCreating ? task.patient.id : null, public: task.isPublicVisible, @@ -112,6 +119,7 @@ class TaskService { return Subtask( id: response.subtaskId, + taskId: taskId, name: subTask.name, isDone: subTask.isDone, ); From 5a5b2d922be0210f9d1cecb8f12a223cb6258c12 Mon Sep 17 00:00:00 2001 From: Felix Thape Date: Mon, 16 Sep 2024 00:54:38 +0200 Subject: [PATCH 04/36] feat: use offline mode api --- .../tasks/lib/components/assignee_select.dart | 45 +++++----- .../lib/components/patient_bottom_sheet.dart | 7 +- apps/tasks/lib/components/subtask_list.dart | 2 +- apps/tasks/lib/debug/theme_visualizer.dart | 1 + apps/tasks/lib/main.dart | 8 +- apps/tasks/lib/screens/main_screen.dart | 2 +- .../patient_screen.dart | 2 +- .../tasks/lib/screens/ward_select_screen.dart | 2 - .../src/api/offline/offline_client_store.dart | 77 ++++++++++++++++- .../tasks/controllers/task_controller.dart | 4 +- .../offline_clients/bed_offline_client.dart | 5 +- .../patient_offline_client.dart | 46 ++++++++-- .../offline_clients/room_offline_client.dart | 4 +- .../offline_clients/task_offline_client.dart | 4 +- .../offline_clients/ward_offline_client.dart | 84 ++++++++++++++++++- .../src/api/tasks/services/patient_svc.dart | 2 +- .../lib/src/api/tasks/services/room_svc.dart | 2 +- .../lib/src/api/tasks/services/task_svc.dart | 2 +- .../src/api/tasks/services/ward_service.dart | 2 +- .../api/tasks/tasks_api_service_clients.dart | 35 +++++--- .../src/api/user/data_types/organization.dart | 4 +- .../offline_clients/user_offline_client.dart | 2 +- .../api/user/services/organization_svc.dart | 10 +-- .../src/api/user/services/user_service.dart | 4 +- .../api/user/user_api_service_clients.dart | 28 +++++-- 25 files changed, 297 insertions(+), 87 deletions(-) diff --git a/apps/tasks/lib/components/assignee_select.dart b/apps/tasks/lib/components/assignee_select.dart index 08de7ad8..746dc1d0 100644 --- a/apps/tasks/lib/components/assignee_select.dart +++ b/apps/tasks/lib/components/assignee_select.dart @@ -19,31 +19,26 @@ class AssigneeSelect extends StatelessWidget { return BottomSheetBase( titleText: context.localization!.assignee, onClosing: () => {}, - builder: (context) => Flexible( - child: Padding( - padding: const EdgeInsets.symmetric(vertical: paddingMedium), - child: Consumer(builder: (context, assigneeSelectController, __) { - return LoadingAndErrorWidget( - state: assigneeSelectController.state, - child: ListView.builder( - itemCount: assigneeSelectController.users.length, - itemBuilder: (context, index) { - User user = assigneeSelectController.users[index]; - return ListTile( - onTap: () { - assigneeSelectController.changeAssignee(user.id).then((value) { - onChanged(user); - }); - }, - leading: CircleAvatar( - foregroundColor: Colors.blue, backgroundImage: NetworkImage(user.profileUrl.toString())), - title: Text(user.nickName), - ); - }, - ), - ); - }), - ), + builder: (context) => Padding( + padding: const EdgeInsets.symmetric(vertical: paddingMedium), + child: Consumer(builder: (context, assigneeSelectController, __) { + return LoadingAndErrorWidget( + state: assigneeSelectController.state, + child: Column( + children: assigneeSelectController.users.map((user) => + ListTile( + onTap: () { + assigneeSelectController.changeAssignee(user.id).then((value) { + onChanged(user); + }); + }, + leading: CircleAvatar( + foregroundColor: Colors.blue, backgroundImage: NetworkImage(user.profileUrl.toString())), + title: Text(user.nickName), + )).toList(), + ), + ); + }), ), ); } diff --git a/apps/tasks/lib/components/patient_bottom_sheet.dart b/apps/tasks/lib/components/patient_bottom_sheet.dart index 12b6e91e..a987645c 100644 --- a/apps/tasks/lib/components/patient_bottom_sheet.dart +++ b/apps/tasks/lib/components/patient_bottom_sheet.dart @@ -170,10 +170,7 @@ class _PatientBottomSheetState extends State { context.localization!.assignBed, style: TextStyle(color: Theme.of(context).colorScheme.secondary.withOpacity(0.6)), ), - value: !patientController.patient.isUnassigned - ? RoomWithBedFlat( - room: patientController.patient.room!, bed: patientController.patient.bed!) - : null, + value: beds.where((beds) => beds.bed.id == patientController.patient.bed?.id).firstOrNull, items: beds .map((roomWithBed) => DropdownMenuItem( value: roomWithBed, @@ -265,7 +262,7 @@ class _PatientBottomSheetState extends State { // TODO use return value to add it to task list or force a refetch onAdd: () => context.pushModal( context: context, - builder: (context) => TaskBottomSheet(task: Task.empty, patient: patient), + builder: (context) => TaskBottomSheet(task: Task.empty(patient.id), patient: patient), ), ); }), diff --git a/apps/tasks/lib/components/subtask_list.dart b/apps/tasks/lib/components/subtask_list.dart index 11433f36..01c5d314 100644 --- a/apps/tasks/lib/components/subtask_list.dart +++ b/apps/tasks/lib/components/subtask_list.dart @@ -41,7 +41,7 @@ class SubtaskList extends StatelessWidget { style: const TextStyle(fontWeight: FontWeight.bold, fontSize: 16), ), onAdd: () => subtasksController - .add(Subtask(id: "", name: "Subtask ${subtasksController.subtasks.length + 1}")) + .add(Subtask(id: "", name: "Subtask ${subtasksController.subtasks.length + 1}", taskId: taskId)) .then((_) => onChange(subtasksController.subtasks)), itemBuilder: (context, _, subtask) => ListTile( contentPadding: EdgeInsets.zero, diff --git a/apps/tasks/lib/debug/theme_visualizer.dart b/apps/tasks/lib/debug/theme_visualizer.dart index d042da5b..be3cee9f 100644 --- a/apps/tasks/lib/debug/theme_visualizer.dart +++ b/apps/tasks/lib/debug/theme_visualizer.dart @@ -44,6 +44,7 @@ class ThemeVisualizer extends StatelessWidget { name: "Task", notes: "Some Notes", status: TaskStatus.inProgress, + patientId: "patient", dueDate: DateTime.now().add(const Duration(hours: 2)), patient: PatientMinimal( id: "patient", diff --git a/apps/tasks/lib/main.dart b/apps/tasks/lib/main.dart index 5a4da007..65b37680 100644 --- a/apps/tasks/lib/main.dart +++ b/apps/tasks/lib/main.dart @@ -1,5 +1,6 @@ import 'package:flutter/material.dart'; import 'package:helpwave_service/auth.dart'; +import 'package:helpwave_service/user.dart'; import 'package:provider/provider.dart'; import 'package:helpwave_localization/l10n/app_localizations.dart'; import 'package:helpwave_localization/localization.dart'; @@ -11,7 +12,12 @@ import 'package:tasks/screens/login_screen.dart'; void main() { UserSessionService().changeMode(devMode); - TasksAPIServiceClients.apiUrl = usedAPIURL; + TasksAPIServiceClients() + ..apiUrl = usedAPIURL + ..offlineMode = true; + UserAPIServiceClients() + ..apiUrl = usedAPIURL + ..offlineMode = true; runApp(const MyApp()); } diff --git a/apps/tasks/lib/screens/main_screen.dart b/apps/tasks/lib/screens/main_screen.dart index 1235b056..60797a42 100644 --- a/apps/tasks/lib/screens/main_screen.dart +++ b/apps/tasks/lib/screens/main_screen.dart @@ -142,7 +142,7 @@ class _TaskPatientFloatingActionButton extends StatelessWidget { onPressed: () { context.pushModal( context: context, - builder: (context) => TaskBottomSheet(task: Task.empty), + builder: (context) => TaskBottomSheet(task: Task.empty("")), ); }, ), diff --git a/apps/tasks/lib/screens/main_screen_subscreens/patient_screen.dart b/apps/tasks/lib/screens/main_screen_subscreens/patient_screen.dart index 80ce9f6f..2278a361 100644 --- a/apps/tasks/lib/screens/main_screen_subscreens/patient_screen.dart +++ b/apps/tasks/lib/screens/main_screen_subscreens/patient_screen.dart @@ -118,7 +118,7 @@ class _PatientScreenState extends State { context.pushModal( context: context, builder: (context) => TaskBottomSheet( - task: Task.empty, + task: Task.empty(patient.id), patient: patient, ), ).then((value) => patientController.load()); diff --git a/apps/tasks/lib/screens/ward_select_screen.dart b/apps/tasks/lib/screens/ward_select_screen.dart index 73d0018c..41c06071 100644 --- a/apps/tasks/lib/screens/ward_select_screen.dart +++ b/apps/tasks/lib/screens/ward_select_screen.dart @@ -42,7 +42,6 @@ class _WardSelectScreen extends State { body: Column( children: [ ListTile( - // TODO change to organization name title: Text(organization?.longName ?? context.localization!.none), subtitle: Text(context.localization!.organization), trailing: const Icon(Icons.arrow_forward), @@ -71,7 +70,6 @@ class _WardSelectScreen extends State { }), ), ListTile( - // TODO change to organization name title: Text(ward?.name ?? context.localization!.none), subtitle: Text(context.localization!.ward), trailing: const Icon(Icons.arrow_forward), diff --git a/packages/helpwave_service/lib/src/api/offline/offline_client_store.dart b/packages/helpwave_service/lib/src/api/offline/offline_client_store.dart index 3fe8d420..d61296a2 100644 --- a/packages/helpwave_service/lib/src/api/offline/offline_client_store.dart +++ b/packages/helpwave_service/lib/src/api/offline/offline_client_store.dart @@ -1,3 +1,4 @@ +import 'package:helpwave_service/src/api/tasks/index.dart'; import 'package:helpwave_service/src/api/tasks/offline_clients/bed_offline_client.dart'; import 'package:helpwave_service/src/api/tasks/offline_clients/patient_offline_client.dart'; import 'package:helpwave_service/src/api/tasks/offline_clients/room_offline_client.dart'; @@ -6,6 +7,7 @@ import 'package:helpwave_service/src/api/tasks/offline_clients/template_offline_ import 'package:helpwave_service/src/api/tasks/offline_clients/ward_offline_client.dart'; import 'package:helpwave_service/src/api/user/offline_clients/organization_offline_client.dart'; import 'package:helpwave_service/src/api/user/offline_clients/user_offline_client.dart'; +import 'package:helpwave_util/lists.dart'; import '../../../user.dart'; const String profileUrl = "https://helpwave.de/favicon.ico"; @@ -65,9 +67,73 @@ final List initialUsers = [ profileUrl: Uri.parse(profileUrl), ), ]; +final List initialWards = initialOrganizations + .map((organization) => range(0, 3).map((index) => + Ward(id: "${organization.id}${index + 1}", name: "Ward ${index + 1}", organizationId: organization.id))) + .expand((element) => element) + .toList(); +final List initialRooms = initialWards + .map((ward) => range(0, 2) + .map((index) => RoomWithWardId(id: "${ward.id}${index + 1}", name: "Room ${index + 1}", wardId: ward.id))) + .expand((element) => element) + .toList(); +final List initialBeds = initialRooms + .map((room) => range(0, 4) + .map((index) => BedWithRoomId(id: "${room.id}${index + 1}", name: "Bed ${index + 1}", roomId: room.id))) + .expand((element) => element) + .toList(); +final List initialPatients = initialBeds.indexed + .map((e) => PatientWithBedId( + id: "patient${e.$1}", + name: "Patient ${e.$1 + 1}", + notes: "", + isDischarged: e.$1 % 6 == 0, + bedId: e.$1 % 2 == 0 ? e.$2.id : null, + )) + .toList(); +final List initialTasks = initialPatients + .map((patient) => range(0, 3).map((index) => Task( + id: "${patient.id}${index + 1}", + name: "Task ${index + 1}", + patientId: patient.id, + notes: '', + ))) + .expand((element) => element) + .toList(); +final List initialTaskSubtasks = initialTasks + .map((task) => range(0, 2).map((index) => Subtask( + id: "${task.id}${index + 1}", + name: "Subtask ${index + 1}", + taskId: task.id, + ))) + .expand((element) => element) + .toList(); +final List initialTaskTemplates = range(0, 5) + .map((index) => TaskTemplate( + id: "template$index", + name: "template${index + 1}", + notes: "", + )) + .toList() + + initialWards + .map((ward) => TaskTemplate( + id: "wardTemplate${ward.id}", + name: "Ward ${ward.name} Template", + notes: "", + wardId: ward.id, + )) + .toList(); +final List initialTaskTemplateSubtasks = initialTasks + .map((task) => range(0, 3).map((index) => Subtask( + id: "${task.id}${index + 1}", + name: "Template Subtask ${index + 1}", + taskId: task.id, + ))) + .expand((element) => element) + .toList(); class OfflineClientStore { - static final OfflineClientStore _instance = OfflineClientStore._internal(); + static final OfflineClientStore _instance = OfflineClientStore._internal()..reset(); OfflineClientStore._internal(); @@ -88,6 +154,13 @@ class OfflineClientStore { void reset() { organizationStore.organizations = initialOrganizations; userStore.users = initialUsers; - + wardStore.wards = initialWards; + roomStore.rooms = initialRooms; + bedStore.beds = initialBeds; + patientStore.patients = initialPatients; + taskStore.tasks = initialTasks; + subtaskStore.subtasks = initialTaskSubtasks; + taskTemplateStore.taskTemplates = initialTaskTemplates; + taskTemplateSubtaskStore.taskTemplateSubtasks = initialTaskTemplateSubtasks; } } diff --git a/packages/helpwave_service/lib/src/api/tasks/controllers/task_controller.dart b/packages/helpwave_service/lib/src/api/tasks/controllers/task_controller.dart index c52081d0..4e26f1e7 100644 --- a/packages/helpwave_service/lib/src/api/tasks/controllers/task_controller.dart +++ b/packages/helpwave_service/lib/src/api/tasks/controllers/task_controller.dart @@ -141,7 +141,7 @@ class TaskController extends ChangeNotifier { /// Only usable when creating Future changePatient(PatientMinimal patient) async { assert(task.isCreating, "Only use TaskController.changePatient, when you create a new task."); - task = task.copyWith(patient); + task = TaskWithPatient.fromTaskAndPatient(task: task.copyWith(patientId: patient.id), patient: patient); notifyListeners(); } @@ -150,7 +150,7 @@ class TaskController extends ChangeNotifier { assert(!task.patient.isCreating, "A the patient must be set to create a task"); state = LoadingState.loading; return await TaskService().createTask(task).then((value) { - task.id = value; + task.copyWith(id: value); state = LoadingState.loaded; return true; }).catchError((error, stackTrace) { diff --git a/packages/helpwave_service/lib/src/api/tasks/offline_clients/bed_offline_client.dart b/packages/helpwave_service/lib/src/api/tasks/offline_clients/bed_offline_client.dart index 11f565e9..cadcb891 100644 --- a/packages/helpwave_service/lib/src/api/tasks/offline_clients/bed_offline_client.dart +++ b/packages/helpwave_service/lib/src/api/tasks/offline_clients/bed_offline_client.dart @@ -55,7 +55,10 @@ class BedOfflineService { void delete(String bedId) { final valueStore = OfflineClientStore().bedStore; valueStore.beds = valueStore.beds.where((value) => value.id != bedId).toList(); - // TODO: Cascade delete to bed-bound templates + final patient = OfflineClientStore().patientStore.findPatientByBed(bedId); + if(patient != null){ + OfflineClientStore().patientStore.unassignBed(patient.id); + } } } diff --git a/packages/helpwave_service/lib/src/api/tasks/offline_clients/patient_offline_client.dart b/packages/helpwave_service/lib/src/api/tasks/offline_clients/patient_offline_client.dart index 22a46384..97e62b46 100644 --- a/packages/helpwave_service/lib/src/api/tasks/offline_clients/patient_offline_client.dart +++ b/packages/helpwave_service/lib/src/api/tasks/offline_clients/patient_offline_client.dart @@ -3,6 +3,7 @@ import 'package:helpwave_proto_dart/services/tasks_svc/v1/patient_svc.pbgrpc.dar import 'package:helpwave_service/src/api/offline/offline_client_store.dart'; import 'package:helpwave_service/src/api/offline/util.dart'; import 'package:helpwave_service/src/api/tasks/data_types/patient.dart'; +import 'package:helpwave_service/src/api/tasks/util/task_status_mapping.dart'; class PatientUpdate { String id; @@ -97,12 +98,15 @@ class PatientOfflineService { void delete(String patientId) { final valueStore = OfflineClientStore().patientStore; valueStore.patients = valueStore.patients.where((value) => value.id != patientId).toList(); - // TODO: Cascade delete to tasks + final tasks = OfflineClientStore().taskStore.findTasks(patientId); + for (var task in tasks) { + OfflineClientStore().taskStore.delete(task.id); + } } } -class PatientServicePromiseClient extends PatientServiceClient { - PatientServicePromiseClient(super.channel); +class PatientOfflineClient extends PatientServiceClient { + PatientOfflineClient(super.channel); @override ResponseFuture getPatient(GetPatientRequest request, {CallOptions? options}) { @@ -145,7 +149,23 @@ class PatientServicePromiseClient extends PatientServiceClient { humanReadableIdentifier: patient.name, notes: patient.notes, isDischarged: patient.isDischarged, - tasks: [], // TODO tasks + tasks: OfflineClientStore().taskStore.findTasks(patient.id).map((task) => GetPatientDetailsResponse_Task( + id: task.id, + name: task.name, + patientId: patient.id, + assignedUserId: task.assigneeId, + description: task.notes, + public: task.isPublicVisible, + status: GRPCTypeConverter.taskStatusToGRPC(task.status), + subtasks: OfflineClientStore() + .subtaskStore + .findSubtasks(task.id) + .map((subtask) => GetPatientDetailsResponse_Task_SubTask( + id: subtask.id, + name: subtask.name, + done: subtask.isDone, + )), + )), ); if (patient.bedId == null) { @@ -188,7 +208,23 @@ class PatientServicePromiseClient extends PatientServiceClient { id: patient.id, notes: patient.notes, humanReadableIdentifier: patient.name, - tasks: [], // TODO get tasks + tasks: OfflineClientStore().taskStore.findTasks(patient.id).map((task) => GetPatientListResponse_Task( + id: task.id, + name: task.name, + patientId: patient.id, + assignedUserId: task.assigneeId, + description: task.notes, + public: task.isPublicVisible, + status: GRPCTypeConverter.taskStatusToGRPC(task.status), + subtasks: OfflineClientStore() + .subtaskStore + .findSubtasks(task.id) + .map((subtask) => GetPatientListResponse_Task_SubTask( + id: subtask.id, + name: subtask.name, + done: subtask.isDone, + )), + )), ); if (patient.bedId == null) { return res; diff --git a/packages/helpwave_service/lib/src/api/tasks/offline_clients/room_offline_client.dart b/packages/helpwave_service/lib/src/api/tasks/offline_clients/room_offline_client.dart index 34405d0e..57ed4799 100644 --- a/packages/helpwave_service/lib/src/api/tasks/offline_clients/room_offline_client.dart +++ b/packages/helpwave_service/lib/src/api/tasks/offline_clients/room_offline_client.dart @@ -60,8 +60,8 @@ class RoomOfflineService { } } -class RoomServicePromiseClient extends RoomServiceClient { - RoomServicePromiseClient(super.channel); +class RoomOfflineClient extends RoomServiceClient { + RoomOfflineClient(super.channel); @override ResponseFuture getRoom(GetRoomRequest request, {CallOptions? options}) { diff --git a/packages/helpwave_service/lib/src/api/tasks/offline_clients/task_offline_client.dart b/packages/helpwave_service/lib/src/api/tasks/offline_clients/task_offline_client.dart index 86fb98ef..27b1fae0 100644 --- a/packages/helpwave_service/lib/src/api/tasks/offline_clients/task_offline_client.dart +++ b/packages/helpwave_service/lib/src/api/tasks/offline_clients/task_offline_client.dart @@ -185,8 +185,8 @@ class SubtaskOfflineService { } } -class TaskServicePromiseClient extends TaskServiceClient { - TaskServicePromiseClient(super.channel); +class TaskOfflineClient extends TaskServiceClient { + TaskOfflineClient(super.channel); @override ResponseFuture getTask(GetTaskRequest request, {CallOptions? options}) { diff --git a/packages/helpwave_service/lib/src/api/tasks/offline_clients/ward_offline_client.dart b/packages/helpwave_service/lib/src/api/tasks/offline_clients/ward_offline_client.dart index b2d5648d..aacb28a3 100644 --- a/packages/helpwave_service/lib/src/api/tasks/offline_clients/ward_offline_client.dart +++ b/packages/helpwave_service/lib/src/api/tasks/offline_clients/ward_offline_client.dart @@ -57,12 +57,15 @@ class WardOfflineService { OfflineClientStore().roomStore.findRooms(wardId).forEach((element) { OfflineClientStore().roomStore.delete(element.id); }); - // TODO delete ward templates + final taskTemplates = OfflineClientStore().taskTemplateStore.findTaskTemplates(wardId); + for (var element in taskTemplates) { + OfflineClientStore().taskTemplateStore.delete(element.id); + } } } -class WardServicePromiseClient extends WardServiceClient { - WardServicePromiseClient(super.channel); +class WardOfflineClient extends WardServiceClient { + WardOfflineClient(super.channel); @override ResponseFuture getWard(GetWardRequest request, {CallOptions? options}) { @@ -104,7 +107,16 @@ class WardServicePromiseClient extends WardServiceClient { id: ward.id, name: ward.name, rooms: rooms, - // TODO taskTemplates: + taskTemplates: OfflineClientStore() + .taskTemplateStore + .findTaskTemplates(ward.id) + .map((template) => GetWardDetailsResponse_TaskTemplate( + id: template.id, + name: template.name, + subtasks: OfflineClientStore().taskTemplateSubtaskStore.findTemplateSubtasks(template.id).map( + (subtask) => GetWardDetailsResponse_Subtask(id: subtask.id, name: subtask.name), + ), + )), ); return MockResponseFuture.value(response); @@ -124,6 +136,70 @@ class WardServicePromiseClient extends WardServiceClient { return MockResponseFuture.value(response); } + @override + ResponseFuture getRecentWards(GetRecentWardsRequest request, {CallOptions? options}) { + final wards = OfflineClientStore().wardStore.findWards(); + final wardsList = wards.map((ward) { + final rooms = OfflineClientStore().roomStore.findRooms(ward.id); + final beds = + rooms.map((room) => OfflineClientStore().bedStore.findBeds(room.id)).expand((element) => element).toList(); + List patients = []; + for (var bed in beds) { + final patient = OfflineClientStore().patientStore.findPatient(bed.id); + if (patient != null) { + patients.add(patient); + } + } + List tasks = patients + .map((patient) => OfflineClientStore().taskStore.findTasks(patient.id)) + .expand((element) => element) + .toList(); + return GetRecentWardsResponse_Ward( + id: ward.id, + name: ward.name, + bedCount: beds.length, + tasksDone: tasks.where((element) => element.status == TaskStatus.done).length, + tasksInProgress: tasks.where((element) => element.status == TaskStatus.inProgress).length, + tasksTodo: tasks.where((element) => element.status == TaskStatus.todo).length, + ); + }); + final response = GetRecentWardsResponse()..wards.addAll(wardsList); + + return MockResponseFuture.value(response); + } + + @override + ResponseFuture getWardOverviews(GetWardOverviewsRequest request, {CallOptions? options}) { + final wards = OfflineClientStore().wardStore.findWards(); + final wardsList = wards.map((ward) { + final rooms = OfflineClientStore().roomStore.findRooms(ward.id); + final beds = + rooms.map((room) => OfflineClientStore().bedStore.findBeds(room.id)).expand((element) => element).toList(); + List patients = []; + for (var bed in beds) { + final patient = OfflineClientStore().patientStore.findPatient(bed.id); + if (patient != null) { + patients.add(patient); + } + } + List tasks = patients + .map((patient) => OfflineClientStore().taskStore.findTasks(patient.id)) + .expand((element) => element) + .toList(); + return GetWardOverviewsResponse_Ward( + id: ward.id, + name: ward.name, + bedCount: beds.length, + tasksDone: tasks.where((element) => element.status == TaskStatus.done).length, + tasksInProgress: tasks.where((element) => element.status == TaskStatus.inProgress).length, + tasksTodo: tasks.where((element) => element.status == TaskStatus.todo).length, + ); + }); + final response = GetWardOverviewsResponse(wards: wardsList); + + return MockResponseFuture.value(response); + } + @override ResponseFuture createWard(CreateWardRequest request, {CallOptions? options}) { final newWard = Ward( diff --git a/packages/helpwave_service/lib/src/api/tasks/services/patient_svc.dart b/packages/helpwave_service/lib/src/api/tasks/services/patient_svc.dart index 39237b04..32856a47 100644 --- a/packages/helpwave_service/lib/src/api/tasks/services/patient_svc.dart +++ b/packages/helpwave_service/lib/src/api/tasks/services/patient_svc.dart @@ -9,7 +9,7 @@ import 'package:helpwave_service/src/api/tasks/util/task_status_mapping.dart'; /// The server is defined in the underlying [TasksAPIServiceClients] class PatientService { /// The GRPC ServiceClient which handles GRPC - PatientServiceClient patientService = TasksAPIServiceClients.patientServiceClient; + PatientServiceClient patientService = TasksAPIServiceClients().patientServiceClient; // TODO consider an enum instead of an string /// Loads the [Patient]s by [Ward] and sorts them by their assignment status diff --git a/packages/helpwave_service/lib/src/api/tasks/services/room_svc.dart b/packages/helpwave_service/lib/src/api/tasks/services/room_svc.dart index 859a9d97..59ae4381 100644 --- a/packages/helpwave_service/lib/src/api/tasks/services/room_svc.dart +++ b/packages/helpwave_service/lib/src/api/tasks/services/room_svc.dart @@ -9,7 +9,7 @@ import 'package:helpwave_service/src/api/tasks/tasks_api_service_clients.dart'; /// The server is defined in the underlying [TasksAPIServiceClients] class RoomService { /// The GRPC ServiceClient which handles GRPC - RoomServiceClient roomService = TasksAPIServiceClients.roomServiceClient; + RoomServiceClient roomService = TasksAPIServiceClients().roomServiceClient; Future> getRoomOverviews({required String wardId}) async { GetRoomOverviewsByWardRequest request = GetRoomOverviewsByWardRequest(id: wardId); diff --git a/packages/helpwave_service/lib/src/api/tasks/services/task_svc.dart b/packages/helpwave_service/lib/src/api/tasks/services/task_svc.dart index 9a4a0e1a..c4f393dd 100644 --- a/packages/helpwave_service/lib/src/api/tasks/services/task_svc.dart +++ b/packages/helpwave_service/lib/src/api/tasks/services/task_svc.dart @@ -10,7 +10,7 @@ import '../util/task_status_mapping.dart'; /// The server is defined in the underlying [TasksAPIServiceClients] class TaskService { /// The GRPC ServiceClient which handles GRPC - TaskServiceClient taskService = TasksAPIServiceClients.taskServiceClient; + TaskServiceClient taskService = TasksAPIServiceClients().taskServiceClient; /// Loads the [Task]s by a [Patient] identifier Future> getTasksByPatient({String? patientId}) async { diff --git a/packages/helpwave_service/lib/src/api/tasks/services/ward_service.dart b/packages/helpwave_service/lib/src/api/tasks/services/ward_service.dart index 60156291..3d28ca97 100644 --- a/packages/helpwave_service/lib/src/api/tasks/services/ward_service.dart +++ b/packages/helpwave_service/lib/src/api/tasks/services/ward_service.dart @@ -9,7 +9,7 @@ import 'package:helpwave_service/src/api/tasks/tasks_api_service_clients.dart'; /// The server is defined in the underlying [TasksAPIServiceClients] class WardService { /// The GRPC ServiceClient which handles GRPC - WardServiceClient wardService = TasksAPIServiceClients.wardServiceClient; + WardServiceClient wardService = TasksAPIServiceClients().wardServiceClient; /// Loads a [WardMinimal] by its identifier Future getWard({required String id}) async { diff --git a/packages/helpwave_service/lib/src/api/tasks/tasks_api_service_clients.dart b/packages/helpwave_service/lib/src/api/tasks/tasks_api_service_clients.dart index 8a578a29..b2e06333 100644 --- a/packages/helpwave_service/lib/src/api/tasks/tasks_api_service_clients.dart +++ b/packages/helpwave_service/lib/src/api/tasks/tasks_api_service_clients.dart @@ -3,18 +3,29 @@ import 'package:helpwave_proto_dart/services/tasks_svc/v1/ward_svc.pbgrpc.dart'; import 'package:helpwave_proto_dart/services/tasks_svc/v1/patient_svc.pbgrpc.dart'; import 'package:helpwave_proto_dart/services/tasks_svc/v1/room_svc.pbgrpc.dart'; import 'package:helpwave_proto_dart/services/tasks_svc/v1/task_svc.pbgrpc.dart'; +import 'package:helpwave_service/src/api/tasks/offline_clients/patient_offline_client.dart'; +import 'package:helpwave_service/src/api/tasks/offline_clients/ward_offline_client.dart'; import 'package:helpwave_service/src/auth/index.dart'; +import 'offline_clients/room_offline_client.dart'; +import 'offline_clients/task_offline_client.dart'; + /// The Underlying GrpcService it provides other clients and the correct metadata for the requests class TasksAPIServiceClients { + TasksAPIServiceClients._privateConstructor(); + + static final TasksAPIServiceClients _instance = TasksAPIServiceClients._privateConstructor(); + + factory TasksAPIServiceClients() => _instance; + /// The api URL used - static String? apiUrl; + String? apiUrl; + + bool offlineMode = false; - static ClientChannel get serviceChannel { - assert(TasksAPIServiceClients.apiUrl != null); - return ClientChannel( - TasksAPIServiceClients.apiUrl!, - ); + ClientChannel get serviceChannel { + assert(apiUrl != null); + return ClientChannel(apiUrl!); } Map getMetaData({String? organizationId}) { @@ -32,11 +43,15 @@ class TasksAPIServiceClients { return metaData; } - static PatientServiceClient get patientServiceClient => PatientServiceClient(serviceChannel); + PatientServiceClient get patientServiceClient => + offlineMode ? PatientOfflineClient(serviceChannel) : PatientServiceClient(serviceChannel); - static WardServiceClient get wardServiceClient => WardServiceClient(serviceChannel); + WardServiceClient get wardServiceClient => + offlineMode ? WardOfflineClient(serviceChannel) : WardServiceClient(serviceChannel); - static RoomServiceClient get roomServiceClient => RoomServiceClient(serviceChannel); + RoomServiceClient get roomServiceClient => + offlineMode ? RoomOfflineClient(serviceChannel) : RoomServiceClient(serviceChannel); - static TaskServiceClient get taskServiceClient => TaskServiceClient(serviceChannel); + TaskServiceClient get taskServiceClient => + offlineMode ? TaskOfflineClient(serviceChannel) : TaskServiceClient(serviceChannel); } diff --git a/packages/helpwave_service/lib/src/api/user/data_types/organization.dart b/packages/helpwave_service/lib/src/api/user/data_types/organization.dart index ca0caf6c..9bc349a6 100644 --- a/packages/helpwave_service/lib/src/api/user/data_types/organization.dart +++ b/packages/helpwave_service/lib/src/api/user/data_types/organization.dart @@ -28,6 +28,7 @@ class Organization extends OrganizationMinimal { }); Organization copyWith({ + String? id, String? shortName, String? longName, String? avatarURL, @@ -36,8 +37,7 @@ class Organization extends OrganizationMinimal { bool? isVerified, }) { return Organization( - id: id, - // `id` is not changeable + id: id ?? this.id, shortName: shortName ?? this.shortName, longName: longName ?? this.longName, avatarURL: avatarURL ?? this.avatarURL, diff --git a/packages/helpwave_service/lib/src/api/user/offline_clients/user_offline_client.dart b/packages/helpwave_service/lib/src/api/user/offline_clients/user_offline_client.dart index 10839d27..d39c57c6 100644 --- a/packages/helpwave_service/lib/src/api/user/offline_clients/user_offline_client.dart +++ b/packages/helpwave_service/lib/src/api/user/offline_clients/user_offline_client.dart @@ -1,4 +1,4 @@ -import 'package:grpc/grpc_web.dart'; +import 'package:grpc/grpc.dart'; import 'package:helpwave_service/src/api/offline/offline_client_store.dart'; import 'package:helpwave_service/src/api/offline/util.dart'; import 'package:helpwave_service/src/api/user/index.dart'; diff --git a/packages/helpwave_service/lib/src/api/user/services/organization_svc.dart b/packages/helpwave_service/lib/src/api/user/services/organization_svc.dart index 0775410f..39c23315 100644 --- a/packages/helpwave_service/lib/src/api/user/services/organization_svc.dart +++ b/packages/helpwave_service/lib/src/api/user/services/organization_svc.dart @@ -10,14 +10,14 @@ import '../data_types/index.dart'; /// The server is defined in the underlying [UserAPIServiceClients] class OrganizationService { /// The GRPC ServiceClient which handles GRPC - OrganizationServiceClient organizationService = UserAPIServiceClients.organizationServiceClient; + OrganizationServiceClient organizationService = UserAPIServiceClients().organizationServiceClient; /// Load a Organization by its identifier Future getOrganization({required String id}) async { GetOrganizationRequest request = GetOrganizationRequest(id: id); GetOrganizationResponse response = await organizationService.getOrganization( request, - options: CallOptions(metadata: UserAPIServiceClients.getMetaData(organizationId: id)), + options: CallOptions(metadata: UserAPIServiceClients().getMetaData(organizationId: id)), ); Organization organization = Organization( @@ -37,7 +37,7 @@ class OrganizationService { GetOrganizationsForUserResponse response = await organizationService.getOrganizationsForUser( request, options: CallOptions( - metadata: UserAPIServiceClients.getMetaData( + metadata: UserAPIServiceClients().getMetaData( organizationId: AuthenticationUtility.fallbackOrganizationId, ), ), @@ -46,7 +46,7 @@ class OrganizationService { List organizations = response.organizations .map((organization) => Organization( id: organization.id, - longName: organization.id, + longName: organization.longName, shortName: organization.shortName, avatarURL: organization.avatarUrl, email: organization.contactEmail, @@ -62,7 +62,7 @@ class OrganizationService { GetMembersByOrganizationResponse response = await organizationService.getMembersByOrganization( request, options: CallOptions( - metadata: UserAPIServiceClients.getMetaData(organizationId: organizationId), + metadata: UserAPIServiceClients().getMetaData(organizationId: organizationId), ), ); diff --git a/packages/helpwave_service/lib/src/api/user/services/user_service.dart b/packages/helpwave_service/lib/src/api/user/services/user_service.dart index 52468816..943bba4a 100644 --- a/packages/helpwave_service/lib/src/api/user/services/user_service.dart +++ b/packages/helpwave_service/lib/src/api/user/services/user_service.dart @@ -10,7 +10,7 @@ import '../data_types/index.dart'; /// The server is defined in the underlying [UserAPIServiceClients] class UserService { /// The GRPC ServiceClient which handles GRPC - UserServiceClient userService = UserAPIServiceClients.userServiceClient; + UserServiceClient userService = UserAPIServiceClients().userServiceClient; /// Loads the [User]s by it's identifier Future getUser({String? id}) async { @@ -18,7 +18,7 @@ class UserService { ReadPublicProfileResponse response = await userService.readPublicProfile( request, options: CallOptions( - metadata: UserAPIServiceClients.getMetaData( + metadata: UserAPIServiceClients().getMetaData( organizationId: AuthenticationUtility.fallbackOrganizationId, ), ), diff --git a/packages/helpwave_service/lib/src/api/user/user_api_service_clients.dart b/packages/helpwave_service/lib/src/api/user/user_api_service_clients.dart index 5e1f8954..e41b01f6 100644 --- a/packages/helpwave_service/lib/src/api/user/user_api_service_clients.dart +++ b/packages/helpwave_service/lib/src/api/user/user_api_service_clients.dart @@ -1,23 +1,31 @@ import 'package:grpc/grpc.dart'; import 'package:helpwave_proto_dart/services/user_svc/v1/user_svc.pbgrpc.dart'; import 'package:helpwave_proto_dart/services/user_svc/v1/organization_svc.pbgrpc.dart'; +import 'package:helpwave_service/src/api/user/offline_clients/organization_offline_client.dart'; +import 'package:helpwave_service/src/api/user/offline_clients/user_offline_client.dart'; import 'package:helpwave_service/src/auth/index.dart'; /// A bundling of all User API services which can be used and are configured /// /// Make sure to set the [apiURL] to use the services class UserAPIServiceClients { + UserAPIServiceClients._privateConstructor(); + + static final UserAPIServiceClients _instance = UserAPIServiceClients._privateConstructor(); + + factory UserAPIServiceClients() => _instance; + /// The api URL used - static String? apiUrl; + String? apiUrl; + + bool offlineMode = false; - static ClientChannel get serviceChannel { - assert(UserAPIServiceClients.apiUrl != null); - return ClientChannel( - UserAPIServiceClients.apiUrl!, - ); + ClientChannel get serviceChannel { + assert(apiUrl != null); + return ClientChannel(apiUrl!); } - static Map getMetaData({String? organizationId}) { + Map getMetaData({String? organizationId}) { var metaData = { ...AuthenticationUtility.authMetaData, "dapr-app-id": "user-svc", @@ -30,7 +38,9 @@ class UserAPIServiceClients { return metaData; } - static UserServiceClient get userServiceClient => UserServiceClient(serviceChannel); + UserServiceClient get userServiceClient => + offlineMode ? UserOfflineClient(serviceChannel) : UserServiceClient(serviceChannel); - static OrganizationServiceClient get organizationServiceClient => OrganizationServiceClient(serviceChannel); + OrganizationServiceClient get organizationServiceClient => + offlineMode ? OrganizationOfflineClient(serviceChannel) : OrganizationServiceClient(serviceChannel); } From 777e8588417fb7780d3345e527e044e03df53f56 Mon Sep 17 00:00:00 2001 From: Felix Thape Date: Mon, 16 Sep 2024 17:44:25 +0200 Subject: [PATCH 05/36] feat: draft for loading change notifier --- .../lib/components/patient_bottom_sheet.dart | 6 +- apps/tasks/lib/components/subtask_list.dart | 6 +- .../lib/components/task_expansion_tile.dart | 4 +- .../my_tasks_screen.dart | 6 +- .../assignee_select_controller.dart | 55 +++-- .../controllers/my_tasks_controller.dart | 56 ++--- .../tasks/controllers/patient_controller.dart | 192 ++++++++---------- .../controllers/subtask_list_controller.dart | 123 +++++------ .../tasks/controllers/task_controller.dart | 70 ++----- .../controllers/ward_patients_controller.dart | 2 +- .../lib/src/api/tasks/data_types/patient.dart | 22 +- .../src/api/tasks/services/patient_svc.dart | 8 +- .../lib/src/api/tasks/services/task_svc.dart | 33 +++ .../api/user/controllers/user_controller.dart | 2 +- packages/helpwave_util/lib/loading.dart | 1 + packages/helpwave_util/lib/loading_state.dart | 1 - .../helpwave_util/lib/src/loading/index.dart | 2 + .../src/loading/loading_change_notifier.dart | 48 +++++ .../src/{loading_state => loading}/type.dart | 0 .../src/loading/loading_and_error_widget.dart | 2 +- .../src/loading/loading_future_builder.dart | 2 +- 21 files changed, 316 insertions(+), 325 deletions(-) create mode 100644 packages/helpwave_util/lib/loading.dart delete mode 100644 packages/helpwave_util/lib/loading_state.dart create mode 100644 packages/helpwave_util/lib/src/loading/index.dart create mode 100644 packages/helpwave_util/lib/src/loading/loading_change_notifier.dart rename packages/helpwave_util/lib/src/{loading_state => loading}/type.dart (100%) diff --git a/apps/tasks/lib/components/patient_bottom_sheet.dart b/apps/tasks/lib/components/patient_bottom_sheet.dart index a987645c..cb119c5c 100644 --- a/apps/tasks/lib/components/patient_bottom_sheet.dart +++ b/apps/tasks/lib/components/patient_bottom_sheet.dart @@ -12,7 +12,7 @@ import 'package:provider/provider.dart'; import 'package:tasks/components/task_bottom_sheet.dart'; import 'package:tasks/components/task_expansion_tile.dart'; import 'package:helpwave_service/tasks.dart'; -import 'package:helpwave_util/loading_state.dart'; +import 'package:helpwave_util/loading.dart'; /// A [BottomSheet] for showing [Patient] information and [Task]s for that [Patient] class PatientBottomSheet extends StatefulWidget { @@ -94,7 +94,7 @@ class _PatientBottomSheetState extends State { width: width * 0.4, // TODO make this state checking easier and more readable child: TextButton( - onPressed: patientController.patient.isUnassigned + onPressed: patientController.patient.isNotAssignedToBed ? null : () { patientController.unassign(); @@ -185,7 +185,7 @@ class _PatientBottomSheetState extends State { if (value == null) { return; } - patientController.changeBed(value.room, value.bed); + patientController.assignToBed(value.room, value.bed); }, ), ); diff --git a/apps/tasks/lib/components/subtask_list.dart b/apps/tasks/lib/components/subtask_list.dart index 01c5d314..f1db6772 100644 --- a/apps/tasks/lib/components/subtask_list.dart +++ b/apps/tasks/lib/components/subtask_list.dart @@ -41,7 +41,7 @@ class SubtaskList extends StatelessWidget { style: const TextStyle(fontWeight: FontWeight.bold, fontSize: 16), ), onAdd: () => subtasksController - .add(Subtask(id: "", name: "Subtask ${subtasksController.subtasks.length + 1}", taskId: taskId)) + .create(Subtask(id: "", name: "Subtask ${subtasksController.subtasks.length + 1}", taskId: taskId)) .then((_) => onChange(subtasksController.subtasks)), itemBuilder: (context, _, subtask) => ListTile( contentPadding: EdgeInsets.zero, @@ -52,7 +52,7 @@ class SubtaskList extends StatelessWidget { ).then((value) { if (value != null) { subtask.name = value; - subtasksController.updateSubtask(subTask: subtask); + subtasksController.update(subtask: subtask); } }), title: Row( @@ -70,7 +70,7 @@ class SubtaskList extends StatelessWidget { borderRadius: BorderRadius.circular(iconSizeSmall), ), onChanged: (isDone) => subtasksController - .updateSubtask(subTask: subtask.copyWith(isDone: isDone)) + .update(subtask: subtask.copyWith(isDone: isDone)) .then((value) => onChange(subtasksController.subtasks)), ), trailing: GestureDetector( diff --git a/apps/tasks/lib/components/task_expansion_tile.dart b/apps/tasks/lib/components/task_expansion_tile.dart index dd03b854..a5c31a61 100644 --- a/apps/tasks/lib/components/task_expansion_tile.dart +++ b/apps/tasks/lib/components/task_expansion_tile.dart @@ -57,8 +57,8 @@ class TaskExpansionTile extends StatelessWidget { context: context, builder: (context) => TaskBottomSheet(task: task, patient: task.patient), ).then((_) { - MyTasksController controller = Provider.of(context, listen: false); - controller.load(); + AssignedTasksController controller = Provider.of(context, listen: false); + controller.loadHandler(); }); }, child: TaskCard( diff --git a/apps/tasks/lib/screens/main_screen_subscreens/my_tasks_screen.dart b/apps/tasks/lib/screens/main_screen_subscreens/my_tasks_screen.dart index 43572a93..54b2ca37 100644 --- a/apps/tasks/lib/screens/main_screen_subscreens/my_tasks_screen.dart +++ b/apps/tasks/lib/screens/main_screen_subscreens/my_tasks_screen.dart @@ -18,9 +18,9 @@ class _MyTasksScreenState extends State { @override Widget build(BuildContext context) { return ChangeNotifierProvider( - create: (_) => MyTasksController(), - child: Consumer( - builder: (BuildContext context, MyTasksController tasksController, Widget? child) { + create: (_) => AssignedTasksController(), + child: Consumer( + builder: (BuildContext context, AssignedTasksController tasksController, Widget? child) { return LoadingAndErrorWidget( state: tasksController.state, child: ListView(children: [ diff --git a/packages/helpwave_service/lib/src/api/tasks/controllers/assignee_select_controller.dart b/packages/helpwave_service/lib/src/api/tasks/controllers/assignee_select_controller.dart index a8fd4fed..99f6886c 100644 --- a/packages/helpwave_service/lib/src/api/tasks/controllers/assignee_select_controller.dart +++ b/packages/helpwave_service/lib/src/api/tasks/controllers/assignee_select_controller.dart @@ -1,22 +1,10 @@ -import 'package:flutter/foundation.dart'; import 'package:helpwave_service/auth.dart'; import 'package:helpwave_service/src/api/user/index.dart'; -import 'package:helpwave_util/loading_state.dart'; -import 'package:logger/logger.dart'; +import 'package:helpwave_util/loading.dart'; import 'package:helpwave_service/tasks.dart'; /// The Controller for selecting a [User] as the assignee of a [Task] -class AssigneeSelectController extends ChangeNotifier { - /// The [LoadingState] of the Controller - LoadingState _state = LoadingState.initializing; - - LoadingState get state => _state; - - set state(LoadingState value) { - _state = value; - notifyListeners(); - } - +class AssigneeSelectController extends LoadingChangeNotifier { /// The selected [User] identifier String? _selected; @@ -27,43 +15,46 @@ class AssigneeSelectController extends ChangeNotifier { /// The currently loaded user List _users = []; - /// The currently loaded tasks + /// The currently loaded users List get users => _users; /// The identifier of the current [Task] it determines whether /// changes are pushed to the server String? _taskId; + /// Whether the object is + bool get isCreating => _taskId == null && _taskId!.isNotEmpty; // TODO remove .isNotEmpty when semantic is changed + AssigneeSelectController({String? selected, String? taskId}) { _selected = selected; _taskId = taskId; load(); } - /// Loads the tasks + /// Loads the users Future load() async { - state = LoadingState.loading; - String? currentOrganization = CurrentWardService().currentWard?.organizationId; - if(currentOrganization == null){ - if(kDebugMode){ - Logger().w("Organization Id not set in CurrentWardService" - " while trying to load in AssigneeSelectController"); + loadUsersFuture() async { + String? currentOrganization = CurrentWardService().currentWard?.organizationId; + if (currentOrganization == null) { + // TODO throw a better error here + throw "Organization Id not set in CurrentWardService while trying to load in AssigneeSelectController"; } - state = LoadingState.error; - return; + _users = await OrganizationService().getMembersByOrganization(currentOrganization); } - _users = await OrganizationService().getMembersByOrganization(currentOrganization); - state = LoadingState.loaded; + await loadHandler(future: loadUsersFuture()); } /// Change the assignee - Future changeAssignee(String id) async{ - if(_taskId != null && _taskId!.isNotEmpty){ - state = LoadingState.loading; - await TaskService().assignToUser(taskId: _taskId!, userId: id); + Future changeAssignee(String id) async { + changeAssigneeFuture() async { + if (!isCreating) { + await TaskService().assignToUser(taskId: _taskId!, userId: id).then((value) => _selected = id); + } else { + _selected = id; + } } - _selected = id; - state = LoadingState.loaded; + + await loadHandler(future: changeAssigneeFuture()); } } diff --git a/packages/helpwave_service/lib/src/api/tasks/controllers/my_tasks_controller.dart b/packages/helpwave_service/lib/src/api/tasks/controllers/my_tasks_controller.dart index 05c0b4e5..3953085f 100644 --- a/packages/helpwave_service/lib/src/api/tasks/controllers/my_tasks_controller.dart +++ b/packages/helpwave_service/lib/src/api/tasks/controllers/my_tasks_controller.dart @@ -1,61 +1,33 @@ -import 'package:flutter/cupertino.dart'; import 'package:helpwave_service/tasks.dart'; -import 'package:helpwave_util/loading_state.dart'; +import 'package:helpwave_util/loading.dart'; /// The Controller for [Task]s of the current [User] -class MyTasksController extends ChangeNotifier { - /// The [LoadingState] of the Controller - LoadingState state = LoadingState.initializing; - - /// The currently loaded tasks +class AssignedTasksController extends LoadingChangeNotifier { + /// The currently loaded [Task]s List _tasks = []; - /// The currently loaded tasks + /// The currently loaded [Task]s List get tasks => _tasks; - /// The loaded tasks which have [TaskStatus.todo] + /// The loaded [Task]s which have [TaskStatus.todo] List get todo => _tasks.where((element) => element.status == TaskStatus.todo).toList(); - /// The loaded tasks which have [TaskStatus.inProgress] + /// The loaded [Task]s which have [TaskStatus.inProgress] List get inProgress => _tasks.where((element) => element.status == TaskStatus.inProgress).toList(); - /// The loaded tasks which have [TaskStatus.done] + /// The loaded [Task]s which have [TaskStatus.done] List get done => _tasks.where((element) => element.status == TaskStatus.done).toList(); - MyTasksController() { - load(); + AssignedTasksController() { + loadTasks(); } - /// Loads the tasks - Future load() async { - if (state == LoadingState.initializing) { - state = LoadingState.loading; - notifyListeners(); - } - - var patients = await PatientService().getPatientList(); - ListallPatients = patients.all; - - _tasks = []; - // TODO use the already given information by the later updated getPatientList - for(Patient patient in allPatients) { - List tasks = await TaskService().getTasksByPatient(patientId: patient.id); - for(var task in tasks) { - _tasks.add(TaskWithPatient( - id: task.id, - name: task.name, - assigneeId: task.assigneeId, - notes: task.notes, - dueDate: task.dueDate, - status: task.status, - subtasks: task.subtasks, - patientId: patient.id, - patient: patient, - )); - } + /// Loads the [Task]s + Future loadTasks() async { + loadTasksFuture() async { + _tasks = await TaskService().getAssignedTasks(); } - state = LoadingState.loaded; - notifyListeners(); + loadHandler(future: loadTasksFuture()); } } diff --git a/packages/helpwave_service/lib/src/api/tasks/controllers/patient_controller.dart b/packages/helpwave_service/lib/src/api/tasks/controllers/patient_controller.dart index 95daca37..9bfa732e 100644 --- a/packages/helpwave_service/lib/src/api/tasks/controllers/patient_controller.dart +++ b/packages/helpwave_service/lib/src/api/tasks/controllers/patient_controller.dart @@ -1,151 +1,129 @@ -import 'package:flutter/material.dart'; -import 'package:helpwave_util/loading_state.dart'; +import 'package:helpwave_util/loading.dart'; import 'package:helpwave_service/src/api/tasks/index.dart'; +import 'package:logger/logger.dart'; /// The Controller for managing [Patient]s in a Ward -class PatientController extends ChangeNotifier { - /// The [LoadingState] of the Controller - LoadingState _state = LoadingState.initializing; - - /// The current [Patient], may or may not be loaded depending on the [_state] +class PatientController extends LoadingChangeNotifier { + /// The current [Patient] Patient _patient; - /// The error message to show should only be used when [state] == [LoadingState.error] - String errorMessage = ""; - - PatientController(this._patient) { - if (!_patient.isCreating) { - load(); - } - } - - LoadingState get state => _state; - - set state(LoadingState value) { - _state = value; - notifyListeners(); - } - + /// The current [Patient] Patient get patient => _patient; set patient(Patient value) { _patient = value; - _state = LoadingState.loaded; - notifyListeners(); + changeState(LoadingState.loaded); } - /// Saves whether we are currently creating of a patient or already have them + /// Is the current [Patient] already saved on the server or are we creating? get isCreating => _patient.isCreating; - // Used to trigger the notify call without having to copy or save the patient locally - updatePatient(void Function(Patient patient) updateTransform, void Function(Patient patient) revertTransform) { - updateTransform(patient); - if (!isCreating) { - PatientService().updatePatient(patient).then((value) { - if (value) { - state = LoadingState.loaded; - } else { - throw Error(); - } - }).catchError((error, stackTrace) { - revertTransform(patient); - errorMessage = error.toString(); - state = LoadingState.error; - }); - } else { - state = LoadingState.loaded; + PatientController(this._patient) { + if (!_patient.isCreating) { + load(); } } /// A function to load the [Patient] Future load() async { - state = LoadingState.loading; - await PatientService().getPatientDetails(patientId: patient.id).then((value) { - patient = value; - state = LoadingState.loaded; - }).catchError((error, stackTrace) { - errorMessage = error.toString(); - state = LoadingState.error; - }); + if (isCreating) { + Logger().w("PatientController.load should not be called when the patient has not been created"); + return; + } + loadPatient() async { + patient = await PatientService().getPatientDetails(patientId: patient.id); + } + + loadHandler( + future: loadPatient(), + ); } - /// Unassigns the patient the [patients] + /// Unassigns the [Patient] from their [Bed] Future unassign() async { - state = LoadingState.loading; - notifyListeners(); - await PatientService().unassignPatient(patientId: patient.id); - // Here we can maybe use optimistic updates - load(); + unassignPatient() async { + await PatientService().unassignPatient(patientId: patient.id).then((value) { + final patientCopy = patient.copyWith(); + patientCopy.bed = null; + patientCopy.room = null; + patient = patientCopy; + }); + } + + loadHandler(future: unassignPatient()); } - /// Discharges the patient the [patients] + /// Discharges the [Patient] Future discharge() async { - state = LoadingState.loading; - notifyListeners(); - await PatientService().dischargePatient(patientId: patient.id); - // Here we can maybe use optimistic updates - load(); + dischargePatient() async { + await PatientService().dischargePatient(patientId: patient.id).then((value) { + final patientCopy = patient.copyWith(isDischarged: true); + patientCopy.bed = null; + patientCopy.room = null; + patient = patientCopy; + }); + } + + loadHandler(future: dischargePatient()); } - /// Assigns the patient to a bed - Future changeBed(RoomMinimal room, BedMinimal bed) async { + /// Assigns the [Patient] to a [Bed] and [Room] + Future assignToBed(RoomMinimal room, BedMinimal bed) async { if (isCreating) { patient.room = room; patient.bed = bed; - state = LoadingState.loaded; return; } - state = LoadingState.loading; - await PatientService().assignBed(patientId: patient.id, bedId: bed.id).then((value) { - patient.room = room; - patient.bed = bed; - state = LoadingState.loaded; - }).catchError((error, stackTrace) { - errorMessage = error.toString(); - state = LoadingState.error; - }); + assignPatientToBed() async { + await PatientService().assignBed(patientId: patient.id, bedId: bed.id).then((value) { + patient = patient.copyWith(bed: bed, room: room); + }); + } + + loadHandler(future: assignPatientToBed()); } - /// Change the name of the [patient] + /// Change the name of the [Patient] Future changeName(String name) async { - String oldName = name; - updatePatient( - (patient) { + if(isCreating){ + patient.name = name; + return; + } + updateName() async { + await PatientService().updatePatient(id: patient.id, name: name).then((_) { patient.name = name; - }, - (patient) { - patient.name = oldName; - }, - ); + }); + } + + loadHandler(future: updateName()); } - /// Change the notes of the [patient] + /// Change the notes of the [Patient] Future changeNotes(String notes) async { - String oldNotes = notes; - updatePatient( - (patient) { + if(isCreating){ + patient.notes = notes; + return; + } + updateNotes() async { + await PatientService().updatePatient(id: patient.id, notes: notes).then((_) { patient.notes = notes; - }, - (patient) { - patient.notes = oldNotes; - }, - ); + }); + } + + loadHandler(future: updateNotes()); } - /// Creates the patient and returns - Future create() async { - state = LoadingState.loading; - return await PatientService().createPatient(patient).then((value) async { - patient.id = value; - if (!patient.isUnassigned) { - await PatientService().assignBed(patientId: patient.id, bedId: patient.bed!.id); + /// Creates the [Patient] + Future create() async { + createPatient() async { + await PatientService().createPatient(patient).then((id) { + patient = patient.copyWith(id: id); + }); + if (!patient.isNotAssignedToBed) { + await assignToBed(patient.room!, patient.bed!); } - state = LoadingState.loaded; - return true; - }).catchError((error, stackTrace) { - errorMessage = error.toString(); - state = LoadingState.error; - return false; - }); + } + + loadHandler(future: createPatient()); } } diff --git a/packages/helpwave_service/lib/src/api/tasks/controllers/subtask_list_controller.dart b/packages/helpwave_service/lib/src/api/tasks/controllers/subtask_list_controller.dart index f11402f2..a0b8e98c 100644 --- a/packages/helpwave_service/lib/src/api/tasks/controllers/subtask_list_controller.dart +++ b/packages/helpwave_service/lib/src/api/tasks/controllers/subtask_list_controller.dart @@ -1,24 +1,13 @@ import 'dart:async'; -import 'package:flutter/cupertino.dart'; import 'package:helpwave_service/src/api/tasks/index.dart'; -import 'package:helpwave_util/loading_state.dart'; +import 'package:helpwave_util/loading.dart'; /// The Controller for managing [Subtask]s in a [Task] /// /// Providing a [taskId] means loading and synchronising the [Subtask]s with /// the backend while no [taskId] or a empty [String] means that the subtasks /// only used locally -class SubtasksController extends ChangeNotifier { - /// The [LoadingState] of the Controller - LoadingState _state = LoadingState.initializing; - - LoadingState get state => _state; - - set state(LoadingState value) { - _state = state; - notifyListeners(); - } - +class SubtasksController extends LoadingChangeNotifier { /// The [Subtask]s List _subtasks = []; @@ -29,41 +18,30 @@ class SubtasksController extends ChangeNotifier { notifyListeners(); } - bool _isCreating = false; - - bool get isCreating => _isCreating; - - String taskId; + bool get isCreating => taskId == null || taskId!.isEmpty; - /// Only valid in case [state] == [LoadingState.error] - String errorMessage = ""; + String? taskId; SubtasksController({this.taskId = "", List? subtasks}) { - _isCreating = taskId == ""; if (!isCreating) { load(); - } else { - state = LoadingState.unspecified; } } + /// Loads a [Task] Future load() async { - state = LoadingState.loading; - if (!isCreating) { - await TaskService().getTask(id: taskId).then((task) { - subtasks = task.subtasks; - state = LoadingState.loaded; - }).onError( - (error, stackTrace) { - state = LoadingState.error; - }, - ); + if (isCreating) { return; } - state = LoadingState.loaded; + loadTask() async { + final task = await TaskService().getTask(id: taskId); + subtasks = task.subtasks; + } + + loadHandler(future: loadTask()); } - /// Delete the subtask by the index.dart + /// Delete the [Subtask] by its index int the list Future deleteByIndex(int index) async { if (index < 0 || index >= subtasks.length) { return; @@ -73,55 +51,60 @@ class SubtasksController extends ChangeNotifier { notifyListeners(); return; } - state = LoadingState.loading; - await TaskService().deleteSubTask(subtaskId: subtasks[index].id, taskId: taskId).then((value) { - if (value) { - _subtasks.removeAt(index); - state = LoadingState.loaded; - } - }).catchError((error, stackTrace) { - errorMessage = error.toString(); - state = LoadingState.error; - return null; - }); + await deleteById(subtasks[index].id); } /// Delete the [Subtask] by the id - Future delete(String id) async { - assert(!isCreating, "delete should not be used when creating a completely new SubTask list"); - int index = _subtasks.indexWhere((element) => element.id == id); - if (index != -1) { - deleteByIndex(index); + Future deleteById(String id) async { + assert(!isCreating, "deleteById should not be used when creating a completely new Subtask list"); + deleteSubtask() async { + await TaskService().deleteSubTask(subtaskId: id, taskId: taskId!).then((value) { + if (value) { + int index = _subtasks.indexWhere((element) => element.id == id); + if (index != -1) { + _subtasks.removeAt(index); + } + } + }); } + + loadHandler(future: deleteSubtask()); } /// Add the [Subtask] - Future add(Subtask subTask) async { + Future create(Subtask subTask) async { if (isCreating) { _subtasks.add(subTask); notifyListeners(); return; } - await TaskService().createSubTask(taskId: taskId, subTask: subTask).then((value) { - _subtasks.add(value); - state = LoadingState.loaded; - }).catchError((error, stackTrace) { - errorMessage = error.toString(); - state = LoadingState.error; - return null; - }); + createSubtask() async { + await TaskService().createSubTask(taskId: taskId!, subTask: subTask).then((value) { + _subtasks.add(value); + }); + } + + loadHandler(future: createSubtask()); } - Future updateSubtask({required Subtask subTask}) async { - if (!subTask.isCreating) { - state = LoadingState.loading; - await TaskService().updateSubtask(subtask: subTask, taskId: taskId).then((value) { - state = LoadingState.loaded; - }).catchError((error, stackTrace) { - // Just reload in case of an error - load(); - }); + Future update({required Subtask subtask, int? index}) async { + if (isCreating) { + assert( + index != null && index >= 0 && index < subtasks.length, + "When creating a subtask list and updating a subtask, a index for the subtask must be provided", + ); + subtasks[index!] = subtask; + return; } - notifyListeners(); + updateSubtask() async { + assert(!subtask.isCreating, "To update a subtask on the server the subtask must have an id"); + await TaskService().updateSubtask(subtask: subtask, taskId: taskId!); + int index = subtasks.indexWhere((element) => element.id == subtask.id); + if (index != -1) { + subtasks[index] = subtask; + } + } + + loadHandler(future: updateSubtask()); } } diff --git a/packages/helpwave_service/lib/src/api/tasks/controllers/task_controller.dart b/packages/helpwave_service/lib/src/api/tasks/controllers/task_controller.dart index 4e26f1e7..80061bb8 100644 --- a/packages/helpwave_service/lib/src/api/tasks/controllers/task_controller.dart +++ b/packages/helpwave_service/lib/src/api/tasks/controllers/task_controller.dart @@ -1,81 +1,45 @@ -import 'package:flutter/material.dart'; -import 'package:helpwave_util/loading_state.dart'; +import 'package:helpwave_util/loading.dart'; import 'package:helpwave_service/src/api/tasks/index.dart'; /// The Controller for managing a [TaskWithPatient] -class TaskController extends ChangeNotifier { - /// The [LoadingState] of the Controller - LoadingState _state = LoadingState.initializing; - - /// The current [Task], may or may not be loaded depending on the [_state] +class TaskController extends LoadingChangeNotifier { + /// The current [Task] TaskWithPatient _task; - /// The error message to show should only be used when [state] == [LoadingState.error] - String errorMessage = ""; - TaskController(this._task) { if (!_task.isCreating) { load(); - } else { - state = LoadingState.unspecified; } } - LoadingState get state => _state; - - set state(LoadingState value) { - _state = value; - notifyListeners(); - } - TaskWithPatient get task => _task; + set task(TaskWithPatient value) { _task = value; - _state = LoadingState.loaded; notifyListeners(); } - /// Used to trigger the notify call without having to copy or save the Task locally - updateTask(void Function(TaskWithPatient task) updateTransform, void Function(TaskWithPatient task) revertTransform) { - updateTransform(task); - if (!isCreating) { - TaskService().updateTask(task).then((value) { - if (value) { - state = LoadingState.loaded; - } else { - throw Error(); - } - }).catchError((error, stackTrace) { - revertTransform(task); - errorMessage = error.toString(); - state = LoadingState.error; - }); - } else { - state = LoadingState.loaded; - } - } + PatientMinimal get patient => task.patient; bool get isCreating => _task.isCreating; - // only create when a patient is assigned + /// Whether the [Task] object can be used to create a [Task] + /// + /// Create is only possible when a [Patient] is assigned to the [Task] bool get isReadyForCreate => !task.patient.isCreating; - PatientMinimal get patient => task.patient; - /// A function to load the [Task] load() async { - state = LoadingState.loading; - await TaskService().getTask(id: task.id).then((value) { - task = value; - state = LoadingState.loaded; - }).catchError((error, stackTrace) { - errorMessage = error.toString(); - state = LoadingState.error; - }); + loadTask() async { + await TaskService().getTask(id: task.id).then((value) { + task = value; + }); + } + loadHandler(future: loadTask()); } - /// Changes the Assignee + /// Changes the assigned [User] /// /// Without a backend request as we expect this to be done in the [AssigneeSelectController] Future changeAssignee(String assigneeId) async { @@ -140,14 +104,14 @@ class TaskController extends ChangeNotifier { /// Only usable when creating Future changePatient(PatientMinimal patient) async { - assert(task.isCreating, "Only use TaskController.changePatient, when you create a new task."); + assert(isCreating, "Only use TaskController.changePatient, when you create a new task."); task = TaskWithPatient.fromTaskAndPatient(task: task.copyWith(patientId: patient.id), patient: patient); notifyListeners(); } /// Creates the Task and returns Future create() async { - assert(!task.patient.isCreating, "A the patient must be set to create a task"); + assert(!isReadyForCreate, "A the patient must be set to create a task"); state = LoadingState.loading; return await TaskService().createTask(task).then((value) { task.copyWith(id: value); diff --git a/packages/helpwave_service/lib/src/api/tasks/controllers/ward_patients_controller.dart b/packages/helpwave_service/lib/src/api/tasks/controllers/ward_patients_controller.dart index e18d4d01..c93817d7 100644 --- a/packages/helpwave_service/lib/src/api/tasks/controllers/ward_patients_controller.dart +++ b/packages/helpwave_service/lib/src/api/tasks/controllers/ward_patients_controller.dart @@ -1,6 +1,6 @@ import 'package:flutter/cupertino.dart'; import 'package:helpwave_service/src/api/tasks/index.dart'; -import 'package:helpwave_util/loading_state.dart'; +import 'package:helpwave_util/loading.dart'; import 'package:helpwave_util/search.dart'; /// The Controller for managing [Patient]s in a Ward diff --git a/packages/helpwave_service/lib/src/api/tasks/data_types/patient.dart b/packages/helpwave_service/lib/src/api/tasks/data_types/patient.dart index 26b0ce61..db56424f 100644 --- a/packages/helpwave_service/lib/src/api/tasks/data_types/patient.dart +++ b/packages/helpwave_service/lib/src/api/tasks/data_types/patient.dart @@ -80,7 +80,7 @@ class Patient extends PatientMinimal { List tasks; bool isDischarged; - get isUnassigned => bed == null && room == null; + get isNotAssignedToBed => bed == null && room == null; get isActive => bed != null && room != null; @@ -109,6 +109,26 @@ class Patient extends PatientMinimal { this.room, this.bed, }); + + Patient copyWith({ + String? id, + String? name, + List? tasks, + String? notes, + bool? isDischarged, + RoomMinimal? room, + BedMinimal? bed, + }) { + return Patient( + id: id ?? this.id, + name: name ?? this.name, + tasks: tasks ?? this.tasks, + notes: notes ?? this.notes, + isDischarged: isDischarged ?? this.isDischarged, + room: room ?? this.room, + bed: bed ?? this.bed, + ); + } } /// A data class which maps all [PatientAssignmentStatus]es to a [List] of [Patient]s diff --git a/packages/helpwave_service/lib/src/api/tasks/services/patient_svc.dart b/packages/helpwave_service/lib/src/api/tasks/services/patient_svc.dart index 32856a47..a6898183 100644 --- a/packages/helpwave_service/lib/src/api/tasks/services/patient_svc.dart +++ b/packages/helpwave_service/lib/src/api/tasks/services/patient_svc.dart @@ -161,11 +161,11 @@ class PatientService { } /// Update a [Patient] - Future updatePatient(Patient patient) async { + Future updatePatient({required String id, String? notes, String? name}) async { UpdatePatientRequest request = UpdatePatientRequest( - id: patient.id, - notes: patient.notes, - humanReadableIdentifier: patient.name, + id: id, + notes: notes, + humanReadableIdentifier: name, ); UpdatePatientResponse response = await patientService.updatePatient( request, diff --git a/packages/helpwave_service/lib/src/api/tasks/services/task_svc.dart b/packages/helpwave_service/lib/src/api/tasks/services/task_svc.dart index c4f393dd..2de819e2 100644 --- a/packages/helpwave_service/lib/src/api/tasks/services/task_svc.dart +++ b/packages/helpwave_service/lib/src/api/tasks/services/task_svc.dart @@ -74,6 +74,39 @@ class TaskService { ); } + /// Loads the [Task]s assigned to the current [User] + Future> getAssignedTasks({String? id}) async { + GetAssignedTasksRequest request = GetAssignedTasksRequest(); + GetAssignedTasksResponse response = await taskService.getAssignedTasks( + request, + options: CallOptions(metadata: TasksAPIServiceClients().getMetaData()), + ); + + return response.tasks + .map((task) => TaskWithPatient( + id: task.id, + name: task.name, + notes: task.description, + isPublicVisible: task.public, + status: GRPCTypeConverter.taskStatusFromGRPC(task.status), + assigneeId: task.assignedUserId, + dueDate: task.dueAt.toDateTime(), + patient: PatientMinimal(id: task.patient.id, name: task.patient.humanReadableIdentifier), + subtasks: task.subtasks + .map((subtask) => Subtask( + id: subtask.id, + taskId: task.id, + name: subtask.name, + isDone: subtask.done, + )) + .toList(), + patientId: task.patient.id, + createdBy: task.createdBy, + creationDate: task.createdAt.toDateTime(), + )) + .toList(); + } + Future createTask(TaskWithPatient task) async { CreateTaskRequest request = CreateTaskRequest( name: task.name, diff --git a/packages/helpwave_service/lib/src/api/user/controllers/user_controller.dart b/packages/helpwave_service/lib/src/api/user/controllers/user_controller.dart index dce15479..6d975aa2 100644 --- a/packages/helpwave_service/lib/src/api/user/controllers/user_controller.dart +++ b/packages/helpwave_service/lib/src/api/user/controllers/user_controller.dart @@ -1,5 +1,5 @@ import 'package:flutter/material.dart'; -import 'package:helpwave_util/loading_state.dart'; +import 'package:helpwave_util/loading.dart'; import '../index.dart'; /// The Controller for managing a [User] diff --git a/packages/helpwave_util/lib/loading.dart b/packages/helpwave_util/lib/loading.dart new file mode 100644 index 00000000..938db0bd --- /dev/null +++ b/packages/helpwave_util/lib/loading.dart @@ -0,0 +1 @@ +export 'package:helpwave_util/src/loading/index.dart'; diff --git a/packages/helpwave_util/lib/loading_state.dart b/packages/helpwave_util/lib/loading_state.dart deleted file mode 100644 index b47bbe00..00000000 --- a/packages/helpwave_util/lib/loading_state.dart +++ /dev/null @@ -1 +0,0 @@ -export 'package:helpwave_util/src/loading_state/type.dart'; diff --git a/packages/helpwave_util/lib/src/loading/index.dart b/packages/helpwave_util/lib/src/loading/index.dart new file mode 100644 index 00000000..e47463c6 --- /dev/null +++ b/packages/helpwave_util/lib/src/loading/index.dart @@ -0,0 +1,2 @@ +export 'type.dart'; +export 'loading_change_notifier.dart'; diff --git a/packages/helpwave_util/lib/src/loading/loading_change_notifier.dart b/packages/helpwave_util/lib/src/loading/loading_change_notifier.dart new file mode 100644 index 00000000..53dfa8c3 --- /dev/null +++ b/packages/helpwave_util/lib/src/loading/loading_change_notifier.dart @@ -0,0 +1,48 @@ +import 'package:flutter/cupertino.dart'; +import '../../loading.dart'; + +/// A [ChangeNotifier] that manages a [LoadingState] to indicate to components what state they should show +class LoadingChangeNotifier extends ChangeNotifier { + /// The [LoadingState] of the Controller + LoadingState _state = LoadingState.initializing; + + /// The [LoadingState] of the Controller + LoadingState get state => _state; + + /// The Error, when the Controller is [LoadingState.error] + Object? error; + + /// Function + changeState(LoadingState value) { + _state = value; + notifyListeners(); + } + + LoadingChangeNotifier(); + + Future loadHandler({ + required Future future, + Future Function(Object? error, StackTrace stackTrace)? errorHandler, + }) async { + defaultErrorHandler(errorObj, _) async { + error = errorObj.toString(); + return false; + } + + changeState(LoadingState.loading); + await future.then((_) => changeState(LoadingState.loaded)).onError((error, stackTrace) async { + if (errorHandler != null) { + try { + bool isHandled = await errorHandler(error, stackTrace); + if (isHandled) { + return; + } + } catch (_) {} + } else { + defaultErrorHandler(error, stackTrace); + } + + changeState(LoadingState.error); + }); + } +} diff --git a/packages/helpwave_util/lib/src/loading_state/type.dart b/packages/helpwave_util/lib/src/loading/type.dart similarity index 100% rename from packages/helpwave_util/lib/src/loading_state/type.dart rename to packages/helpwave_util/lib/src/loading/type.dart diff --git a/packages/helpwave_widget/lib/src/loading/loading_and_error_widget.dart b/packages/helpwave_widget/lib/src/loading/loading_and_error_widget.dart index d9b26fc7..1f64ddf1 100644 --- a/packages/helpwave_widget/lib/src/loading/loading_and_error_widget.dart +++ b/packages/helpwave_widget/lib/src/loading/loading_and_error_widget.dart @@ -1,6 +1,6 @@ import 'package:flutter/cupertino.dart'; import 'package:helpwave_theme/constants.dart'; -import 'package:helpwave_util/loading_state.dart'; +import 'package:helpwave_util/loading.dart'; import 'package:helpwave_widget/loading.dart'; /// A [Widget] to show different [Widget]s depending on the [LoadingState] diff --git a/packages/helpwave_widget/lib/src/loading/loading_future_builder.dart b/packages/helpwave_widget/lib/src/loading/loading_future_builder.dart index ccfb938e..08b1625b 100644 --- a/packages/helpwave_widget/lib/src/loading/loading_future_builder.dart +++ b/packages/helpwave_widget/lib/src/loading/loading_future_builder.dart @@ -1,5 +1,5 @@ import 'package:flutter/cupertino.dart'; -import 'package:helpwave_util/loading_state.dart'; +import 'package:helpwave_util/loading.dart'; import 'package:helpwave_widget/loading.dart'; /// A Wrapper for the standard [FutureBuilder] to easily distinguish the three From a0759182caac9b1b32ef697a107bf804f7a80a76 Mon Sep 17 00:00:00 2001 From: Felix Thape Date: Tue, 17 Sep 2024 14:40:18 +0200 Subject: [PATCH 06/36] feat: update all controllers --- .../tasks/lib/components/assignee_select.dart | 53 ++++--- .../lib/components/patient_bottom_sheet.dart | 2 +- .../lib/components/task_bottom_sheet.dart | 138 ++++++++--------- .../lib/components/task_expansion_tile.dart | 2 +- .../lib/components/user_bottom_sheet.dart | 2 +- .../helpwave_localization/lib/l10n/app_de.arb | 3 +- .../helpwave_localization/lib/l10n/app_en.arb | 3 +- .../src/api/offline/offline_client_store.dart | 9 +- .../assignee_select_controller.dart | 60 -------- .../lib/src/api/tasks/controllers/index.dart | 1 - .../controllers/my_tasks_controller.dart | 4 +- .../tasks/controllers/task_controller.dart | 145 ++++++++++-------- .../controllers/ward_patients_controller.dart | 30 ++-- .../lib/src/api/tasks/data_types/patient.dart | 3 +- .../src/api/tasks/services/patient_svc.dart | 1 - .../lib/src/api/tasks/services/task_svc.dart | 55 +++++-- .../src/loading/loading_change_notifier.dart | 9 +- .../lib/content_selection.dart | 4 +- .../lib/src/content_selection/index.dart | 4 + .../src/content_selection/list_select.dart | 28 ++++ .../src/loading/loading_future_builder.dart | 54 ++++--- 21 files changed, 308 insertions(+), 302 deletions(-) delete mode 100644 packages/helpwave_service/lib/src/api/tasks/controllers/assignee_select_controller.dart create mode 100644 packages/helpwave_widget/lib/src/content_selection/index.dart create mode 100644 packages/helpwave_widget/lib/src/content_selection/list_select.dart diff --git a/apps/tasks/lib/components/assignee_select.dart b/apps/tasks/lib/components/assignee_select.dart index 746dc1d0..02e4794f 100644 --- a/apps/tasks/lib/components/assignee_select.dart +++ b/apps/tasks/lib/components/assignee_select.dart @@ -1,18 +1,23 @@ +import 'dart:async'; import 'package:flutter/material.dart'; import 'package:helpwave_localization/localization.dart'; -import 'package:helpwave_service/tasks.dart'; import 'package:helpwave_service/user.dart'; import 'package:helpwave_theme/constants.dart'; import 'package:helpwave_widget/bottom_sheets.dart'; -import 'package:helpwave_widget/loading.dart'; -import 'package:provider/provider.dart'; +import 'package:helpwave_widget/content_selection.dart'; /// A [BottomSheet] for selecting a assignee -class AssigneeSelect extends StatelessWidget { +class AssigneeSelectBottomSheet extends StatelessWidget { /// The callback when the assignee should be changed - final Function(User assignee) onChanged; + /// + /// Null if the assignee should be removed + final Function(User? assignee) onChanged; - const AssigneeSelect({super.key, required this.onChanged}); + final FutureOr> users; + + final String? selectedId; + + const AssigneeSelectBottomSheet({super.key, required this.onChanged, required this.users, this.selectedId}); @override Widget build(BuildContext context) { @@ -21,24 +26,26 @@ class AssigneeSelect extends StatelessWidget { onClosing: () => {}, builder: (context) => Padding( padding: const EdgeInsets.symmetric(vertical: paddingMedium), - child: Consumer(builder: (context, assigneeSelectController, __) { - return LoadingAndErrorWidget( - state: assigneeSelectController.state, - child: Column( - children: assigneeSelectController.users.map((user) => - ListTile( - onTap: () { - assigneeSelectController.changeAssignee(user.id).then((value) { - onChanged(user); - }); - }, - leading: CircleAvatar( - foregroundColor: Colors.blue, backgroundImage: NetworkImage(user.profileUrl.toString())), - title: Text(user.nickName), - )).toList(), + child: Column( + children: [ + TextButton( + child: Text(context.localization!.remove), + onPressed: () => onChanged(null), + ), + const SizedBox(height: 10), + ListSelect( + items: users, + onSelect: onChanged, + builder: (context, user, select) => ListTile( + onTap: select, + leading: CircleAvatar( + foregroundColor: Colors.blue, backgroundImage: NetworkImage(user.profileUrl.toString())), + title: Text(user.nickName, + style: TextStyle(decoration: user.id == selectedId ? TextDecoration.underline : null)), + ), ), - ); - }), + ], + ), ), ); } diff --git a/apps/tasks/lib/components/patient_bottom_sheet.dart b/apps/tasks/lib/components/patient_bottom_sheet.dart index cb119c5c..431773a4 100644 --- a/apps/tasks/lib/components/patient_bottom_sheet.dart +++ b/apps/tasks/lib/components/patient_bottom_sheet.dart @@ -151,7 +151,7 @@ class _PatientBottomSheetState extends State { Center( child: Consumer(builder: (context, patientController, _) { return LoadingFutureBuilder( - future: loadRoomsWithBeds(patientController.patient.id), + data: loadRoomsWithBeds(patientController.patient.id), // TODO use a better loading widget loadingWidget: const SizedBox(), thenWidgetBuilder: (context, beds) { diff --git a/apps/tasks/lib/components/task_bottom_sheet.dart b/apps/tasks/lib/components/task_bottom_sheet.dart index 44e62720..7f2590d4 100644 --- a/apps/tasks/lib/components/task_bottom_sheet.dart +++ b/apps/tasks/lib/components/task_bottom_sheet.dart @@ -1,7 +1,9 @@ import 'package:flutter/material.dart'; import 'package:helpwave_localization/localization.dart'; +import 'package:helpwave_service/auth.dart'; import 'package:helpwave_service/user.dart'; import 'package:helpwave_theme/constants.dart'; +import 'package:helpwave_util/loading.dart'; import 'package:helpwave_widget/bottom_sheets.dart'; import 'package:helpwave_widget/loading.dart'; import 'package:helpwave_widget/text_input.dart'; @@ -137,24 +139,24 @@ class _TaskBottomSheetState extends State { child: Consumer( builder: (context, taskController, child) => taskController.isCreating ? Padding( - padding: const EdgeInsets.only(top: paddingSmall), - child: Align( - alignment: Alignment.topRight, - child: TextButton( - style: buttonStyleBig, - onPressed: taskController.isReadyForCreate - ? () { - taskController.create().then((value) { - if (value) { - Navigator.pop(context); - } - }); - } - : null, - child: Text(context.localization!.create), - ), - ), - ) + padding: const EdgeInsets.only(top: paddingSmall), + child: Align( + alignment: Alignment.topRight, + child: TextButton( + style: buttonStyleBig, + onPressed: taskController.isReadyForCreate + ? () { + taskController.create().then((value) { + if (value) { + Navigator.pop(context); + } + }); + } + : null, + child: Text(context.localization!.create), + ), + ), + ) : const SizedBox(), ), ), @@ -166,34 +168,35 @@ class _TaskBottomSheetState extends State { children: [ Center( child: Consumer(builder: - // TODO move this to its own component + // TODO move this to its own component (context, taskController, __) { return LoadingAndErrorWidget.pulsing( state: taskController.state, child: !taskController.isCreating ? Text(taskController.patient.name) : LoadingFutureBuilder( - future: PatientService().getPatientList(), - loadingWidget: const PulsingContainer(), - thenWidgetBuilder: (context, patientList) { - List patients = patientList.active + patientList.unassigned; - return DropdownButton( - underline: const SizedBox(), - iconEnabledColor: Theme.of(context).colorScheme.secondary.withOpacity(0.6), - // removes the default underline - padding: EdgeInsets.zero, - hint: Text( - context.localization!.selectPatient, - style: TextStyle(color: Theme.of(context).colorScheme.secondary.withOpacity(0.6)), - ), - isDense: true, - items: patients - .map((patient) => DropdownMenuItem(value: patient, child: Text(patient.name))) - .toList(), - value: taskController.patient.isCreating ? null : taskController.patient, - onChanged: (patient) => taskController.changePatient(patient ?? PatientMinimal.empty()), - ); - }), + data: PatientService().getPatientList(), + loadingWidget: const PulsingContainer(), + thenWidgetBuilder: (context, patientList) { + List patients = patientList.active + patientList.unassigned; + return DropdownButton( + underline: const SizedBox(), + iconEnabledColor: Theme.of(context).colorScheme.secondary.withOpacity(0.6), + // removes the default underline + padding: EdgeInsets.zero, + hint: Text( + context.localization!.selectPatient, + style: TextStyle(color: Theme.of(context).colorScheme.secondary.withOpacity(0.6)), + ), + isDense: true, + items: patients + .map((patient) => DropdownMenuItem(value: patient, child: Text(patient.name))) + .toList(), + value: taskController.patient.isCreating ? null : taskController.patient, + onChanged: (patient) => + taskController.changePatient(patient ?? PatientMinimal.empty()), + ); + }), ); }), ), @@ -207,43 +210,30 @@ class _TaskBottomSheetState extends State { label: context.localization!.assignedTo, onTap: () => context.pushModal( context: context, - builder: (BuildContext context) => LoadingAndErrorWidget( - state: taskController.state, - child: ChangeNotifierProvider( - create: (BuildContext context) => AssigneeSelectController( - selected: taskController.task.assigneeId, - taskId: taskController.task.id, - ), - child: AssigneeSelect( - onChanged: (assignee) { - taskController.changeAssignee(assignee.id).then((value) => Navigator.of(context).pop()); - }, - ), - ), + builder: (BuildContext context) => AssigneeSelectBottomSheet( + users: OrganizationService() + .getMembersByOrganization(CurrentWardService().currentWard!.organizationId), + onChanged: (User? assignee) { + taskController.changeAssignee(assignee); + Navigator.pop(context); + }, + selectedId: taskController.task.assigneeId, ), ), - // TODO maybe do some optimisations here - // TODO update the error and loading widgets - valueWidget: LoadingAndErrorWidget.pulsing( - state: taskController.state, - child: taskController.task.hasAssignee - ? ChangeNotifierProvider( - create: (context) => UserController(User.empty(id: taskController.task.assigneeId!)), - child: Consumer( - builder: (context, userController, __) => LoadingAndErrorWidget.pulsing( - state: userController.state, + valueWidget: taskController.task.hasAssignee + ? LoadingAndErrorWidget.pulsing( + state: taskController.assignee != null ? LoadingState.loaded : LoadingState.loading, child: Text( - userController.user.name, + // Never the case that we display the empty String, but the text is computed + // before being displayed + taskController.assignee?.name ?? "", style: editableValueTextStyle(context), ), + ) + : Text( + context.localization!.unassigned, + style: editableValueTextStyle(context), ), - ), - ) - : Text( - context.localization!.unassigned, - style: editableValueTextStyle(context), - ), - ), ); }), Consumer( @@ -278,7 +268,8 @@ class _TaskBottomSheetState extends State { lastDate: DateTime.now().add(const Duration(days: 365 * 5)), builder: (context, child) { // Overwrite the Theme - ThemeData pickerTheme = Theme.of(context).copyWith(textButtonTheme: const TextButtonThemeData()); + ThemeData pickerTheme = + Theme.of(context).copyWith(textButtonTheme: const TextButtonThemeData()); return Theme(data: pickerTheme, child: child ?? const SizedBox()); }, ).then((date) async { @@ -389,8 +380,7 @@ class _TaskBottomSheetState extends State { ), ), ], - ) - ), + )), ), ), ); diff --git a/apps/tasks/lib/components/task_expansion_tile.dart b/apps/tasks/lib/components/task_expansion_tile.dart index a5c31a61..de316ace 100644 --- a/apps/tasks/lib/components/task_expansion_tile.dart +++ b/apps/tasks/lib/components/task_expansion_tile.dart @@ -58,7 +58,7 @@ class TaskExpansionTile extends StatelessWidget { builder: (context) => TaskBottomSheet(task: task, patient: task.patient), ).then((_) { AssignedTasksController controller = Provider.of(context, listen: false); - controller.loadHandler(); + controller.load(); }); }, child: TaskCard( diff --git a/apps/tasks/lib/components/user_bottom_sheet.dart b/apps/tasks/lib/components/user_bottom_sheet.dart index 41d94aa1..4b18600a 100644 --- a/apps/tasks/lib/components/user_bottom_sheet.dart +++ b/apps/tasks/lib/components/user_bottom_sheet.dart @@ -77,7 +77,7 @@ class _UserBottomSheetState extends State { child: Consumer(builder: (context, currentWardController, __) { return LoadingFutureBuilder( loadingWidget: const SizedBox(), - future: + data: WardService().getWardOverviews(organizationId: currentWardController.currentWard!.organizationId), thenWidgetBuilder: (BuildContext context, List data) { double menuWidth = min(250, width * 0.7); diff --git a/packages/helpwave_localization/lib/l10n/app_de.arb b/packages/helpwave_localization/lib/l10n/app_de.arb index 4490bc54..18a2eeef 100644 --- a/packages/helpwave_localization/lib/l10n/app_de.arb +++ b/packages/helpwave_localization/lib/l10n/app_de.arb @@ -168,5 +168,6 @@ "darkMode": "Dunkel", "lightMode": "Hell", "system": "System", - "newTaskOrPatient": "Neu" + "newTaskOrPatient": "Neu", + "remove": "Entfernen" } diff --git a/packages/helpwave_localization/lib/l10n/app_en.arb b/packages/helpwave_localization/lib/l10n/app_en.arb index c516a9ab..0321789b 100644 --- a/packages/helpwave_localization/lib/l10n/app_en.arb +++ b/packages/helpwave_localization/lib/l10n/app_en.arb @@ -168,5 +168,6 @@ "darkMode": "Dark", "lightMode": "Light", "system": "System", - "newTaskOrPatient": "New" + "newTaskOrPatient": "New", + "remove": "Remove" } diff --git a/packages/helpwave_service/lib/src/api/offline/offline_client_store.dart b/packages/helpwave_service/lib/src/api/offline/offline_client_store.dart index d61296a2..5f62cd5a 100644 --- a/packages/helpwave_service/lib/src/api/offline/offline_client_store.dart +++ b/packages/helpwave_service/lib/src/api/offline/offline_client_store.dart @@ -15,14 +15,6 @@ const String profileUrl = "https://helpwave.de/favicon.ico"; final List initialOrganizations = [ Organization( id: "organization1", - shortName: "Test", - longName: "Test Organization", - avatarURL: profileUrl, - email: "test@helpwave.de", - isPersonal: false, - isVerified: true), - Organization( - id: "organization2", shortName: "MK", longName: "Musterklinikum", avatarURL: profileUrl, @@ -97,6 +89,7 @@ final List initialTasks = initialPatients name: "Task ${index + 1}", patientId: patient.id, notes: '', + assigneeId: [initialUsers[0].id, null, initialUsers[2].id][index], ))) .expand((element) => element) .toList(); diff --git a/packages/helpwave_service/lib/src/api/tasks/controllers/assignee_select_controller.dart b/packages/helpwave_service/lib/src/api/tasks/controllers/assignee_select_controller.dart deleted file mode 100644 index 99f6886c..00000000 --- a/packages/helpwave_service/lib/src/api/tasks/controllers/assignee_select_controller.dart +++ /dev/null @@ -1,60 +0,0 @@ -import 'package:helpwave_service/auth.dart'; -import 'package:helpwave_service/src/api/user/index.dart'; -import 'package:helpwave_util/loading.dart'; -import 'package:helpwave_service/tasks.dart'; - -/// The Controller for selecting a [User] as the assignee of a [Task] -class AssigneeSelectController extends LoadingChangeNotifier { - /// The selected [User] identifier - String? _selected; - - String? get selected => _selected; - - User? get selectedUser => _users.firstWhere((user) => user.id == selected); - - /// The currently loaded user - List _users = []; - - /// The currently loaded users - List get users => _users; - - /// The identifier of the current [Task] it determines whether - /// changes are pushed to the server - String? _taskId; - - /// Whether the object is - bool get isCreating => _taskId == null && _taskId!.isNotEmpty; // TODO remove .isNotEmpty when semantic is changed - - AssigneeSelectController({String? selected, String? taskId}) { - _selected = selected; - _taskId = taskId; - load(); - } - - /// Loads the users - Future load() async { - loadUsersFuture() async { - String? currentOrganization = CurrentWardService().currentWard?.organizationId; - if (currentOrganization == null) { - // TODO throw a better error here - throw "Organization Id not set in CurrentWardService while trying to load in AssigneeSelectController"; - } - _users = await OrganizationService().getMembersByOrganization(currentOrganization); - } - - await loadHandler(future: loadUsersFuture()); - } - - /// Change the assignee - Future changeAssignee(String id) async { - changeAssigneeFuture() async { - if (!isCreating) { - await TaskService().assignToUser(taskId: _taskId!, userId: id).then((value) => _selected = id); - } else { - _selected = id; - } - } - - await loadHandler(future: changeAssigneeFuture()); - } -} diff --git a/packages/helpwave_service/lib/src/api/tasks/controllers/index.dart b/packages/helpwave_service/lib/src/api/tasks/controllers/index.dart index a987ab71..2940b0c6 100644 --- a/packages/helpwave_service/lib/src/api/tasks/controllers/index.dart +++ b/packages/helpwave_service/lib/src/api/tasks/controllers/index.dart @@ -1,6 +1,5 @@ export 'patient_controller.dart'; export 'subtask_list_controller.dart'; export 'task_controller.dart'; -export 'assignee_select_controller.dart'; export 'my_tasks_controller.dart'; export 'ward_patients_controller.dart'; diff --git a/packages/helpwave_service/lib/src/api/tasks/controllers/my_tasks_controller.dart b/packages/helpwave_service/lib/src/api/tasks/controllers/my_tasks_controller.dart index 3953085f..70b89070 100644 --- a/packages/helpwave_service/lib/src/api/tasks/controllers/my_tasks_controller.dart +++ b/packages/helpwave_service/lib/src/api/tasks/controllers/my_tasks_controller.dart @@ -19,11 +19,11 @@ class AssignedTasksController extends LoadingChangeNotifier { List get done => _tasks.where((element) => element.status == TaskStatus.done).toList(); AssignedTasksController() { - loadTasks(); + load(); } /// Loads the [Task]s - Future loadTasks() async { + Future load() async { loadTasksFuture() async { _tasks = await TaskService().getAssignedTasks(); } diff --git a/packages/helpwave_service/lib/src/api/tasks/controllers/task_controller.dart b/packages/helpwave_service/lib/src/api/tasks/controllers/task_controller.dart index 80061bb8..2eae7924 100644 --- a/packages/helpwave_service/lib/src/api/tasks/controllers/task_controller.dart +++ b/packages/helpwave_service/lib/src/api/tasks/controllers/task_controller.dart @@ -1,6 +1,8 @@ import 'package:helpwave_util/loading.dart'; import 'package:helpwave_service/src/api/tasks/index.dart'; +import '../../../../user.dart'; + /// The Controller for managing a [TaskWithPatient] class TaskController extends LoadingChangeNotifier { /// The current [Task] @@ -14,7 +16,6 @@ class TaskController extends LoadingChangeNotifier { TaskWithPatient get task => _task; - set task(TaskWithPatient value) { _task = value; notifyListeners(); @@ -22,6 +23,10 @@ class TaskController extends LoadingChangeNotifier { PatientMinimal get patient => task.patient; + User? _assignee; + + User? get assignee => _assignee; + bool get isCreating => _task.isCreating; /// Whether the [Task] object can be used to create a [Task] @@ -32,79 +37,100 @@ class TaskController extends LoadingChangeNotifier { /// A function to load the [Task] load() async { loadTask() async { - await TaskService().getTask(id: task.id).then((value) { + await TaskService().getTask(id: task.id).then((value) async { task = value; + if (task.hasAssignee) { + UserService().getUser(id: task.assigneeId!).then((value) => _assignee = value); + } }); } + loadHandler(future: loadTask()); } /// Changes the assigned [User] - /// - /// Without a backend request as we expect this to be done in the [AssigneeSelectController] - Future changeAssignee(String assigneeId) async { - String? old = task.assigneeId; - updateTask( - (task) { - task.assigneeId = assigneeId; - }, - (task) { - task.assigneeId = old; - }, - ); + Future changeAssignee(User? user) async { + if (isCreating) { + task.assigneeId = user?.id; + _assignee = user; + notifyListeners(); + return; + } + changeAssigneeFuture() async { + await TaskService().changeAssignee(taskId: task.id, userId: user?.id).then((value) { + task.assigneeId = user?.id; + _assignee = user; + }); + } + + await loadHandler(future: changeAssigneeFuture()); } Future changeName(String name) async { - String oldName = task.name; - updateTask( - (task) { - task.name = name; - }, - (task) { - task.name = oldName; - }, - ); + if (isCreating) { + task = TaskWithPatient.fromTaskAndPatient(task: task.copyWith(name: name), patient: task.patient); + notifyListeners(); + return; + } + updateName() async { + await TaskService().updateTask(taskId: task.id, name: name).then( + (_) => task = TaskWithPatient.fromTaskAndPatient(task: task.copyWith(name: name), patient: task.patient)); + } + + loadHandler(future: updateName()); } Future changeIsPublic(bool isPublic) async { - bool old = task.isPublicVisible; - updateTask( - (task) { - task.isPublicVisible = isPublic; - }, - (task) { - task.isPublicVisible = old; - }, - ); + if (isCreating) { + task = TaskWithPatient.fromTaskAndPatient(task: task.copyWith(isPublicVisible: isPublic), patient: task.patient); + notifyListeners(); + return; + } + updateIsPublic() async { + await TaskService().updateTask(taskId: task.id, isPublic: isPublic).then((_) => task = + TaskWithPatient.fromTaskAndPatient(task: task.copyWith(isPublicVisible: isPublic), patient: task.patient)); + } + + loadHandler(future: updateIsPublic()); } Future changeNotes(String notes) async { - String oldNotes = notes; - updateTask( - (task) { - task.notes = notes; - }, - (task) { - task.notes = oldNotes; - }, - ); + if (isCreating) { + task = TaskWithPatient.fromTaskAndPatient(task: task.copyWith(notes: notes), patient: task.patient); + notifyListeners(); + return; + } + updateNotes() async { + await TaskService().updateTask(taskId: task.id, notes: notes).then( + (_) => task = TaskWithPatient.fromTaskAndPatient(task: task.copyWith(notes: notes), patient: task.patient)); + } + + loadHandler(future: updateNotes()); } Future changeDueDate(DateTime? dueDate) async { - DateTime? old = task.dueDate; - updateTask( - (task) { - task.dueDate = dueDate; - }, - (task) { - task.dueDate = old; - }, - ); + if (isCreating) { + task = TaskWithPatient.fromTaskAndPatient(task: task.copyWith(dueDate: dueDate), patient: task.patient); + notifyListeners(); + return; + } + updateDueDate() async { + await TaskService().updateTask(taskId: task.id, dueDate: dueDate).then((_) => + task = TaskWithPatient.fromTaskAndPatient(task: task.copyWith(dueDate: dueDate), patient: task.patient)); + } + + removeDueDate() async { + await TaskService().removeDueDate(taskId: task.id).then((_) => + task = TaskWithPatient.fromTaskAndPatient(task: task.copyWith(dueDate: dueDate), patient: task.patient)); + } + + loadHandler(future: dueDate == null ? removeDueDate() : updateDueDate()); } - /// Only usable when creating + /// Only usable when creating a [Task] Future changePatient(PatientMinimal patient) async { assert(isCreating, "Only use TaskController.changePatient, when you create a new task."); + assert(!patient.isCreating, "The patient you are trying to attach the Task to must exist"); task = TaskWithPatient.fromTaskAndPatient(task: task.copyWith(patientId: patient.id), patient: patient); notifyListeners(); } @@ -112,15 +138,12 @@ class TaskController extends LoadingChangeNotifier { /// Creates the Task and returns Future create() async { assert(!isReadyForCreate, "A the patient must be set to create a task"); - state = LoadingState.loading; - return await TaskService().createTask(task).then((value) { - task.copyWith(id: value); - state = LoadingState.loaded; - return true; - }).catchError((error, stackTrace) { - errorMessage = error.toString(); - state = LoadingState.error; - return false; - }); + createTask() async { + await TaskService().createTask(task).then((value) { + task.copyWith(id: value); + }); + } + + return loadHandler(future: createTask()); } } diff --git a/packages/helpwave_service/lib/src/api/tasks/controllers/ward_patients_controller.dart b/packages/helpwave_service/lib/src/api/tasks/controllers/ward_patients_controller.dart index c93817d7..c1a29d76 100644 --- a/packages/helpwave_service/lib/src/api/tasks/controllers/ward_patients_controller.dart +++ b/packages/helpwave_service/lib/src/api/tasks/controllers/ward_patients_controller.dart @@ -1,13 +1,9 @@ -import 'package:flutter/cupertino.dart'; import 'package:helpwave_service/src/api/tasks/index.dart'; import 'package:helpwave_util/loading.dart'; import 'package:helpwave_util/search.dart'; -/// The Controller for managing [Patient]s in a Ward -class WardPatientsController extends ChangeNotifier { - /// The [LoadingState] of the Controller - LoadingState state = LoadingState.initializing; - +/// The Controller for managing [Patient]s in a [Ward] +class WardPatientsController extends LoadingChangeNotifier { /// The [Patient]s mapped by the [PatientsByAssignmentStatus] PatientsByAssignmentStatus _patientsByAssignmentStatus = PatientsByAssignmentStatus(); @@ -70,22 +66,20 @@ class WardPatientsController extends ChangeNotifier { return results; } - /// Loads the [patients] + /// Loads the [Patient]s Future load() async { - state = LoadingState.loading; - notifyListeners(); - - _patientsByAssignmentStatus = await PatientService().getPatientList(); - state = LoadingState.loaded; - notifyListeners(); + loadPatients() async { + _patientsByAssignmentStatus = await PatientService().getPatientList(); + } + loadHandler(future: loadPatients()); } /// Discharges the patient the [patients] Future discharge(String patientId) async { - state = LoadingState.loading; - notifyListeners(); - await PatientService().dischargePatient(patientId: patientId); - // Here we can maybe use optimistic updates - load(); + dischargePatient() async { + await PatientService().dischargePatient(patientId: patientId); + await load(); + } + loadHandler(future: dischargePatient()); } } diff --git a/packages/helpwave_service/lib/src/api/tasks/data_types/patient.dart b/packages/helpwave_service/lib/src/api/tasks/data_types/patient.dart index db56424f..9a4adc94 100644 --- a/packages/helpwave_service/lib/src/api/tasks/data_types/patient.dart +++ b/packages/helpwave_service/lib/src/api/tasks/data_types/patient.dart @@ -136,13 +136,12 @@ class PatientsByAssignmentStatus { List active; List unassigned; List discharged; - List all; + List get all => active + unassigned + discharged; PatientsByAssignmentStatus({ this.active = const [], this.unassigned = const [], this.discharged = const [], - this.all = const [], }); byAssignmentStatus(PatientAssignmentStatus status) { diff --git a/packages/helpwave_service/lib/src/api/tasks/services/patient_svc.dart b/packages/helpwave_service/lib/src/api/tasks/services/patient_svc.dart index a6898183..5695ddbe 100644 --- a/packages/helpwave_service/lib/src/api/tasks/services/patient_svc.dart +++ b/packages/helpwave_service/lib/src/api/tasks/services/patient_svc.dart @@ -59,7 +59,6 @@ class PatientService { final discharged = response.dischargedPatients.map(mapping).toList(); return PatientsByAssignmentStatus( - all: active + unassigned + discharged, active: active, unassigned: unassigned, discharged: discharged, diff --git a/packages/helpwave_service/lib/src/api/tasks/services/task_svc.dart b/packages/helpwave_service/lib/src/api/tasks/services/task_svc.dart index 2de819e2..f32e175f 100644 --- a/packages/helpwave_service/lib/src/api/tasks/services/task_svc.dart +++ b/packages/helpwave_service/lib/src/api/tasks/services/task_svc.dart @@ -124,16 +124,20 @@ class TaskService { return response.id; } - /// Assign a [Task] to a [User] - Future assignToUser({required String taskId, required String userId}) async { - AssignTaskRequest request = AssignTaskRequest(taskId: taskId, userId: userId); - AssignTaskResponse response = await taskService.assignTask( - request, - options: CallOptions(metadata: TasksAPIServiceClients().getMetaData()), - ); - - if (!response.isInitialized()) { - // Handle error + /// Assign a [Task] to a [User] or unassign the [User] + Future changeAssignee({required String taskId, required String? userId}) async { + if(userId != null){ + AssignTaskRequest request = AssignTaskRequest(taskId: taskId, userId: userId); + await taskService.assignTask( + request, + options: CallOptions(metadata: TasksAPIServiceClients().getMetaData()), + ); + } else { + UnassignTaskRequest request = UnassignTaskRequest(taskId: taskId, userId: userId); + await taskService.unassignTask( + request, + options: CallOptions(metadata: TasksAPIServiceClients().getMetaData()), + ); } } @@ -184,14 +188,21 @@ class TaskService { return response.isInitialized(); } - Future updateTask(Task task) async { + Future updateTask({ + required String taskId, + String? name, + String? notes, + DateTime? dueDate, + bool? isPublic, + TaskStatus? status, + }) async { UpdateTaskRequest request = UpdateTaskRequest( - id: task.id, - name: task.name, - description: task.notes, - dueAt: task.dueDate != null ? Timestamp.fromDateTime(task.dueDate!) : null, - public: task.isPublicVisible, - status: GRPCTypeConverter.taskStatusToGRPC(task.status), + id: taskId, + name: name, + description: notes, + dueAt: dueDate != null ? Timestamp.fromDateTime(dueDate) : null, + public: isPublic, + status: status != null ? GRPCTypeConverter.taskStatusToGRPC(status) : null, ); UpdateTaskResponse response = await taskService.updateTask( @@ -201,4 +212,14 @@ class TaskService { return response.isInitialized(); } + + Future removeDueDate({ + required String taskId, + }) async { + RemoveTaskDueDateRequest request = RemoveTaskDueDateRequest(taskId: taskId); + await taskService.removeTaskDueDate( + request, + options: CallOptions(metadata: TasksAPIServiceClients().getMetaData()), + ); + } } diff --git a/packages/helpwave_util/lib/src/loading/loading_change_notifier.dart b/packages/helpwave_util/lib/src/loading/loading_change_notifier.dart index 53dfa8c3..a3fa3782 100644 --- a/packages/helpwave_util/lib/src/loading/loading_change_notifier.dart +++ b/packages/helpwave_util/lib/src/loading/loading_change_notifier.dart @@ -20,17 +20,21 @@ class LoadingChangeNotifier extends ChangeNotifier { LoadingChangeNotifier(); - Future loadHandler({ + Future loadHandler({ required Future future, Future Function(Object? error, StackTrace stackTrace)? errorHandler, }) async { + bool success = false; defaultErrorHandler(errorObj, _) async { error = errorObj.toString(); return false; } changeState(LoadingState.loading); - await future.then((_) => changeState(LoadingState.loaded)).onError((error, stackTrace) async { + await future.then((_) { + changeState(LoadingState.loaded); + success = true; + }).onError((error, stackTrace) async { if (errorHandler != null) { try { bool isHandled = await errorHandler(error, stackTrace); @@ -44,5 +48,6 @@ class LoadingChangeNotifier extends ChangeNotifier { changeState(LoadingState.error); }); + return success; } } diff --git a/packages/helpwave_widget/lib/content_selection.dart b/packages/helpwave_widget/lib/content_selection.dart index dab610d1..ad24aa53 100644 --- a/packages/helpwave_widget/lib/content_selection.dart +++ b/packages/helpwave_widget/lib/content_selection.dart @@ -1,3 +1 @@ -export 'package:helpwave_widget/src/content_selection/list_search.dart' show ListSearch; -export 'package:helpwave_widget/src/content_selection/content_selector.dart' show ContentSelector; -export 'package:helpwave_widget/src/content_selection/chip_select.dart'; +export 'package:helpwave_widget/src/content_selection/index.dart'; diff --git a/packages/helpwave_widget/lib/src/content_selection/index.dart b/packages/helpwave_widget/lib/src/content_selection/index.dart new file mode 100644 index 00000000..6c033924 --- /dev/null +++ b/packages/helpwave_widget/lib/src/content_selection/index.dart @@ -0,0 +1,4 @@ +export 'chip_select.dart'; +export 'content_selector.dart' show ContentSelector; +export 'list_search.dart'; +export 'list_select.dart'; diff --git a/packages/helpwave_widget/lib/src/content_selection/list_select.dart b/packages/helpwave_widget/lib/src/content_selection/list_select.dart new file mode 100644 index 00000000..1f7e9383 --- /dev/null +++ b/packages/helpwave_widget/lib/src/content_selection/list_select.dart @@ -0,0 +1,28 @@ +import 'dart:async'; +import 'package:flutter/cupertino.dart'; +import 'package:helpwave_widget/loading.dart'; + +class ListSelect extends StatelessWidget { + final FutureOr> items; + + final void Function(T item) onSelect; + + final Widget Function(BuildContext context, T item, Function() select) builder; + + const ListSelect({ + super.key, + required this.items, + required this.onSelect, + required this.builder, + }); + + @override + Widget build(BuildContext context) { + return LoadingFutureBuilder( + data: items, + thenWidgetBuilder: (context, data) => Column( + children: data.map((item) => builder(context, item, () => onSelect(item))).toList(), + ), + ); + } +} diff --git a/packages/helpwave_widget/lib/src/loading/loading_future_builder.dart b/packages/helpwave_widget/lib/src/loading/loading_future_builder.dart index 08b1625b..fb7439c0 100644 --- a/packages/helpwave_widget/lib/src/loading/loading_future_builder.dart +++ b/packages/helpwave_widget/lib/src/loading/loading_future_builder.dart @@ -1,3 +1,4 @@ +import 'dart:async'; import 'package:flutter/cupertino.dart'; import 'package:helpwave_util/loading.dart'; import 'package:helpwave_widget/loading.dart'; @@ -5,21 +6,21 @@ import 'package:helpwave_widget/loading.dart'; /// A Wrapper for the standard [FutureBuilder] to easily distinguish the three /// cases error, loading, then class LoadingFutureBuilder extends StatelessWidget { - /// The [Future] to load - final Future future; + /// The [FutureOr] to load + final FutureOr data; - /// The Builder for the [Widget] upon an successful [Future] + /// The Builder for the [Widget] upon an successful [FutureOr] final Widget Function(BuildContext context, T data) thenWidgetBuilder; - /// The Builder for the [Widget] when loading the [Future] + /// The Builder for the [Widget] when loading the [FutureOr] final Widget loadingWidget; - /// The [Widget] for an error containing [Future] + /// The [Widget] for an error containing [FutureOr] final Widget errorWidget; const LoadingFutureBuilder({ super.key, - required this.future, + required this.data, required this.thenWidgetBuilder, this.loadingWidget = const LoadingSpinner(), this.errorWidget = const LoadErrorWidget(), @@ -27,24 +28,27 @@ class LoadingFutureBuilder extends StatelessWidget { @override Widget build(BuildContext context) { - return FutureBuilder( - future: future, - builder: (context, snapshot) { - LoadingState state = LoadingState.loaded; - if (snapshot.hasError) { - state = LoadingState.error; - } - if (!snapshot.hasData || snapshot.data == null) { - state = LoadingState.loading; - } - return LoadingAndErrorWidget( - state: state, - errorWidget: Center(child: errorWidget), - loadingWidget: Center(child: loadingWidget), - // Safety check because typecast may fail otherwise - child: snapshot.data != null ? thenWidgetBuilder(context, snapshot.data as T) : const SizedBox(), - ); - }, - ); + if(data is Future){ + return FutureBuilder( + future: data as Future, + builder: (context, snapshot) { + LoadingState state = LoadingState.loaded; + if (snapshot.hasError) { + state = LoadingState.error; + } + if (!snapshot.hasData || snapshot.data == null) { + state = LoadingState.loading; + } + return LoadingAndErrorWidget( + state: state, + errorWidget: Center(child: errorWidget), + loadingWidget: Center(child: loadingWidget), + // Safety check because typecast may fail otherwise + child: snapshot.data != null ? thenWidgetBuilder(context, snapshot.data as T) : const SizedBox(), + ); + }, + ); + } + return thenWidgetBuilder(context, data as T); } } From fe095c1b41ecb93b7890f13b5a1dce3258f254af Mon Sep 17 00:00:00 2001 From: Felix Thape Date: Wed, 18 Sep 2024 18:21:09 +0200 Subject: [PATCH 07/36] feat: update bottom sheets to handle nested navigation --- .../tasks/lib/components/assignee_select.dart | 30 +- .../lib/components/patient_bottom_sheet.dart | 270 +++++----- .../lib/components/task_bottom_sheet.dart | 461 +++++++++--------- .../lib/components/user_bottom_sheet.dart | 61 +-- apps/tasks/lib/components/user_header.dart | 152 ++---- .../lib/components/visibility_select.dart | 6 +- apps/tasks/lib/main.dart | 4 +- apps/tasks/lib/screens/main_screen.dart | 2 +- apps/tasks/lib/screens/settings_screen.dart | 134 ++++- .../src/api/user/services/user_service.dart | 20 + .../helpwave_theme/lib/src/constants.dart | 2 + .../lib/src/theme/dark_theme.dart | 6 + .../lib/src/theme/light_theme.dart | 8 +- .../helpwave_theme/lib/src/theme/theme.dart | 10 +- .../helpwave_widget/lib/bottom_sheets.dart | 2 +- .../src/bottom_sheets/bottom_sheet_base.dart | 253 ++++++---- .../lib/src/bottom_sheets/index.dart | 2 + .../nested_bottom_sheet_navigation.dart | 102 ++++ .../lib/src/widgets/fallback_avatar.dart | 40 ++ .../lib/src/widgets/index.dart | 2 + .../{widgets.dart => list_tile_card.dart} | 0 packages/helpwave_widget/lib/widgets.dart | 2 +- packages/helpwave_widget/pubspec.yaml | 1 + 23 files changed, 946 insertions(+), 624 deletions(-) create mode 100644 packages/helpwave_widget/lib/src/bottom_sheets/index.dart create mode 100644 packages/helpwave_widget/lib/src/bottom_sheets/nested_bottom_sheet_navigation.dart create mode 100644 packages/helpwave_widget/lib/src/widgets/fallback_avatar.dart create mode 100644 packages/helpwave_widget/lib/src/widgets/index.dart rename packages/helpwave_widget/lib/src/widgets/{widgets.dart => list_tile_card.dart} (100%) diff --git a/apps/tasks/lib/components/assignee_select.dart b/apps/tasks/lib/components/assignee_select.dart index 02e4794f..3e314f90 100644 --- a/apps/tasks/lib/components/assignee_select.dart +++ b/apps/tasks/lib/components/assignee_select.dart @@ -2,7 +2,6 @@ import 'dart:async'; import 'package:flutter/material.dart'; import 'package:helpwave_localization/localization.dart'; import 'package:helpwave_service/user.dart'; -import 'package:helpwave_theme/constants.dart'; import 'package:helpwave_widget/bottom_sheets.dart'; import 'package:helpwave_widget/content_selection.dart'; @@ -22,31 +21,32 @@ class AssigneeSelectBottomSheet extends StatelessWidget { @override Widget build(BuildContext context) { return BottomSheetBase( - titleText: context.localization!.assignee, + header: BottomSheetHeader( + titleText: context.localization!.assignee, + ), onClosing: () => {}, - builder: (context) => Padding( - padding: const EdgeInsets.symmetric(vertical: paddingMedium), - child: Column( + builder: (context) => Column( children: [ TextButton( child: Text(context.localization!.remove), onPressed: () => onChanged(null), ), const SizedBox(height: 10), - ListSelect( - items: users, - onSelect: onChanged, - builder: (context, user, select) => ListTile( - onTap: select, - leading: CircleAvatar( - foregroundColor: Colors.blue, backgroundImage: NetworkImage(user.profileUrl.toString())), - title: Text(user.nickName, - style: TextStyle(decoration: user.id == selectedId ? TextDecoration.underline : null)), + SingleChildScrollView( + child: ListSelect( + items: users, + onSelect: onChanged, + builder: (context, user, select) => ListTile( + onTap: select, + leading: CircleAvatar( + foregroundColor: Colors.blue, backgroundImage: NetworkImage(user.profileUrl.toString())), + title: Text(user.nickName, + style: TextStyle(decoration: user.id == selectedId ? TextDecoration.underline : null)), + ), ), ), ], ), - ), ); } } diff --git a/apps/tasks/lib/components/patient_bottom_sheet.dart b/apps/tasks/lib/components/patient_bottom_sheet.dart index 431773a4..446882d2 100644 --- a/apps/tasks/lib/components/patient_bottom_sheet.dart +++ b/apps/tasks/lib/components/patient_bottom_sheet.dart @@ -52,29 +52,31 @@ class _PatientBottomSheetState extends State { ), ], child: BottomSheetBase( - title: Consumer(builder: (context, patientController, _) { - if (patientController.state == LoadingState.loaded || patientController.isCreating) { - return ClickableTextEdit( - initialValue: patientController.patient.name, - onUpdated: patientController.changeName, - textAlign: TextAlign.center, - textStyle: TextStyle( - color: Theme.of(context).colorScheme.secondary, - fontWeight: FontWeight.bold, - fontSize: iconSizeTiny, - fontFamily: "SpaceGrotesk", - overflow: TextOverflow.ellipsis, - ), - ); - } else { - return const PulsingContainer(width: 30); - } - }), + header: BottomSheetHeader( + title: Consumer(builder: (context, patientController, _) { + if (patientController.state == LoadingState.loaded || patientController.isCreating) { + return ClickableTextEdit( + initialValue: patientController.patient.name, + onUpdated: patientController.changeName, + textAlign: TextAlign.center, + textStyle: TextStyle( + color: Theme.of(context).colorScheme.secondary, + fontWeight: FontWeight.bold, + fontSize: iconSizeTiny, + fontFamily: "SpaceGrotesk", + overflow: TextOverflow.ellipsis, + ), + ); + } else { + return const PulsingContainer(width: 30); + } + }), + ), onClosing: () { // TODO handle this }, bottomWidget: Padding( - padding: const EdgeInsets.only(top: paddingMedium), + padding: const EdgeInsets.only(top: paddingSmall), child: Consumer(builder: (context, patientController, _) { return LoadingAndErrorWidget( state: patientController.state, @@ -145,129 +147,131 @@ class _PatientBottomSheetState extends State { )); }), ), - builder: (BuildContext context) => Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Center( - child: Consumer(builder: (context, patientController, _) { - return LoadingFutureBuilder( - data: loadRoomsWithBeds(patientController.patient.id), - // TODO use a better loading widget - loadingWidget: const SizedBox(), - thenWidgetBuilder: (context, beds) { - if (beds.isEmpty) { - return Text( - context.localization!.noFreeBeds, - style: TextStyle(color: Theme.of(context).disabledColor, fontWeight: FontWeight.bold), - ); - } - return DropdownButtonHideUnderline( - child: DropdownButton( - iconEnabledColor: Theme.of(context).colorScheme.secondary.withOpacity(0.6), - padding: EdgeInsets.zero, - isDense: true, - hint: Text( - context.localization!.assignBed, - style: TextStyle(color: Theme.of(context).colorScheme.secondary.withOpacity(0.6)), + builder: (BuildContext context) => SingleChildScrollView( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Center( + child: Consumer(builder: (context, patientController, _) { + return LoadingFutureBuilder( + data: loadRoomsWithBeds(patientController.patient.id), + // TODO use a better loading widget + loadingWidget: const SizedBox(), + thenWidgetBuilder: (context, beds) { + if (beds.isEmpty) { + return Text( + context.localization!.noFreeBeds, + style: TextStyle(color: Theme.of(context).disabledColor, fontWeight: FontWeight.bold), + ); + } + return DropdownButtonHideUnderline( + child: DropdownButton( + iconEnabledColor: Theme.of(context).colorScheme.secondary.withOpacity(0.6), + padding: EdgeInsets.zero, + isDense: true, + hint: Text( + context.localization!.assignBed, + style: TextStyle(color: Theme.of(context).colorScheme.secondary.withOpacity(0.6)), + ), + value: beds.where((beds) => beds.bed.id == patientController.patient.bed?.id).firstOrNull, + items: beds + .map((roomWithBed) => DropdownMenuItem( + value: roomWithBed, + child: Text( + "${roomWithBed.room.name} - ${roomWithBed.bed.name}", + style: TextStyle(color: Theme.of(context).colorScheme.primary.withOpacity(0.6)), + ), + )) + .toList(), + onChanged: (RoomWithBedFlat? value) { + // TODO later unassign here + if (value == null) { + return; + } + patientController.assignToBed(value.room, value.bed); + }, ), - value: beds.where((beds) => beds.bed.id == patientController.patient.bed?.id).firstOrNull, - items: beds - .map((roomWithBed) => DropdownMenuItem( - value: roomWithBed, - child: Text( - "${roomWithBed.room.name} - ${roomWithBed.bed.name}", - style: TextStyle(color: Theme.of(context).colorScheme.primary.withOpacity(0.6)), - ), - )) - .toList(), - onChanged: (RoomWithBedFlat? value) { - // TODO later unassign here - if (value == null) { - return; - } - patientController.assignToBed(value.room, value.bed); - }, - ), - ); - }, - ); - }), - ), - Text( - context.localization!.notes, - style: const TextStyle(fontSize: fontSizeBig, fontWeight: FontWeight.bold), - ), - const SizedBox(height: distanceSmall), - Consumer( - builder: (context, patientController, _) => - patientController.state == LoadingState.loaded || patientController.isCreating - ? TextFormFieldWithTimer( - initialValue: patientController.patient.notes, - maxLines: 6, - onUpdate: patientController.changeNotes, - ) - : TextFormField(maxLines: 6), - ), - Padding( - padding: const EdgeInsets.symmetric(vertical: paddingMedium), - child: Consumer(builder: (context, patientController, _) { - Patient patient = patientController.patient; - return AddList( - maxHeight: width * 0.5, - items: [ - ...patient.unscheduledTasks, - ...patient.inProgressTasks, - ...patient.doneTasks, - ], - itemBuilder: (_, index, taskList) { - if (index == 0) { - return TaskExpansionTile( - tasks: patient.unscheduledTasks - .map((task) => TaskWithPatient.fromTaskAndPatient( - task: task, - patient: patient, - )) - .toList(), - title: context.localization!.upcoming, - color: upcomingColor, ); - } - if (index == 2) { + }, + ); + }), + ), + Text( + context.localization!.notes, + style: const TextStyle(fontSize: fontSizeBig, fontWeight: FontWeight.bold), + ), + const SizedBox(height: distanceSmall), + Consumer( + builder: (context, patientController, _) => + patientController.state == LoadingState.loaded || patientController.isCreating + ? TextFormFieldWithTimer( + initialValue: patientController.patient.notes, + maxLines: 6, + onUpdate: patientController.changeNotes, + ) + : TextFormField(maxLines: 6), + ), + Padding( + padding: const EdgeInsets.symmetric(vertical: paddingMedium), + child: Consumer(builder: (context, patientController, _) { + Patient patient = patientController.patient; + return AddList( + maxHeight: width * 0.5, + items: [ + ...patient.unscheduledTasks, + ...patient.inProgressTasks, + ...patient.doneTasks, + ], + itemBuilder: (_, index, taskList) { + if (index == 0) { + return TaskExpansionTile( + tasks: patient.unscheduledTasks + .map((task) => TaskWithPatient.fromTaskAndPatient( + task: task, + patient: patient, + )) + .toList(), + title: context.localization!.upcoming, + color: upcomingColor, + ); + } + if (index == 2) { + return TaskExpansionTile( + tasks: patient.doneTasks + .map((task) => TaskWithPatient.fromTaskAndPatient( + task: task, + patient: patient, + )) + .toList(), + title: context.localization!.inProgress, + color: inProgressColor, + ); + } return TaskExpansionTile( - tasks: patient.doneTasks + tasks: patient.inProgressTasks .map((task) => TaskWithPatient.fromTaskAndPatient( task: task, patient: patient, )) .toList(), - title: context.localization!.inProgress, - color: inProgressColor, + title: context.localization!.done, + color: doneColor, ); - } - return TaskExpansionTile( - tasks: patient.inProgressTasks - .map((task) => TaskWithPatient.fromTaskAndPatient( - task: task, - patient: patient, - )) - .toList(), - title: context.localization!.done, - color: doneColor, - ); - }, - title: Text( - context.localization!.tasks, - style: const TextStyle(fontSize: fontSizeBig, fontWeight: FontWeight.bold), - ), - // TODO use return value to add it to task list or force a refetch - onAdd: () => context.pushModal( - context: context, - builder: (context) => TaskBottomSheet(task: Task.empty(patient.id), patient: patient), - ), - ); - }), - ), - ], + }, + title: Text( + context.localization!.tasks, + style: const TextStyle(fontSize: fontSizeBig, fontWeight: FontWeight.bold), + ), + // TODO use return value to add it to task list or force a refetch + onAdd: () => context.pushModal( + context: context, + builder: (context) => TaskBottomSheet(task: Task.empty(patient.id), patient: patient), + ), + ); + }), + ), + ], + ), ), ), ); diff --git a/apps/tasks/lib/components/task_bottom_sheet.dart b/apps/tasks/lib/components/task_bottom_sheet.dart index 7f2590d4..271facb8 100644 --- a/apps/tasks/lib/components/task_bottom_sheet.dart +++ b/apps/tasks/lib/components/task_bottom_sheet.dart @@ -116,11 +116,11 @@ class _TaskBottomSheetState extends State { return ChangeNotifierProvider( create: (context) => TaskController(TaskWithPatient.fromTaskAndPatient(task: widget.task, patient: widget.patient)), - child: SingleChildScrollView( - child: BottomSheetBase( - onClosing: () async { - // TODO do saving or something when the dialog is closed - }, + child: BottomSheetBase( + onClosing: () async { + // TODO do saving or something when the dialog is closed + }, + header: BottomSheetHeader( title: Consumer( builder: (context, taskController, child) => ClickableTextEdit( initialValue: taskController.task.name, @@ -135,252 +135,247 @@ class _TaskBottomSheetState extends State { ), ), ), - bottomWidget: Flexible( - child: Consumer( - builder: (context, taskController, child) => taskController.isCreating - ? Padding( - padding: const EdgeInsets.only(top: paddingSmall), - child: Align( - alignment: Alignment.topRight, - child: TextButton( - style: buttonStyleBig, - onPressed: taskController.isReadyForCreate - ? () { - taskController.create().then((value) { - if (value) { - Navigator.pop(context); - } - }); + ), + bottomWidget: Consumer( + builder: (context, taskController, child) => taskController.isCreating + ? Padding( + padding: const EdgeInsets.only(top: paddingSmall), + child: Align( + alignment: Alignment.topRight, + child: TextButton( + style: buttonStyleBig, + onPressed: taskController.isReadyForCreate + ? () { + taskController.create().then((value) { + if (value) { + Navigator.pop(context); } - : null, - child: Text(context.localization!.create), - ), - ), - ) - : const SizedBox(), - ), - ), - builder: (context) => Container( - constraints: BoxConstraints(maxHeight: MediaQuery.of(context).size.height * 0.8), - child: Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Center( - child: Consumer(builder: - // TODO move this to its own component - (context, taskController, __) { - return LoadingAndErrorWidget.pulsing( - state: taskController.state, - child: !taskController.isCreating - ? Text(taskController.patient.name) - : LoadingFutureBuilder( - data: PatientService().getPatientList(), - loadingWidget: const PulsingContainer(), - thenWidgetBuilder: (context, patientList) { - List patients = patientList.active + patientList.unassigned; - return DropdownButton( - underline: const SizedBox(), - iconEnabledColor: Theme.of(context).colorScheme.secondary.withOpacity(0.6), - // removes the default underline - padding: EdgeInsets.zero, - hint: Text( - context.localization!.selectPatient, - style: TextStyle(color: Theme.of(context).colorScheme.secondary.withOpacity(0.6)), - ), - isDense: true, - items: patients - .map((patient) => DropdownMenuItem(value: patient, child: Text(patient.name))) - .toList(), - value: taskController.patient.isCreating ? null : taskController.patient, - onChanged: (patient) => - taskController.changePatient(patient ?? PatientMinimal.empty()), - ); - }), - ); - }), + }); + } + : null, + child: Text(context.localization!.create), + ), ), - const SizedBox(height: distanceMedium), - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Consumer(builder: (context, taskController, __) { - return _SheetListTile( - icon: Icons.person, - label: context.localization!.assignedTo, - onTap: () => context.pushModal( - context: context, - builder: (BuildContext context) => AssigneeSelectBottomSheet( - users: OrganizationService() - .getMembersByOrganization(CurrentWardService().currentWard!.organizationId), - onChanged: (User? assignee) { - taskController.changeAssignee(assignee); - Navigator.pop(context); - }, - selectedId: taskController.task.assigneeId, - ), - ), - valueWidget: taskController.task.hasAssignee - ? LoadingAndErrorWidget.pulsing( - state: taskController.assignee != null ? LoadingState.loaded : LoadingState.loading, - child: Text( - // Never the case that we display the empty String, but the text is computed - // before being displayed - taskController.assignee?.name ?? "", - style: editableValueTextStyle(context), - ), - ) - : Text( - context.localization!.unassigned, - style: editableValueTextStyle(context), + ) + : const SizedBox(), + ), + builder: (context) => Flexible( + child: ListView( + children: [ + Center( + child: Consumer(builder: + // TODO move this to its own component + (context, taskController, __) { + return LoadingAndErrorWidget.pulsing( + state: taskController.state, + child: !taskController.isCreating + ? Text(taskController.patient.name) + : LoadingFutureBuilder( + data: PatientService().getPatientList(), + loadingWidget: const PulsingContainer(), + thenWidgetBuilder: (context, patientList) { + List patients = patientList.active + patientList.unassigned; + return DropdownButton( + underline: const SizedBox(), + iconEnabledColor: Theme.of(context).colorScheme.secondary.withOpacity(0.6), + // removes the default underline + padding: EdgeInsets.zero, + hint: Text( + context.localization!.selectPatient, + style: TextStyle(color: Theme.of(context).colorScheme.secondary.withOpacity(0.6)), ), - ); - }), - Consumer( - builder: (context, taskController, __) => LoadingAndErrorWidget.pulsing( - state: taskController.state, - child: _SheetListTile( - icon: Icons.access_time, - label: context.localization!.due, - // TODO localization and date formatting here - valueWidget: Builder(builder: (context) { - DateTime? dueDate = taskController.task.dueDate; - if (dueDate != null) { - String date = - "${dueDate.day.toString().padLeft(2, "0")}.${dueDate.month.toString().padLeft(2, "0")}.${dueDate.year.toString().padLeft(4, "0")}"; - String time = - "${dueDate.hour.toString().padLeft(2, "0")}:${dueDate.minute.toString().padLeft(2, "0")}"; - return Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text(time, style: editableValueTextStyle(context)), - Text(date), - ], - ); - } - return Text(context.localization!.none); + isDense: true, + items: patients + .map((patient) => DropdownMenuItem(value: patient, child: Text(patient.name))) + .toList(), + value: taskController.patient.isCreating ? null : taskController.patient, + onChanged: (patient) => taskController.changePatient(patient ?? PatientMinimal.empty()), + ); }), - onTap: () => showDatePicker( - context: context, - initialDate: taskController.task.dueDate ?? DateTime.now(), - firstDate: DateTime(1960), - lastDate: DateTime.now().add(const Duration(days: 365 * 5)), - builder: (context, child) { - // Overwrite the Theme - ThemeData pickerTheme = - Theme.of(context).copyWith(textButtonTheme: const TextButtonThemeData()); - return Theme(data: pickerTheme, child: child ?? const SizedBox()); - }, - ).then((date) async { - await showTimePicker( - context: context, - initialTime: TimeOfDay.fromDateTime(taskController.task.dueDate ?? DateTime.now()), - builder: (context, child) { - ThemeData originalTheme = Theme.of(context); - - // Temporarily set a default theme for the picker - ThemeData pickerTheme = ThemeData.fallback().copyWith( - colorScheme: originalTheme.colorScheme, - ); - return Theme(data: pickerTheme, child: child ?? const SizedBox()); - }, - ).then((time) { - if (date == null && time == null) { - return; - } - date ??= taskController.task.dueDate; - if (date == null) { - return; - } - if (time != null) { - date = DateTime( - date!.year, - date!.month, - date!.day, - time.hour, - time.minute, - ); - } - taskController.changeDueDate(date); - }); - }), - ), + ); + }), + ), + const SizedBox(height: distanceMedium), + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Consumer(builder: (context, taskController, __) { + return _SheetListTile( + icon: Icons.person, + label: context.localization!.assignedTo, + onTap: () => context.pushModal( + context: context, + builder: (BuildContext context) => AssigneeSelectBottomSheet( + users: OrganizationService() + .getMembersByOrganization(CurrentWardService().currentWard!.organizationId), + onChanged: (User? assignee) { + taskController.changeAssignee(assignee); + Navigator.pop(context); + }, + selectedId: taskController.task.assigneeId, ), ), - ], - ), - const SizedBox(height: distanceSmall), + valueWidget: taskController.task.hasAssignee + ? LoadingAndErrorWidget.pulsing( + state: taskController.assignee != null ? LoadingState.loaded : LoadingState.loading, + child: Text( + // Never the case that we display the empty String, but the text is computed + // before being displayed + taskController.assignee?.name ?? "", + style: editableValueTextStyle(context), + ), + ) + : Text( + context.localization!.unassigned, + style: editableValueTextStyle(context), + ), + ); + }), Consumer( - builder: (_, taskController, __) => LoadingAndErrorWidget.pulsing( + builder: (context, taskController, __) => LoadingAndErrorWidget.pulsing( state: taskController.state, child: _SheetListTile( - icon: Icons.lock, - label: context.localization!.visibility, - valueWidget: VisibilitySelect( - isPublicVisible: taskController.task.isPublicVisible, - onChanged: taskController.changeIsPublic, - isCreating: taskController.isCreating, - textStyle: editableValueTextStyle(context), - ), + icon: Icons.access_time, + label: context.localization!.due, + // TODO localization and date formatting here + valueWidget: Builder(builder: (context) { + DateTime? dueDate = taskController.task.dueDate; + if (dueDate != null) { + String date = + "${dueDate.day.toString().padLeft(2, "0")}.${dueDate.month.toString().padLeft(2, "0")}.${dueDate.year.toString().padLeft(4, "0")}"; + String time = + "${dueDate.hour.toString().padLeft(2, "0")}:${dueDate.minute.toString().padLeft(2, "0")}"; + return Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text(time, style: editableValueTextStyle(context)), + Text(date), + ], + ); + } + return Text(context.localization!.none); + }), + onTap: () => showDatePicker( + context: context, + initialDate: taskController.task.dueDate ?? DateTime.now(), + firstDate: DateTime(1960), + lastDate: DateTime.now().add(const Duration(days: 365 * 5)), + builder: (context, child) { + // Overwrite the Theme + ThemeData pickerTheme = + Theme.of(context).copyWith(textButtonTheme: const TextButtonThemeData()); + return Theme(data: pickerTheme, child: child ?? const SizedBox()); + }, + ).then((date) async { + await showTimePicker( + context: context, + initialTime: TimeOfDay.fromDateTime(taskController.task.dueDate ?? DateTime.now()), + builder: (context, child) { + ThemeData originalTheme = Theme.of(context); + + // Temporarily set a default theme for the picker + ThemeData pickerTheme = ThemeData.fallback().copyWith( + colorScheme: originalTheme.colorScheme, + ); + return Theme(data: pickerTheme, child: child ?? const SizedBox()); + }, + ).then((time) { + if (date == null && time == null) { + return; + } + date ??= taskController.task.dueDate; + if (date == null) { + return; + } + if (time != null) { + date = DateTime( + date!.year, + date!.month, + date!.day, + time.hour, + time.minute, + ); + } + taskController.changeDueDate(date); + }); + }), ), ), ), - const SizedBox(height: distanceMedium), - Text( - context.localization!.notes, - style: const TextStyle(fontWeight: FontWeight.bold, fontSize: 16), + ], + ), + const SizedBox(height: distanceSmall), + Consumer( + builder: (_, taskController, __) => LoadingAndErrorWidget.pulsing( + state: taskController.state, + child: _SheetListTile( + icon: Icons.lock, + label: context.localization!.visibility, + valueWidget: VisibilitySelect( + isPublicVisible: taskController.task.isPublicVisible, + onChanged: taskController.changeIsPublic, + isCreating: taskController.isCreating, + textStyle: editableValueTextStyle(context), + ), ), - const SizedBox(height: distanceTiny), - Consumer( - builder: (_, taskController, __) => LoadingAndErrorWidget( - state: taskController.state, - loadingWidget: PulsingContainer( - width: MediaQuery.of(context).size.width, - height: 25 * 6, // 25px per line - ), - errorWidget: PulsingContainer( - width: MediaQuery.of(context).size.width, - height: 25 * 6, - // 25px per line - maxOpacity: 1, - minOpacity: 1, - color: negativeColor, - ), - child: TextFormFieldWithTimer( - initialValue: taskController.task.notes, - onUpdate: taskController.changeNotes, - maxLines: 6, - decoration: InputDecoration( - contentPadding: const EdgeInsets.all(paddingMedium), - border: const OutlineInputBorder( - borderSide: BorderSide( - width: 1.0, - ), - ), - hintText: context.localization!.yourNotes, + ), + ), + const SizedBox(height: distanceMedium), + Text( + context.localization!.notes, + style: const TextStyle(fontWeight: FontWeight.bold, fontSize: 16), + ), + const SizedBox(height: distanceTiny), + Consumer( + builder: (_, taskController, __) => LoadingAndErrorWidget( + state: taskController.state, + loadingWidget: PulsingContainer( + width: MediaQuery.of(context).size.width, + height: 25 * 6, // 25px per line + ), + errorWidget: PulsingContainer( + width: MediaQuery.of(context).size.width, + height: 25 * 6, + // 25px per line + maxOpacity: 1, + minOpacity: 1, + color: negativeColor, + ), + child: TextFormFieldWithTimer( + initialValue: taskController.task.notes, + onUpdate: taskController.changeNotes, + maxLines: 6, + decoration: InputDecoration( + contentPadding: const EdgeInsets.all(paddingMedium), + border: const OutlineInputBorder( + borderSide: BorderSide( + width: 1.0, ), ), + hintText: context.localization!.yourNotes, ), ), - const SizedBox(height: distanceBig), - // TODO add callback here for task creation to update the Task accordingly - Consumer( - builder: (_, taskController, __) => LoadingAndErrorWidget.pulsing( - state: taskController.state, - child: SubtaskList( - taskId: taskController.task.id, - subtasks: taskController.task.subtasks, - onChange: (subtasks) { - if (taskController.task.isCreating) { - taskController.task.subtasks = subtasks; - } - }, - ), - ), + ), + ), + const SizedBox(height: distanceBig), + // TODO add callback here for task creation to update the Task accordingly + Consumer( + builder: (_, taskController, __) => LoadingAndErrorWidget.pulsing( + state: taskController.state, + child: SubtaskList( + taskId: taskController.task.id, + subtasks: taskController.task.subtasks, + onChange: (subtasks) { + if (taskController.task.isCreating) { + taskController.task.subtasks = subtasks; + } + }, ), - ], - )), + ), + ), + ], + ), ), ), ); diff --git a/apps/tasks/lib/components/user_bottom_sheet.dart b/apps/tasks/lib/components/user_bottom_sheet.dart index 4b18600a..4823ac6f 100644 --- a/apps/tasks/lib/components/user_bottom_sheet.dart +++ b/apps/tasks/lib/components/user_bottom_sheet.dart @@ -1,6 +1,7 @@ import 'dart:math'; import 'package:flutter/material.dart'; import 'package:helpwave_localization/localization.dart'; +import 'package:helpwave_service/user.dart'; import 'package:helpwave_theme/constants.dart'; import 'package:helpwave_widget/bottom_sheets.dart'; import 'package:helpwave_widget/loading.dart'; @@ -8,50 +9,42 @@ import 'package:provider/provider.dart'; import 'package:helpwave_service/tasks.dart'; import 'package:helpwave_service/auth.dart'; import 'package:tasks/screens/login_screen.dart'; +import 'package:helpwave_widget/widgets.dart'; +import 'package:tasks/screens/settings_screen.dart'; -/// A [BottomSheet] for showing the [User]s information -class UserBottomSheet extends StatefulWidget { - const UserBottomSheet({super.key}); +class UserBottomSheetPageBuilder with BottomSheetPageBuilder { @override - State createState() => _UserBottomSheetState(); -} + BottomSheetHeader? headerBuilder(BuildContext context, NestedBottomSheetNavigationController controller) { + return BottomSheetHeader( + trailing: BottomSheetAction( + icon: Icons.settings, + onPressed: () => controller.push(SettingsBottomSheetPageBuilder()), + ), + ); + } -class _UserBottomSheetState extends State { @override - Widget build(BuildContext context) { + Widget build(BuildContext context, NestedBottomSheetNavigationController controller) { final double width = MediaQuery.of(context).size.width; - return BottomSheetBase( - onClosing: () {}, - titleText: context.localization!.profile, - builder: (BuildContext ctx) => Column( + return Flexible( + child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ Padding( padding: const EdgeInsets.all(paddingSmall).copyWith(top: paddingMedium), - child: CircleAvatar( - radius: iconSizeMedium, - child: Container( - decoration: const BoxDecoration( - shape: BoxShape.circle, - gradient: LinearGradient( - begin: Alignment.topLeft, - end: Alignment(0.8, 1), - colors: [ - Color(0xff1f005c), - Color(0xff5b0060), - Color(0xff870160), - Color(0xffac255e), - Color(0xffca485c), - Color(0xffe16b5c), - Color(0xfff39060), - Color(0xffffb56b), - ], - tileMode: TileMode.mirror, - ), - ), - ), + child: LoadingFutureBuilder( + data: UserService().getSelf(), + thenWidgetBuilder: (context, data) { + return CircleAvatar( + backgroundColor: Colors.transparent, + radius: iconSizeMedium, + foregroundImage: NetworkImage(data.profileUrl.toString()), + ); + }, + loadingWidget: const FallbackAvatar(size: iconSizeMedium), + errorWidget: const FallbackAvatar(size: iconSizeMedium), ), ), Consumer(builder: (context, userSessionController, _) { @@ -60,7 +53,6 @@ class _UserBottomSheetState extends State { style: const TextStyle(fontSize: fontSizeBig), ); }), - // TODO consider a loading widget here Consumer( builder: (context, currentWardController, __) => Text( currentWardController.currentWard?.organizationName ?? context.localization!.loading, @@ -147,6 +139,7 @@ class _UserBottomSheetState extends State { }); }), ), + const Spacer(), Padding( padding: const EdgeInsets.only(bottom: distanceMedium), child: Consumer(builder: (context, currentWardService, _) { diff --git a/apps/tasks/lib/components/user_header.dart b/apps/tasks/lib/components/user_header.dart index b66cac3a..5ea59d78 100644 --- a/apps/tasks/lib/components/user_header.dart +++ b/apps/tasks/lib/components/user_header.dart @@ -1,126 +1,78 @@ import 'package:flutter/material.dart'; import 'package:helpwave_service/auth.dart'; import 'package:helpwave_service/tasks.dart'; +import 'package:helpwave_service/user.dart'; import 'package:helpwave_theme/constants.dart'; +import 'package:helpwave_theme/util.dart'; import 'package:helpwave_widget/bottom_sheets.dart'; +import 'package:helpwave_widget/loading.dart'; +import 'package:helpwave_widget/widgets.dart'; import 'package:provider/provider.dart'; import 'package:tasks/components/user_bottom_sheet.dart'; -import 'package:tasks/screens/settings_screen.dart'; /// A [AppBar] for displaying the current [User], [Organization] and [Ward] class UserHeader extends StatelessWidget implements PreferredSizeWidget { - // TODO fetch users by with grpc const UserHeader({Key? key}) : super(key: key); @override Widget build(BuildContext context) { - return Padding( - padding: const EdgeInsets.all(paddingSmall), - child: AppBar( - centerTitle: false, - titleSpacing: paddingSmall, - leadingWidth: iconSizeSmall, - leading: GestureDetector( - onTap: () { - context.pushModal( - context: context, - builder: (context) => const UserBottomSheet(), - ); - }, - child: CircleAvatar( - backgroundColor: Colors.transparent, - radius: iconSizeSmall / 2, - child: Container( + return Container( + color: context.theme.appBarTheme.backgroundColor, + child: Consumer(builder: (context, userSession, __) { + return SafeArea( + child: ListTile( + leading: SizedBox( width: iconSizeSmall, height: iconSizeSmall, - decoration: const BoxDecoration( - shape: BoxShape.circle, - gradient: LinearGradient( - begin: Alignment.topLeft, - end: Alignment(0.8, 1), - colors: [ - Color(0xff1f005c), - Color(0xff5b0060), - Color(0xff870160), - Color(0xffac255e), - Color(0xffca485c), - Color(0xffe16b5c), - Color(0xfff39060), - Color(0xffffb56b), - ], - tileMode: TileMode.mirror, - ), - ), - ), - ), - ), - title: GestureDetector( - onTap: () { - context.pushModal( - context: context, - builder: (context) => const UserBottomSheet(), - ); - }, - child: Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - // TODO get information somewhere - Consumer( - builder: (context, userSession, __) { - return Text( - userSession.identity!.name, - style: const TextStyle(fontSize: 16), + child: LoadingFutureBuilder( + data: UserService().getSelf(), + thenWidgetBuilder: (context, data) { + return CircleAvatar( + backgroundColor: Colors.transparent, + radius: iconSizeSmall / 2, + foregroundImage: NetworkImage(data.profileUrl.toString()), ); - } + }, + loadingWidget: const FallbackAvatar(), + errorWidget: const FallbackAvatar(), ), - - // TODO maybe show something for loading - Consumer( - builder: (context, currentWardController, __) => Row( - children: [ - Text( - "${currentWardController.currentWard?.organization.shortName ?? ""} - ", - style: const TextStyle( - color: Colors.grey, - fontSize: 14, - ), + ), + onTap: () { + context.pushModal( + context: context, + builder: (context) => NestedBottomSheetNavigator(initialPageBuilder: UserBottomSheetPageBuilder()), + ); + }, + title: Text( + userSession.identity!.name, + style: const TextStyle(fontSize: 16), + ), + subtitle: Consumer( + builder: (context, currentWardController, __) => Row( + children: [ + Text( + "${currentWardController.currentWard?.organization.shortName ?? ""} - ", + style: TextStyle( + color: Theme.of(context).colorScheme.onBackground.withOpacity(0.7), + fontSize: 14, ), - Text( - currentWardController.currentWard?.wardName ?? "", - overflow: TextOverflow.fade, - style: const TextStyle( - fontWeight: FontWeight.bold, - fontSize: 14, - ), + ), + Text( + currentWardController.currentWard?.wardName ?? "", + overflow: TextOverflow.fade, + style: const TextStyle( + fontWeight: FontWeight.bold, + fontSize: 14, ), - ], - ), + ), + ], ), - ], + ), + trailing: const Icon(Icons.expand_more), ), - ), - actions: [ - IconButton( - padding: EdgeInsets.zero, - splashRadius: iconSizeSmall / 2, - constraints: const BoxConstraints(maxWidth: iconSizeSmall, maxHeight: iconSizeSmall), - iconSize: iconSizeSmall, - onPressed: () { - Navigator.push( - context, - MaterialPageRoute( - builder: (context) => const SettingsScreen(), - ), - ); - }, - icon: const Icon(Icons.settings), - ) - ], - ), + ); + }), ); - - } @override diff --git a/apps/tasks/lib/components/visibility_select.dart b/apps/tasks/lib/components/visibility_select.dart index 2c458b20..2ae8ca04 100644 --- a/apps/tasks/lib/components/visibility_select.dart +++ b/apps/tasks/lib/components/visibility_select.dart @@ -25,9 +25,11 @@ class _VisibilityBottomSheet extends StatelessWidget { top: paddingMedium, bottom: paddingBig, ), - titleText: context.localization!.visibility, + header: BottomSheetHeader( + titleText: context.localization!.visibility, + ), builder: (context) { - return Column( + return ListView( children: [ const SizedBox(height: distanceSmall), GestureDetector( diff --git a/apps/tasks/lib/main.dart b/apps/tasks/lib/main.dart index 65b37680..0350f9c2 100644 --- a/apps/tasks/lib/main.dart +++ b/apps/tasks/lib/main.dart @@ -57,9 +57,7 @@ class MyApp extends StatelessWidget { ], supportedLocales: getSupportedLocals(), locale: Locale(languageNotifier.language), - home: const Scaffold( - body: SafeArea(child: LoginScreen()), - ), + home: const LoginScreen(), ); }), ); diff --git a/apps/tasks/lib/screens/main_screen.dart b/apps/tasks/lib/screens/main_screen.dart index 60797a42..d39b2564 100644 --- a/apps/tasks/lib/screens/main_screen.dart +++ b/apps/tasks/lib/screens/main_screen.dart @@ -45,7 +45,7 @@ class _MainScreenState extends State { } return Scaffold( appBar: const UserHeader(), - body: [const MyTasksScreen(), const SizedBox(), const PatientScreen()][index], + body: SafeArea(child: [const MyTasksScreen(), const SizedBox(), const PatientScreen()][index]), floatingActionButtonLocation: FloatingActionButtonLocation.centerFloat, floatingActionButton: PopInAndOutAnimator( visible: isShowingActionButton, diff --git a/apps/tasks/lib/screens/settings_screen.dart b/apps/tasks/lib/screens/settings_screen.dart index e943d53b..9f31e582 100644 --- a/apps/tasks/lib/screens/settings_screen.dart +++ b/apps/tasks/lib/screens/settings_screen.dart @@ -4,18 +4,14 @@ import 'package:helpwave_localization/localization_model.dart'; import 'package:helpwave_service/auth.dart'; import 'package:helpwave_theme/constants.dart'; import 'package:helpwave_theme/theme.dart'; +import 'package:helpwave_widget/bottom_sheets.dart'; import 'package:provider/provider.dart'; import 'package:tasks/screens/login_screen.dart'; /// Screen for settings and other app options -class SettingsScreen extends StatefulWidget { +class SettingsScreen extends StatelessWidget { const SettingsScreen({super.key}); - @override - State createState() => _SettingsScreenState(); -} - -class _SettingsScreenState extends State { @override Widget build(BuildContext context) { return Scaffold( @@ -147,3 +143,129 @@ class _SettingsScreenState extends State { ); } } + +class SettingsBottomSheetPageBuilder with BottomSheetPageBuilder { + @override + Widget build(BuildContext context, NestedBottomSheetNavigationController controller) { + return Flexible( + child: ListView( + children: ListTile.divideTiles( + context: context, + tiles: [ + ListTile( + leading: const Icon(Icons.brightness_medium), + title: Text(context.localization!.darkMode), + trailing: Consumer( + builder: (_, ThemeModel themeNotifier, __) { + return PopupMenuButton( + initialValue: themeNotifier.themeMode, + position: PopupMenuPosition.under, + itemBuilder: (context) => [ + PopupMenuItem(value: ThemeMode.dark, child: Text(context.localization!.darkMode)), + PopupMenuItem(value: ThemeMode.light, child: Text(context.localization!.lightMode)), + PopupMenuItem(value: ThemeMode.system, child: Text(context.localization!.system)), + ], + onSelected: (value) { + if (value == ThemeMode.system) { + themeNotifier.isDark = null; + } else { + themeNotifier.isDark = value == ThemeMode.dark; + } + }, + child: Material( + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Text( + { + ThemeMode.dark: context.localization!.darkMode, + ThemeMode.light: context.localization!.lightMode, + ThemeMode.system: context.localization!.system, + }[themeNotifier.themeMode]!, + style: const TextStyle( + fontSize: 14, + fontWeight: FontWeight.w800, + )), + const SizedBox( + width: distanceTiny, + ), + const Icon( + Icons.expand_more_rounded, + size: iconSizeTiny, + ), + ], + ), + ), + ); + }, + ), + ), + Consumer( + builder: (context, languageModel, child) { + return ListTile( + leading: const Icon(Icons.language), + title: Text(context.localization!.language), + trailing: PopupMenuButton( + position: PopupMenuPosition.under, + initialValue: languageModel.local, + onSelected: (value) { + languageModel.setLanguage(value); + }, + itemBuilder: (BuildContext context) => getSupportedLocalsWithName() + .map((local) => PopupMenuItem( + value: local.local, + child: Text( + local.name, + ), + )) + .toList(), + child: Material( + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Text(languageModel.name, + style: const TextStyle( + fontSize: 14, + fontWeight: FontWeight.w800, + )), + const SizedBox( + width: distanceTiny, + ), + const Icon( + Icons.expand_more_rounded, + size: iconSizeTiny, + ), + ], + ), + ), + ), + ); + }, + ), + ListTile( + leading: const Icon(Icons.info_outline), + title: Text(context.localization!.licenses), + trailing: const Icon(Icons.arrow_forward), + onTap: () => {showLicensePage(context: context)}, + ), + Consumer( + builder: (context, currentWardService, _) { + return ListTile( + leading: const Icon(Icons.logout), + title: Text(context.localization!.logout), + onTap: () { + UserSessionService().logout(); + currentWardService.clear(); + Navigator.of(context).pushReplacement( + MaterialPageRoute(builder: (_) => const LoginScreen()), + ); + }, + ); + }, + ), + ], + ).toList(), + ), + ); + } +} diff --git a/packages/helpwave_service/lib/src/api/user/services/user_service.dart b/packages/helpwave_service/lib/src/api/user/services/user_service.dart index 943bba4a..16680bdd 100644 --- a/packages/helpwave_service/lib/src/api/user/services/user_service.dart +++ b/packages/helpwave_service/lib/src/api/user/services/user_service.dart @@ -32,4 +32,24 @@ class UserService { profileUrl: Uri.parse(response.avatarUrl), ); } + + Future getSelf() async { + ReadSelfRequest request = ReadSelfRequest(); + ReadSelfResponse response = await userService.readSelf( + request, + options: CallOptions( + metadata: UserAPIServiceClients().getMetaData( + organizationId: AuthenticationUtility.fallbackOrganizationId, + ), + ), + ); + + return User( + id: response.id, + name: response.name, + nickName: response.nickname, + email: "no-email", // TODO replace this + profileUrl: Uri.parse(response.avatarUrl), + ); + } } diff --git a/packages/helpwave_theme/lib/src/constants.dart b/packages/helpwave_theme/lib/src/constants.dart index 6d0e95c0..291df3aa 100644 --- a/packages/helpwave_theme/lib/src/constants.dart +++ b/packages/helpwave_theme/lib/src/constants.dart @@ -130,6 +130,8 @@ const chipTheme = ChipThemeData( pressElevation: 4, ); +const AppBarTheme sharedAppBarTheme = AppBarTheme(centerTitle: true); + /// TextStyles TextStyle editableValueTextStyle(BuildContext context) => TextStyle(color: Theme.of(context).colorScheme.secondary, fontSize: 16, fontWeight: FontWeight.bold); diff --git a/packages/helpwave_theme/lib/src/theme/dark_theme.dart b/packages/helpwave_theme/lib/src/theme/dark_theme.dart index 5002ff66..8953e56f 100644 --- a/packages/helpwave_theme/lib/src/theme/dark_theme.dart +++ b/packages/helpwave_theme/lib/src/theme/dark_theme.dart @@ -81,4 +81,10 @@ ThemeData darkTheme = makeTheme( // additional brightness: Brightness.dark, + + // flutter themes + appBarTheme: sharedAppBarTheme.copyWith( + backgroundColor: const Color.fromARGB(255, 15, 15, 15), + foregroundColor: Colors.white, + ) ); diff --git a/packages/helpwave_theme/lib/src/theme/light_theme.dart b/packages/helpwave_theme/lib/src/theme/light_theme.dart index 72e708cf..881e600d 100644 --- a/packages/helpwave_theme/lib/src/theme/light_theme.dart +++ b/packages/helpwave_theme/lib/src/theme/light_theme.dart @@ -10,7 +10,7 @@ const onSecondaryColor = Color.fromARGB(255, 255, 255, 255); const tertiary = Color.fromARGB(255, 180, 180, 180); const onTertiary = Color.fromARGB(255, 0, 0, 0); -const backgroundColor = Color.fromARGB(255, 230, 230, 230); +const backgroundColor = Color.fromARGB(255, 242, 242, 242); const onBackgroundColor = Color.fromARGB(255, 0, 0, 0); const surface = Color.fromARGB(255, 220, 220, 220); @@ -81,4 +81,10 @@ ThemeData lightTheme = makeTheme( // additional brightness: Brightness.dark, + + // flutter themes + appBarTheme: sharedAppBarTheme.copyWith( + backgroundColor: Colors.white, + foregroundColor: Colors.black, + ) ); diff --git a/packages/helpwave_theme/lib/src/theme/theme.dart b/packages/helpwave_theme/lib/src/theme/theme.dart index a9a27fd7..6c559af3 100644 --- a/packages/helpwave_theme/lib/src/theme/theme.dart +++ b/packages/helpwave_theme/lib/src/theme/theme.dart @@ -48,6 +48,9 @@ ThemeData makeTheme({ // additional parameters required Brightness brightness, + + // Flutter Themes + AppBarTheme appBarTheme = sharedAppBarTheme, }) { return ThemeData( useMaterial3: true, @@ -104,12 +107,7 @@ ThemeData makeTheme({ listTileTheme: ListTileThemeData( iconColor: focusedColor, ), - appBarTheme: AppBarTheme( - centerTitle: true, - foregroundColor: primaryColor, - backgroundColor: Colors.transparent, - shadowColor: Colors.transparent, - ), + appBarTheme: appBarTheme, elevatedButtonTheme: ElevatedButtonThemeData( style: buttonStyleSmall.copyWith( backgroundColor: resolveByStates( diff --git a/packages/helpwave_widget/lib/bottom_sheets.dart b/packages/helpwave_widget/lib/bottom_sheets.dart index bd339f47..4625700e 100644 --- a/packages/helpwave_widget/lib/bottom_sheets.dart +++ b/packages/helpwave_widget/lib/bottom_sheets.dart @@ -1 +1 @@ -export 'package:helpwave_widget/src/bottom_sheets/bottom_sheet_base.dart'; +export 'package:helpwave_widget/src/bottom_sheets/index.dart'; diff --git a/packages/helpwave_widget/lib/src/bottom_sheets/bottom_sheet_base.dart b/packages/helpwave_widget/lib/src/bottom_sheets/bottom_sheet_base.dart index 67e6e62d..451e5735 100644 --- a/packages/helpwave_widget/lib/src/bottom_sheets/bottom_sheet_base.dart +++ b/packages/helpwave_widget/lib/src/bottom_sheets/bottom_sheet_base.dart @@ -6,13 +6,14 @@ extension PushModalContextExtension on BuildContext { required BuildContext context, required Widget Function(BuildContext context) builder, Duration animationDuration = const Duration(milliseconds: 500), + Duration reverseDuration = const Duration(milliseconds: 250), }) async { T? value = await Navigator.of(context).push( PageRouteBuilder( - barrierColor: Colors.black.withOpacity(0.3), + barrierColor: Colors.black.withOpacity(0.4), barrierDismissible: true, transitionDuration: animationDuration, - reverseTransitionDuration: animationDuration, + reverseTransitionDuration: reverseDuration, opaque: false, // Set to false to make the route semi-transparent pageBuilder: (BuildContext context, _, __) { @@ -92,7 +93,7 @@ class _ModalWrapperState extends State<_ModalWrapper> with TickerProviderStateMi if (newOffset < 0) { newOffset = 0; } - + offset.value = newOffset; touchPositionY = details.globalPosition.dy; @@ -111,18 +112,142 @@ class _ModalWrapperState extends State<_ModalWrapper> with TickerProviderStateMi child: Align( alignment: Alignment.bottomCenter, child: ValueListenableBuilder( - valueListenable: offset, - child: Material( - color: Colors.transparent, - child: widget.builder(context), - ), - builder: (context, offsetValue, child) { - return Transform.translate( + valueListenable: offset, + child: Material( + color: Colors.transparent, + child: widget.builder(context), + ), + builder: (context, offsetValue, child) { + return Transform.translate( offset: Offset(0, offsetValue), - child: child, - ); - }, + child: child, + ); + }, + ), + ), + ); + } +} + +class BottomSheetAction { + final IconData icon; + final Function() onPressed; + + BottomSheetAction({required this.icon, required this.onPressed}); +} + +class BottomSheetHeader extends StatelessWidget { + /// A [BottomSheetAction] leading before the [title] + /// + /// Defaults to a closing button + final BottomSheetAction? leading; + + /// A [BottomSheetAction] trailing after the [title] + final BottomSheetAction? trailing; + + /// The title of the [BottomSheetHeader] displayed in the center + /// + /// Overwrites [titleText] + final Widget? title; + + /// The title text of the [BottomSheetHeader] displayed in the center + /// + /// Overwritten by [title] + final String? titleText; + + /// Whether the drag handler widget should be shown + final bool isShowingDragHandler; + + /// An additional padding for the [BottomSheetHeader] + /// + /// Be aware that the [BottomSheetBase] **already provides a padding** to all sides. + /// + /// You most likely want to change the bottom padding to create spacing between [BottomSheetHeader] and content of + /// the [BottomSheetBase]. + final EdgeInsets padding; + + const BottomSheetHeader({ + super.key, + this.leading, + this.trailing, + this.title, + this.titleText, + this.isShowingDragHandler = false, + this.padding = const EdgeInsets.only(bottom: paddingSmall), + }); + + @override + Widget build(BuildContext context) { + BottomSheetAction usedLeading = + leading ?? BottomSheetAction(icon: Icons.close_rounded, onPressed: () => Navigator.maybePop(context)); + + const double iconSize = iconSizeSmall; + + return Padding( + padding: padding, + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Visibility( + visible: isShowingDragHandler, + child: Padding( + padding: const EdgeInsets.only(bottom: paddingSmall), + child: Center( + child: Container( + height: 5, + width: 30, + decoration: BoxDecoration( + color: Theme.of(context).colorScheme.onBackground.withOpacity(0.8), + borderRadius: BorderRadius.circular(5), + ), + ), + ), + ), ), + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + SizedBox( + width: iconSize, + height: iconSize, + child: IconButton( + padding: EdgeInsets.zero, + iconSize: iconSize, + onPressed: usedLeading.onPressed, + icon: Icon(usedLeading.icon), + ), + ), + Flexible( + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: paddingSmall), + child: title ?? + Text( + titleText ?? "", + style: const TextStyle( + fontWeight: FontWeight.bold, + fontSize: iconSizeTiny, + fontFamily: "SpaceGrotesk", + overflow: TextOverflow.ellipsis, + ), + ), + ), + ), + SizedBox( + width: iconSize, + height: iconSize, + child: trailing != null + ? IconButton( + padding: EdgeInsets.zero, + iconSize: iconSize, + onPressed: trailing!.onPressed, + icon: Icon(trailing!.icon), + ) + : null, + ), + ], + ), + ], ), ); } @@ -136,32 +261,27 @@ class BottomSheetBase extends StatefulWidget { /// The builder function call to build the content of the [BottomSheetBase] final Widget Function(BuildContext context) builder; - /// The title of the titlebar - /// - /// Overwrites [titleText] - final Widget? title; - - /// The title text of the titlebar + /// A header [Widget] above the builder content /// - /// Overwritten by [title] - final String titleText; + /// Defaults to the [BottomSheetHeader] + final Widget header; /// The bottom [Widget] below the [builder] - /// - /// Overwrites [titleText] final Widget? bottomWidget; /// The [Padding] of the builder [Widget] final EdgeInsetsGeometry padding; + final MainAxisSize mainAxisSize; + const BottomSheetBase({ super.key, required this.onClosing, required this.builder, - this.title, - this.titleText = "", this.padding = const EdgeInsets.all(paddingMedium), this.bottomWidget, + this.header = const BottomSheetHeader(), + this.mainAxisSize = MainAxisSize.min, }); @override @@ -171,69 +291,26 @@ class BottomSheetBase extends StatefulWidget { class _BottomSheetBase extends State with TickerProviderStateMixin { @override Widget build(BuildContext context) { - return BottomSheet( - animationController: BottomSheet.createAnimationController(this), - enableDrag: false, - onClosing: widget.onClosing, - builder: (context) { - return Padding( - padding: widget.padding, - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - Padding( - padding: const EdgeInsets.only(bottom: paddingSmall), - child: Center( - child: Container( - height: 5, - width: 30, - decoration: BoxDecoration( - color: Theme.of(context).colorScheme.onBackground.withOpacity(0.8), - borderRadius: BorderRadius.circular(5), - ), - ), - ), - ), - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - SizedBox( - width: iconSizeTiny, - height: iconSizeTiny, - child: IconButton( - padding: EdgeInsets.zero, - iconSize: iconSizeTiny, - onPressed: () => Navigator.maybePop(context), - icon: const Icon(Icons.close_rounded), - ), - ), - Flexible( - child: Padding( - padding: const EdgeInsets.symmetric(horizontal: paddingSmall), - child: widget.title ?? - Text( - widget.titleText, - style: const TextStyle( - fontWeight: FontWeight.bold, - fontSize: iconSizeTiny, - fontFamily: "SpaceGrotesk", - overflow: TextOverflow.ellipsis, - ), - ), - ), - ), - const SizedBox(width: iconSizeTiny), - ], - ), - SingleChildScrollView( - child: widget.builder(context), - ), - widget.bottomWidget ?? const SizedBox(), - ], - ), - ); - }, + return SafeArea( + child: BottomSheet( + constraints: BoxConstraints(maxHeight: MediaQuery.of(context).size.height * 0.85), + animationController: BottomSheet.createAnimationController(this), + enableDrag: false, + onClosing: widget.onClosing, + builder: (context) { + return Padding( + padding: widget.padding, + child: Column( + mainAxisSize: widget.mainAxisSize, + children: [ + widget.header, + widget.builder(context), + widget.bottomWidget ?? const SizedBox(), + ], + ), + ); + }, + ), ); } } diff --git a/packages/helpwave_widget/lib/src/bottom_sheets/index.dart b/packages/helpwave_widget/lib/src/bottom_sheets/index.dart new file mode 100644 index 00000000..e1423f66 --- /dev/null +++ b/packages/helpwave_widget/lib/src/bottom_sheets/index.dart @@ -0,0 +1,2 @@ +export 'bottom_sheet_base.dart'; +export 'nested_bottom_sheet_navigation.dart'; diff --git a/packages/helpwave_widget/lib/src/bottom_sheets/nested_bottom_sheet_navigation.dart b/packages/helpwave_widget/lib/src/bottom_sheets/nested_bottom_sheet_navigation.dart new file mode 100644 index 00000000..af15be7e --- /dev/null +++ b/packages/helpwave_widget/lib/src/bottom_sheets/nested_bottom_sheet_navigation.dart @@ -0,0 +1,102 @@ +import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; +import 'package:helpwave_widget/src/bottom_sheets/bottom_sheet_base.dart'; + +mixin BottomSheetPageBuilder { + /// The [BottomSheetHeader] of the [BottomSheetPageBuilder] + /// + /// The leading icon will always be ignored to + BottomSheetHeader? headerBuilder(BuildContext context, NestedBottomSheetNavigationController controller) { + return null; + } + + /// The [BottomSheetBase] bottom widget to display for the [BottomSheetPageBuilder] + Widget? bottomWidgetBuilder(BuildContext context, NestedBottomSheetNavigationController controller) { + return null; + } + + /// The builder function call to build the content of the [BottomSheetBase] + Widget build(BuildContext context, NestedBottomSheetNavigationController controller); +} + +class NestedBottomSheetNavigationController extends ChangeNotifier { + List stack = []; + + BottomSheetPageBuilder get currentPage => stack.last; + + bool get canPop => stack.length > 1; + + bool get isInitialPage => stack.length == 1; + + NestedBottomSheetNavigationController({ + required BottomSheetPageBuilder initialPageBuilder, + }) { + stack.add(initialPageBuilder); + } + + void push(BottomSheetPageBuilder page) { + stack.add(page); + notifyListeners(); + } + + void pop() { + stack.removeLast(); + notifyListeners(); + } +} + +class NestedBottomSheetNavigator extends StatelessWidget { + final BottomSheetPageBuilder initialPageBuilder; + + const NestedBottomSheetNavigator({super.key, required this.initialPageBuilder}); + + @override + Widget build(BuildContext context) { + return ChangeNotifierProvider( + create: (context) => NestedBottomSheetNavigationController( + initialPageBuilder: initialPageBuilder, + ), + child: Consumer( + builder: (context, navigationController, child) { + BottomSheetPageBuilder page = navigationController.currentPage; + + BottomSheetHeader? computedHeader = page.headerBuilder(context, navigationController); + + BottomSheetHeader header = BottomSheetHeader( + title: computedHeader?.title, + titleText: computedHeader?.titleText, + isShowingDragHandler: computedHeader?.isShowingDragHandler ?? false, + trailing: computedHeader?.trailing, + leading: BottomSheetAction( + icon: navigationController.isInitialPage ? Icons.close : Icons.chevron_left_rounded, + onPressed: () { + if (navigationController.canPop) { + navigationController.pop(); + } else { + Navigator.pop(context); + } + }, + ), + ); + + return PopScope( + // If the navigation controller cannot pop to another bottom sheet, a normal pop is correct + canPop: !navigationController.canPop, + onPopInvoked: (didPop) { + if(navigationController.canPop){ + navigationController.pop(); + } + }, + child: BottomSheetBase( + onClosing: () {}, + builder: (context) => page.build(context, navigationController), + header: header, + bottomWidget: page.bottomWidgetBuilder(context, navigationController), + mainAxisSize: MainAxisSize.max, + ), + ); + }, + ), + ); + } +} diff --git a/packages/helpwave_widget/lib/src/widgets/fallback_avatar.dart b/packages/helpwave_widget/lib/src/widgets/fallback_avatar.dart new file mode 100644 index 00000000..678fcd1e --- /dev/null +++ b/packages/helpwave_widget/lib/src/widgets/fallback_avatar.dart @@ -0,0 +1,40 @@ +import 'package:flutter/material.dart'; +import 'package:helpwave_theme/constants.dart'; + +/// A [CircleAvatar] with a dummy image that can be sized as needed +class FallbackAvatar extends StatelessWidget{ + /// The size of the [CircleAvatar] + final double size; + + const FallbackAvatar({super.key, this.size = iconSizeSmall}); + + @override + Widget build(BuildContext context) { + return CircleAvatar( + backgroundColor: Colors.transparent, + radius: iconSizeSmall / 2, + child: Container( + width: iconSizeSmall, + height: iconSizeSmall, + decoration: const BoxDecoration( + shape: BoxShape.circle, + gradient: LinearGradient( + begin: Alignment.topLeft, + end: Alignment(0.8, 1), + colors: [ + Color(0xff1f005c), + Color(0xff5b0060), + Color(0xff870160), + Color(0xffac255e), + Color(0xffca485c), + Color(0xffe16b5c), + Color(0xfff39060), + Color(0xffffb56b), + ], + tileMode: TileMode.mirror, + ), + ), + ), + ); + } +} diff --git a/packages/helpwave_widget/lib/src/widgets/index.dart b/packages/helpwave_widget/lib/src/widgets/index.dart new file mode 100644 index 00000000..5cbf43ab --- /dev/null +++ b/packages/helpwave_widget/lib/src/widgets/index.dart @@ -0,0 +1,2 @@ +export 'fallback_avatar.dart'; +export 'list_tile_card.dart'; diff --git a/packages/helpwave_widget/lib/src/widgets/widgets.dart b/packages/helpwave_widget/lib/src/widgets/list_tile_card.dart similarity index 100% rename from packages/helpwave_widget/lib/src/widgets/widgets.dart rename to packages/helpwave_widget/lib/src/widgets/list_tile_card.dart diff --git a/packages/helpwave_widget/lib/widgets.dart b/packages/helpwave_widget/lib/widgets.dart index df63dd05..8dfcb136 100644 --- a/packages/helpwave_widget/lib/widgets.dart +++ b/packages/helpwave_widget/lib/widgets.dart @@ -1 +1 @@ -export 'package:helpwave_widget/src/widgets/widgets.dart'; +export 'package:helpwave_widget/src/widgets/index.dart'; diff --git a/packages/helpwave_widget/pubspec.yaml b/packages/helpwave_widget/pubspec.yaml index bf60e6d9..22e6edf3 100644 --- a/packages/helpwave_widget/pubspec.yaml +++ b/packages/helpwave_widget/pubspec.yaml @@ -18,6 +18,7 @@ dependencies: path: "../helpwave_localization" helpwave_util: path: "../helpwave_util" + provider: ^6.0.3 dev_dependencies: flutter_test: From a1d8dc975024a68b9ec1c940b60be33fdc0675fd Mon Sep 17 00:00:00 2001 From: Felix Thape Date: Wed, 18 Sep 2024 18:46:48 +0200 Subject: [PATCH 08/36] feat: make navigator generic --- .../lib/components/user_bottom_sheet.dart | 5 +- apps/tasks/lib/components/user_header.dart | 2 +- .../patient_screen.dart | 2 +- apps/tasks/lib/screens/settings_screen.dart | 13 +-- packages/helpwave_widget/lib/navigation.dart | 1 + .../bottom_sheets/bottom_sheet_navigator.dart | 46 ++++++++ .../bottom_sheet_page_builder.dart | 20 ++++ .../lib/src/bottom_sheets/index.dart | 3 +- .../nested_bottom_sheet_navigation.dart | 102 ------------------ .../lib/src/navigation/index.dart | 2 + .../src/navigation/navigation_controller.dart | 27 +++++ .../lib/src/navigation/simple_navigator.dart | 35 ++++++ 12 files changed, 145 insertions(+), 113 deletions(-) create mode 100644 packages/helpwave_widget/lib/navigation.dart create mode 100644 packages/helpwave_widget/lib/src/bottom_sheets/bottom_sheet_navigator.dart create mode 100644 packages/helpwave_widget/lib/src/bottom_sheets/bottom_sheet_page_builder.dart delete mode 100644 packages/helpwave_widget/lib/src/bottom_sheets/nested_bottom_sheet_navigation.dart create mode 100644 packages/helpwave_widget/lib/src/navigation/index.dart create mode 100644 packages/helpwave_widget/lib/src/navigation/navigation_controller.dart create mode 100644 packages/helpwave_widget/lib/src/navigation/simple_navigator.dart diff --git a/apps/tasks/lib/components/user_bottom_sheet.dart b/apps/tasks/lib/components/user_bottom_sheet.dart index 4823ac6f..74a7dfbd 100644 --- a/apps/tasks/lib/components/user_bottom_sheet.dart +++ b/apps/tasks/lib/components/user_bottom_sheet.dart @@ -5,6 +5,7 @@ import 'package:helpwave_service/user.dart'; import 'package:helpwave_theme/constants.dart'; import 'package:helpwave_widget/bottom_sheets.dart'; import 'package:helpwave_widget/loading.dart'; +import 'package:helpwave_widget/navigation.dart'; import 'package:provider/provider.dart'; import 'package:helpwave_service/tasks.dart'; import 'package:helpwave_service/auth.dart'; @@ -15,7 +16,7 @@ import 'package:tasks/screens/settings_screen.dart'; class UserBottomSheetPageBuilder with BottomSheetPageBuilder { @override - BottomSheetHeader? headerBuilder(BuildContext context, NestedBottomSheetNavigationController controller) { + BottomSheetHeader? headerBuilder(BuildContext context, NavigationController controller) { return BottomSheetHeader( trailing: BottomSheetAction( icon: Icons.settings, @@ -25,7 +26,7 @@ class UserBottomSheetPageBuilder with BottomSheetPageBuilder { } @override - Widget build(BuildContext context, NestedBottomSheetNavigationController controller) { + Widget build(BuildContext context, NavigationController controller) { final double width = MediaQuery.of(context).size.width; return Flexible( diff --git a/apps/tasks/lib/components/user_header.dart b/apps/tasks/lib/components/user_header.dart index 5ea59d78..15a9ece6 100644 --- a/apps/tasks/lib/components/user_header.dart +++ b/apps/tasks/lib/components/user_header.dart @@ -40,7 +40,7 @@ class UserHeader extends StatelessWidget implements PreferredSizeWidget { onTap: () { context.pushModal( context: context, - builder: (context) => NestedBottomSheetNavigator(initialPageBuilder: UserBottomSheetPageBuilder()), + builder: (context) => BottomSheetNavigator(initialPageBuilder: UserBottomSheetPageBuilder()), ); }, title: Text( diff --git a/apps/tasks/lib/screens/main_screen_subscreens/patient_screen.dart b/apps/tasks/lib/screens/main_screen_subscreens/patient_screen.dart index 2278a361..77a4dd12 100644 --- a/apps/tasks/lib/screens/main_screen_subscreens/patient_screen.dart +++ b/apps/tasks/lib/screens/main_screen_subscreens/patient_screen.dart @@ -28,7 +28,7 @@ class _PatientScreenState extends State { child: Column( children: [ Padding( - padding: const EdgeInsets.only(left: paddingSmall, right: paddingSmall, bottom: paddingMedium), + padding: const EdgeInsets.only(left: paddingSmall, right: paddingSmall, bottom: paddingMedium, top: paddingSmall), child: Consumer(builder: (_, patientController, __) { return SearchBar( hintText: context.localization!.searchPatient, diff --git a/apps/tasks/lib/screens/settings_screen.dart b/apps/tasks/lib/screens/settings_screen.dart index 9f31e582..02bc5588 100644 --- a/apps/tasks/lib/screens/settings_screen.dart +++ b/apps/tasks/lib/screens/settings_screen.dart @@ -5,6 +5,7 @@ import 'package:helpwave_service/auth.dart'; import 'package:helpwave_theme/constants.dart'; import 'package:helpwave_theme/theme.dart'; import 'package:helpwave_widget/bottom_sheets.dart'; +import 'package:helpwave_widget/navigation.dart'; import 'package:provider/provider.dart'; import 'package:tasks/screens/login_screen.dart'; @@ -146,7 +147,7 @@ class SettingsScreen extends StatelessWidget { class SettingsBottomSheetPageBuilder with BottomSheetPageBuilder { @override - Widget build(BuildContext context, NestedBottomSheetNavigationController controller) { + Widget build(BuildContext context, NavigationController controller) { return Flexible( child: ListView( children: ListTile.divideTiles( @@ -213,11 +214,11 @@ class SettingsBottomSheetPageBuilder with BottomSheetPageBuilder { }, itemBuilder: (BuildContext context) => getSupportedLocalsWithName() .map((local) => PopupMenuItem( - value: local.local, - child: Text( - local.name, - ), - )) + value: local.local, + child: Text( + local.name, + ), + )) .toList(), child: Material( child: Row( diff --git a/packages/helpwave_widget/lib/navigation.dart b/packages/helpwave_widget/lib/navigation.dart new file mode 100644 index 00000000..073a7465 --- /dev/null +++ b/packages/helpwave_widget/lib/navigation.dart @@ -0,0 +1 @@ +export 'package:helpwave_widget/src/navigation/index.dart'; diff --git a/packages/helpwave_widget/lib/src/bottom_sheets/bottom_sheet_navigator.dart b/packages/helpwave_widget/lib/src/bottom_sheets/bottom_sheet_navigator.dart new file mode 100644 index 00000000..50544009 --- /dev/null +++ b/packages/helpwave_widget/lib/src/bottom_sheets/bottom_sheet_navigator.dart @@ -0,0 +1,46 @@ +import 'package:flutter/material.dart'; +import 'package:helpwave_widget/navigation.dart'; +import '../../bottom_sheets.dart'; + +class BottomSheetNavigator extends StatelessWidget { + final BottomSheetPageBuilder initialPageBuilder; + + const BottomSheetNavigator({super.key, required this.initialPageBuilder}); + + @override + Widget build(BuildContext context) { + return SimpleNavigator( + initialPage: initialPageBuilder, + builder: (context, navigationController) { + BottomSheetPageBuilder page = navigationController.currentPage; + + BottomSheetHeader? computedHeader = page.headerBuilder(context, navigationController); + + BottomSheetHeader header = BottomSheetHeader( + title: computedHeader?.title, + titleText: computedHeader?.titleText, + isShowingDragHandler: computedHeader?.isShowingDragHandler ?? false, + trailing: computedHeader?.trailing, + leading: BottomSheetAction( + icon: navigationController.isInitialPage ? Icons.close : Icons.chevron_left_rounded, + onPressed: () { + if (navigationController.canPop) { + navigationController.pop(); + } else { + Navigator.pop(context); + } + }, + ), + ); + + return BottomSheetBase( + onClosing: () {}, + builder: (context) => page.build(context, navigationController), + header: header, + bottomWidget: page.bottomWidgetBuilder(context, navigationController), + mainAxisSize: MainAxisSize.max, + ); + }, + ); + } +} diff --git a/packages/helpwave_widget/lib/src/bottom_sheets/bottom_sheet_page_builder.dart b/packages/helpwave_widget/lib/src/bottom_sheets/bottom_sheet_page_builder.dart new file mode 100644 index 00000000..0af34f0c --- /dev/null +++ b/packages/helpwave_widget/lib/src/bottom_sheets/bottom_sheet_page_builder.dart @@ -0,0 +1,20 @@ +import 'package:flutter/cupertino.dart'; +import 'package:helpwave_widget/navigation.dart'; +import '../../bottom_sheets.dart'; + +mixin BottomSheetPageBuilder { + /// The [BottomSheetHeader] of the [BottomSheetPageBuilder] + /// + /// The leading icon will always be ignored to + BottomSheetHeader? headerBuilder(BuildContext context, NavigationController controller) { + return null; + } + + /// The [BottomSheetBase] bottom widget to display for the [BottomSheetPageBuilder] + Widget? bottomWidgetBuilder(BuildContext context, NavigationController controller) { + return null; + } + + /// The builder function call to build the content of the [BottomSheetBase] + Widget build(BuildContext context, NavigationController controller); +} diff --git a/packages/helpwave_widget/lib/src/bottom_sheets/index.dart b/packages/helpwave_widget/lib/src/bottom_sheets/index.dart index e1423f66..5277adaf 100644 --- a/packages/helpwave_widget/lib/src/bottom_sheets/index.dart +++ b/packages/helpwave_widget/lib/src/bottom_sheets/index.dart @@ -1,2 +1,3 @@ export 'bottom_sheet_base.dart'; -export 'nested_bottom_sheet_navigation.dart'; +export 'bottom_sheet_navigator.dart'; +export 'bottom_sheet_page_builder.dart'; diff --git a/packages/helpwave_widget/lib/src/bottom_sheets/nested_bottom_sheet_navigation.dart b/packages/helpwave_widget/lib/src/bottom_sheets/nested_bottom_sheet_navigation.dart deleted file mode 100644 index af15be7e..00000000 --- a/packages/helpwave_widget/lib/src/bottom_sheets/nested_bottom_sheet_navigation.dart +++ /dev/null @@ -1,102 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:provider/provider.dart'; -import 'package:helpwave_widget/src/bottom_sheets/bottom_sheet_base.dart'; - -mixin BottomSheetPageBuilder { - /// The [BottomSheetHeader] of the [BottomSheetPageBuilder] - /// - /// The leading icon will always be ignored to - BottomSheetHeader? headerBuilder(BuildContext context, NestedBottomSheetNavigationController controller) { - return null; - } - - /// The [BottomSheetBase] bottom widget to display for the [BottomSheetPageBuilder] - Widget? bottomWidgetBuilder(BuildContext context, NestedBottomSheetNavigationController controller) { - return null; - } - - /// The builder function call to build the content of the [BottomSheetBase] - Widget build(BuildContext context, NestedBottomSheetNavigationController controller); -} - -class NestedBottomSheetNavigationController extends ChangeNotifier { - List stack = []; - - BottomSheetPageBuilder get currentPage => stack.last; - - bool get canPop => stack.length > 1; - - bool get isInitialPage => stack.length == 1; - - NestedBottomSheetNavigationController({ - required BottomSheetPageBuilder initialPageBuilder, - }) { - stack.add(initialPageBuilder); - } - - void push(BottomSheetPageBuilder page) { - stack.add(page); - notifyListeners(); - } - - void pop() { - stack.removeLast(); - notifyListeners(); - } -} - -class NestedBottomSheetNavigator extends StatelessWidget { - final BottomSheetPageBuilder initialPageBuilder; - - const NestedBottomSheetNavigator({super.key, required this.initialPageBuilder}); - - @override - Widget build(BuildContext context) { - return ChangeNotifierProvider( - create: (context) => NestedBottomSheetNavigationController( - initialPageBuilder: initialPageBuilder, - ), - child: Consumer( - builder: (context, navigationController, child) { - BottomSheetPageBuilder page = navigationController.currentPage; - - BottomSheetHeader? computedHeader = page.headerBuilder(context, navigationController); - - BottomSheetHeader header = BottomSheetHeader( - title: computedHeader?.title, - titleText: computedHeader?.titleText, - isShowingDragHandler: computedHeader?.isShowingDragHandler ?? false, - trailing: computedHeader?.trailing, - leading: BottomSheetAction( - icon: navigationController.isInitialPage ? Icons.close : Icons.chevron_left_rounded, - onPressed: () { - if (navigationController.canPop) { - navigationController.pop(); - } else { - Navigator.pop(context); - } - }, - ), - ); - - return PopScope( - // If the navigation controller cannot pop to another bottom sheet, a normal pop is correct - canPop: !navigationController.canPop, - onPopInvoked: (didPop) { - if(navigationController.canPop){ - navigationController.pop(); - } - }, - child: BottomSheetBase( - onClosing: () {}, - builder: (context) => page.build(context, navigationController), - header: header, - bottomWidget: page.bottomWidgetBuilder(context, navigationController), - mainAxisSize: MainAxisSize.max, - ), - ); - }, - ), - ); - } -} diff --git a/packages/helpwave_widget/lib/src/navigation/index.dart b/packages/helpwave_widget/lib/src/navigation/index.dart new file mode 100644 index 00000000..42ae72d6 --- /dev/null +++ b/packages/helpwave_widget/lib/src/navigation/index.dart @@ -0,0 +1,2 @@ +export 'navigation_controller.dart'; +export 'simple_navigator.dart'; diff --git a/packages/helpwave_widget/lib/src/navigation/navigation_controller.dart b/packages/helpwave_widget/lib/src/navigation/navigation_controller.dart new file mode 100644 index 00000000..8d540f8f --- /dev/null +++ b/packages/helpwave_widget/lib/src/navigation/navigation_controller.dart @@ -0,0 +1,27 @@ +import 'package:flutter/cupertino.dart'; + +class NavigationController extends ChangeNotifier { + List stack = []; + + T get currentPage => stack.last; + + bool get canPop => stack.length > 1; + + bool get isInitialPage => stack.length == 1; + + NavigationController({ + required T initialPage, + }) { + stack.add(initialPage); + } + + void push(T page) { + stack.add(page); + notifyListeners(); + } + + void pop() { + stack.removeLast(); + notifyListeners(); + } +} diff --git a/packages/helpwave_widget/lib/src/navigation/simple_navigator.dart b/packages/helpwave_widget/lib/src/navigation/simple_navigator.dart new file mode 100644 index 00000000..de114744 --- /dev/null +++ b/packages/helpwave_widget/lib/src/navigation/simple_navigator.dart @@ -0,0 +1,35 @@ +import 'package:flutter/material.dart'; +import 'package:helpwave_widget/src/navigation/index.dart'; +import 'package:provider/provider.dart'; + +/// A navigation widget which can be used withing the flutter [Navigator] +class SimpleNavigator extends StatelessWidget { + final T initialPage; + + final Widget Function(BuildContext context, NavigationController navigationController) builder; + + const SimpleNavigator({super.key, required this.initialPage, required this.builder}); + + @override + Widget build(BuildContext context) { + return ChangeNotifierProvider( + create: (context) => NavigationController( + initialPage: initialPage, + ), + child: Consumer>( + builder: (context, navigationController, _) { + return PopScope( + // If the navigation controller cannot pop to another bottom sheet, a normal pop is correct + canPop: !navigationController.canPop, + onPopInvoked: (didPop) { + if (navigationController.canPop) { + navigationController.pop(); + } + }, + child: builder(context, navigationController), + ); + }, + ), + ); + } +} From 91c536101729994b4a704f74099bae817e4121dc Mon Sep 17 00:00:00 2001 From: Felix Thape Date: Thu, 19 Sep 2024 03:40:43 +0200 Subject: [PATCH 09/36] feat: update user bottom sheet and setting bottom sheet --- .../lib/components/patient_bottom_sheet.dart | 10 +- .../lib/components/task_bottom_sheet.dart | 11 +- apps/tasks/lib/components/task_card.dart | 13 +- .../lib/components/task_expansion_tile.dart | 9 +- .../lib/components/user_bottom_sheet.dart | 122 +++----- apps/tasks/lib/components/user_header.dart | 9 +- .../components/ward_select_bottom_sheet.dart | 53 ++++ apps/tasks/lib/screens/landing_screen.dart | 2 +- apps/tasks/lib/screens/main_screen.dart | 28 +- .../my_tasks_screen.dart | 3 +- .../patient_screen.dart | 5 +- apps/tasks/lib/screens/settings_screen.dart | 260 +++++++++++++----- apps/tasks/pubspec.yaml | 4 + .../helpwave_localization/lib/l10n/app_de.arb | 8 +- .../helpwave_localization/lib/l10n/app_en.arb | 8 +- .../src/api/tasks/services/ward_service.dart | 15 + .../fonts/Space_Grotesk/OFL.txt | 0 .../fonts/Space_Grotesk/README.txt | 0 .../SpaceGrotesk-VariableFont_wght.ttf | Bin .../static/SpaceGrotesk-Bold.ttf | Bin .../static/SpaceGrotesk-Light.ttf | Bin .../static/SpaceGrotesk-Medium.ttf | Bin .../static/SpaceGrotesk-Regular.ttf | Bin .../static/SpaceGrotesk-SemiBold.ttf | Bin .../helpwave_theme/lib/src/constants.dart | 3 +- .../lib/src/theme/dark_theme.dart | 51 ++-- .../lib/src/theme/light_theme.dart | 47 ++-- .../helpwave_theme/lib/src/theme/theme.dart | 39 +-- .../helpwave_theme/lib/src/theme_model.dart | 3 +- .../util/material_state_color_resolver.dart | 5 +- packages/helpwave_theme/pubspec.yaml | 2 +- packages/helpwave_widget/lib/lists.dart | 2 +- .../src/bottom_sheets/bottom_sheet_base.dart | 3 +- .../lib/src/content_selection/list_entry.dart | 3 +- .../src/content_selection/list_search.dart | 5 +- .../lib/src/lists/add_list.dart | 5 +- .../helpwave_widget/lib/src/lists/index.dart | 2 + .../lib/src/lists/rounded_list_tiles.dart | 56 ++++ .../lib/src/loading/loading_spinner.dart | 3 +- .../src/text_input/clickable_text_edit.dart | 11 +- 40 files changed, 516 insertions(+), 284 deletions(-) create mode 100644 apps/tasks/lib/components/ward_select_bottom_sheet.dart rename packages/helpwave_theme/{assets => lib}/fonts/Space_Grotesk/OFL.txt (100%) rename packages/helpwave_theme/{assets => lib}/fonts/Space_Grotesk/README.txt (100%) rename packages/helpwave_theme/{assets => lib}/fonts/Space_Grotesk/SpaceGrotesk-VariableFont_wght.ttf (100%) rename packages/helpwave_theme/{assets => lib}/fonts/Space_Grotesk/static/SpaceGrotesk-Bold.ttf (100%) rename packages/helpwave_theme/{assets => lib}/fonts/Space_Grotesk/static/SpaceGrotesk-Light.ttf (100%) rename packages/helpwave_theme/{assets => lib}/fonts/Space_Grotesk/static/SpaceGrotesk-Medium.ttf (100%) rename packages/helpwave_theme/{assets => lib}/fonts/Space_Grotesk/static/SpaceGrotesk-Regular.ttf (100%) rename packages/helpwave_theme/{assets => lib}/fonts/Space_Grotesk/static/SpaceGrotesk-SemiBold.ttf (100%) create mode 100644 packages/helpwave_widget/lib/src/lists/index.dart create mode 100644 packages/helpwave_widget/lib/src/lists/rounded_list_tiles.dart diff --git a/apps/tasks/lib/components/patient_bottom_sheet.dart b/apps/tasks/lib/components/patient_bottom_sheet.dart index 446882d2..f5a0cca3 100644 --- a/apps/tasks/lib/components/patient_bottom_sheet.dart +++ b/apps/tasks/lib/components/patient_bottom_sheet.dart @@ -60,7 +60,7 @@ class _PatientBottomSheetState extends State { onUpdated: patientController.changeName, textAlign: TextAlign.center, textStyle: TextStyle( - color: Theme.of(context).colorScheme.secondary, + color: context.theme.colorScheme.primary, fontWeight: FontWeight.bold, fontSize: iconSizeTiny, fontFamily: "SpaceGrotesk", @@ -161,17 +161,17 @@ class _PatientBottomSheetState extends State { if (beds.isEmpty) { return Text( context.localization!.noFreeBeds, - style: TextStyle(color: Theme.of(context).disabledColor, fontWeight: FontWeight.bold), + style: TextStyle(color: context.theme.disabledColor, fontWeight: FontWeight.bold), ); } return DropdownButtonHideUnderline( child: DropdownButton( - iconEnabledColor: Theme.of(context).colorScheme.secondary.withOpacity(0.6), + iconEnabledColor: context.theme.colorScheme.primary.withOpacity(0.6), padding: EdgeInsets.zero, isDense: true, hint: Text( context.localization!.assignBed, - style: TextStyle(color: Theme.of(context).colorScheme.secondary.withOpacity(0.6)), + style: TextStyle(color: context.theme.colorScheme.primary.withOpacity(0.6)), ), value: beds.where((beds) => beds.bed.id == patientController.patient.bed?.id).firstOrNull, items: beds @@ -179,7 +179,7 @@ class _PatientBottomSheetState extends State { value: roomWithBed, child: Text( "${roomWithBed.room.name} - ${roomWithBed.bed.name}", - style: TextStyle(color: Theme.of(context).colorScheme.primary.withOpacity(0.6)), + style: TextStyle(color: context.theme.colorScheme.primary.withOpacity(0.6)), ), )) .toList(), diff --git a/apps/tasks/lib/components/task_bottom_sheet.dart b/apps/tasks/lib/components/task_bottom_sheet.dart index 271facb8..212311e5 100644 --- a/apps/tasks/lib/components/task_bottom_sheet.dart +++ b/apps/tasks/lib/components/task_bottom_sheet.dart @@ -3,6 +3,7 @@ import 'package:helpwave_localization/localization.dart'; import 'package:helpwave_service/auth.dart'; import 'package:helpwave_service/user.dart'; import 'package:helpwave_theme/constants.dart'; +import 'package:helpwave_theme/util.dart'; import 'package:helpwave_util/loading.dart'; import 'package:helpwave_widget/bottom_sheets.dart'; import 'package:helpwave_widget/loading.dart'; @@ -127,7 +128,7 @@ class _TaskBottomSheetState extends State { onUpdated: taskController.changeName, textAlign: TextAlign.center, textStyle: TextStyle( - color: Theme.of(context).colorScheme.secondary, + color: context.theme.colorScheme.primary, fontWeight: FontWeight.bold, fontSize: iconSizeTiny, fontFamily: "SpaceGrotesk", @@ -177,12 +178,12 @@ class _TaskBottomSheetState extends State { List patients = patientList.active + patientList.unassigned; return DropdownButton( underline: const SizedBox(), - iconEnabledColor: Theme.of(context).colorScheme.secondary.withOpacity(0.6), + iconEnabledColor: context.theme.colorScheme.primary.withOpacity(0.6), // removes the default underline padding: EdgeInsets.zero, hint: Text( context.localization!.selectPatient, - style: TextStyle(color: Theme.of(context).colorScheme.secondary.withOpacity(0.6)), + style: TextStyle(color: context.theme.colorScheme.primary.withOpacity(0.6)), ), isDense: true, items: patients @@ -264,7 +265,7 @@ class _TaskBottomSheetState extends State { builder: (context, child) { // Overwrite the Theme ThemeData pickerTheme = - Theme.of(context).copyWith(textButtonTheme: const TextButtonThemeData()); + context.theme.copyWith(textButtonTheme: const TextButtonThemeData()); return Theme(data: pickerTheme, child: child ?? const SizedBox()); }, ).then((date) async { @@ -272,7 +273,7 @@ class _TaskBottomSheetState extends State { context: context, initialTime: TimeOfDay.fromDateTime(taskController.task.dueDate ?? DateTime.now()), builder: (context, child) { - ThemeData originalTheme = Theme.of(context); + ThemeData originalTheme = context.theme; // Temporarily set a default theme for the picker ThemeData pickerTheme = ThemeData.fallback().copyWith( diff --git a/apps/tasks/lib/components/task_card.dart b/apps/tasks/lib/components/task_card.dart index 3eade9a4..d88d195a 100644 --- a/apps/tasks/lib/components/task_card.dart +++ b/apps/tasks/lib/components/task_card.dart @@ -2,6 +2,7 @@ import 'package:flutter/material.dart'; import 'package:helpwave_localization/localization.dart'; import 'package:helpwave_service/tasks.dart'; import 'package:helpwave_theme/constants.dart'; +import 'package:helpwave_theme/util.dart'; import 'package:helpwave_widget/static_progress_indicator.dart'; /// A [Card] showing a [Task]'s information @@ -82,8 +83,8 @@ class TaskCard extends StatelessWidget { children: [ StaticProgressIndicator( progress: task.progress, - color: Theme.of(context).colorScheme.secondary, - backgroundColor: Theme.of(context).colorScheme.onSecondary.withOpacity(0.5), + color: context.theme.colorScheme.primary, + backgroundColor: context.theme.colorScheme.onSurface.withOpacity(0.3), isClockwise: true, angle: 0, ), @@ -110,7 +111,7 @@ class TaskCard extends StatelessWidget { children: [ Text( task.patient.name, - style: TextStyle(color: Theme.of(context).colorScheme.secondary), + style: TextStyle(color: context.theme.colorScheme.primary), ), Text( task.name, @@ -126,7 +127,7 @@ class TaskCard extends StatelessWidget { fontSize: 14, fontFamily: "SpaceGrotesk", overflow: TextOverflow.ellipsis, - color: Theme.of(context).colorScheme.onSurface.withOpacity(0.6), + color: context.theme.colorScheme.onSurface.withOpacity(0.6), ), ) : const SizedBox(), ], @@ -143,7 +144,9 @@ class TaskCard extends StatelessWidget { size: iconSizeTiny, Icons.check_circle_outline_rounded, // TODO change colors later - color: task.status == TaskStatus.done ? Colors.grey : Theme.of(context).colorScheme.secondary, + color: task.status == TaskStatus.done ? context.theme.colorScheme.onSurface.withOpacity(0.4) : context.theme + .colorScheme + .primary, ), ), ], diff --git a/apps/tasks/lib/components/task_expansion_tile.dart b/apps/tasks/lib/components/task_expansion_tile.dart index de316ace..5b9e5725 100644 --- a/apps/tasks/lib/components/task_expansion_tile.dart +++ b/apps/tasks/lib/components/task_expansion_tile.dart @@ -1,5 +1,6 @@ import 'package:flutter/material.dart'; import 'package:helpwave_theme/constants.dart'; +import 'package:helpwave_theme/util.dart'; import 'package:helpwave_widget/bottom_sheets.dart'; import 'package:helpwave_widget/shapes.dart'; import 'package:provider/provider.dart'; @@ -29,13 +30,13 @@ class TaskExpansionTile extends StatelessWidget { @override Widget build(BuildContext context) { return Theme( - data: Theme.of(context).copyWith( + data: context.theme.copyWith( dividerColor: Colors.transparent, - listTileTheme: Theme.of(context).listTileTheme.copyWith(minLeadingWidth: 0, horizontalTitleGap: paddingSmall), + listTileTheme: context.theme.listTileTheme.copyWith(minLeadingWidth: 0, horizontalTitleGap: paddingSmall), ), child: ExpansionTile( - iconColor: Theme.of(context).colorScheme.primary.withOpacity(0.8), - collapsedIconColor: Theme.of(context).colorScheme.primary.withOpacity(0.8), + iconColor: context.theme.colorScheme.primary.withOpacity(0.8), + collapsedIconColor: context.theme.colorScheme.primary.withOpacity(0.8), textColor: color, collapsedTextColor: color, initiallyExpanded: true, diff --git a/apps/tasks/lib/components/user_bottom_sheet.dart b/apps/tasks/lib/components/user_bottom_sheet.dart index 74a7dfbd..f3f649e2 100644 --- a/apps/tasks/lib/components/user_bottom_sheet.dart +++ b/apps/tasks/lib/components/user_bottom_sheet.dart @@ -1,19 +1,20 @@ -import 'dart:math'; import 'package:flutter/material.dart'; import 'package:helpwave_localization/localization.dart'; import 'package:helpwave_service/user.dart'; import 'package:helpwave_theme/constants.dart'; +import 'package:helpwave_theme/util.dart'; import 'package:helpwave_widget/bottom_sheets.dart'; +import 'package:helpwave_widget/lists.dart'; import 'package:helpwave_widget/loading.dart'; import 'package:helpwave_widget/navigation.dart'; import 'package:provider/provider.dart'; import 'package:helpwave_service/tasks.dart'; import 'package:helpwave_service/auth.dart'; +import 'package:tasks/components/ward_select_bottom_sheet.dart'; import 'package:tasks/screens/login_screen.dart'; import 'package:helpwave_widget/widgets.dart'; import 'package:tasks/screens/settings_screen.dart'; - class UserBottomSheetPageBuilder with BottomSheetPageBuilder { @override BottomSheetHeader? headerBuilder(BuildContext context, NavigationController controller) { @@ -27,8 +28,6 @@ class UserBottomSheetPageBuilder with BottomSheetPageBuilder { @override Widget build(BuildContext context, NavigationController controller) { - final double width = MediaQuery.of(context).size.width; - return Flexible( child: Column( mainAxisAlignment: MainAxisAlignment.center, @@ -59,87 +58,48 @@ class UserBottomSheetPageBuilder with BottomSheetPageBuilder { currentWardController.currentWard?.organizationName ?? context.localization!.loading, style: TextStyle( fontSize: fontSizeSmall, - color: Theme.of(context).colorScheme.primary.withOpacity(0.6), + color: context.theme.hintColor, ), ), ), - Padding( - padding: const EdgeInsets.symmetric( - vertical: paddingBig, - ), - child: Consumer(builder: (context, currentWardController, __) { - return LoadingFutureBuilder( - loadingWidget: const SizedBox(), - data: - WardService().getWardOverviews(organizationId: currentWardController.currentWard!.organizationId), - thenWidgetBuilder: (BuildContext context, List data) { - double menuWidth = min(250, width * 0.7); - return PopupMenuButton( - initialValue: currentWardController.currentWard?.wardId, - shadowColor: Colors.transparent, - position: PopupMenuPosition.under, - itemBuilder: (context) { - return data - .map( - (WardOverview ward) => PopupMenuItem( - value: ward.id, - child: SizedBox( - child: Text(ward.name), - ), - ), - ) - .toList(); - }, - onSelected: (wardId) { - currentWardController.currentWard = CurrentWardInformation( - data.firstWhere((ward) => ward.id == wardId), - currentWardController.currentWard!.organization); - }, - // Material used to hide splash effects of the PopupMenu's Inkwell - child: Material( - child: Container( - width: menuWidth, - constraints: BoxConstraints(maxWidth: menuWidth, minWidth: menuWidth), - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(borderRadiusMedium), - color: Theme.of(context).popupMenuTheme.color, - ), - child: Padding( - padding: const EdgeInsets.symmetric(horizontal: paddingMedium, vertical: paddingSmall), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Text( - context.localization!.ward, - style: TextStyle( - color: Theme.of(context).popupMenuTheme.textStyle?.color?.withOpacity(0.6), - ), - ), - Row( - mainAxisSize: MainAxisSize.min, - children: [ - Text( - currentWardController.currentWard?.wardName ?? context.localization!.none, - style: const TextStyle(fontWeight: FontWeight.bold), - ), - const SizedBox( - width: distanceTiny, - ), - const Icon( - Icons.expand_more_rounded, - size: iconSizeTiny, - ), - ], - ), - ], - ), - ), - ), - ), + const SizedBox(height: distanceBig), + RoundedListTiles(items: [ + ListTile( + leading: Icon( + Icons.house_rounded, + color: context.theme.colorScheme.primary, + ), + title: Text(context.localization!.currentWard, style: const TextStyle(fontWeight: FontWeight.bold)), + trailing: Row( + mainAxisSize: MainAxisSize.min, + mainAxisAlignment: MainAxisAlignment.end, + children: [ + Consumer(builder: (context, currentWardController, __) { + return Text( + currentWardController.currentWard!.wardName, + style: context.theme.textTheme.labelLarge, ); - }); - }), - ), + }), + const Icon(Icons.chevron_right_rounded), + ], + ), + onTap: () => { + context.pushModal( + context: context, + builder: (context) => WardSelectBottomSheet( + selectedWardId: CurrentWardService().currentWard!.wardId, + onChange: (WardMinimal ward) { + CurrentWardService().currentWard = CurrentWardInformation( + ward, + CurrentWardService().currentWard!.organization, + ); + Navigator.pop(context); + }, + ), + ) + }, + ), + ]), const Spacer(), Padding( padding: const EdgeInsets.only(bottom: distanceMedium), diff --git a/apps/tasks/lib/components/user_header.dart b/apps/tasks/lib/components/user_header.dart index 15a9ece6..1e942115 100644 --- a/apps/tasks/lib/components/user_header.dart +++ b/apps/tasks/lib/components/user_header.dart @@ -17,7 +17,12 @@ class UserHeader extends StatelessWidget implements PreferredSizeWidget { @override Widget build(BuildContext context) { return Container( - color: context.theme.appBarTheme.backgroundColor, + decoration: BoxDecoration( + color: context.theme.appBarTheme.backgroundColor, + boxShadow: [ + BoxShadow(spreadRadius: 0, blurRadius: 2, offset: const Offset(0, -1), color: Colors.black.withOpacity(0.1)), + ] + ), child: Consumer(builder: (context, userSession, __) { return SafeArea( child: ListTile( @@ -53,7 +58,7 @@ class UserHeader extends StatelessWidget implements PreferredSizeWidget { Text( "${currentWardController.currentWard?.organization.shortName ?? ""} - ", style: TextStyle( - color: Theme.of(context).colorScheme.onBackground.withOpacity(0.7), + color: context.theme.colorScheme.onBackground.withOpacity(0.7), fontSize: 14, ), ), diff --git a/apps/tasks/lib/components/ward_select_bottom_sheet.dart b/apps/tasks/lib/components/ward_select_bottom_sheet.dart new file mode 100644 index 00000000..e84d083d --- /dev/null +++ b/apps/tasks/lib/components/ward_select_bottom_sheet.dart @@ -0,0 +1,53 @@ +import 'package:flutter/material.dart'; +import 'package:helpwave_localization/localization.dart'; +import 'package:helpwave_service/auth.dart'; +import 'package:helpwave_service/tasks.dart'; +import 'package:helpwave_widget/bottom_sheets.dart'; +import 'package:helpwave_widget/loading.dart'; + +class WardSelectBottomSheet extends StatelessWidget { + /// The currently selected [WardMinimal] + /// + /// This is used to highlight the [WardMinimal] in the [List] + final String? selectedWardId; + + /// The [Organization] identifier for which all [WardMinimal]s are loaded + /// + /// If unspecified the organizationId of the [CurrentWardService] is taken + final String? organizationId; + + final void Function(WardMinimal ward) onChange; + + const WardSelectBottomSheet({super.key, this.selectedWardId, this.organizationId, required this.onChange}); + + @override + Widget build(BuildContext context) { + return BottomSheetBase( + onClosing: () {}, + header: BottomSheetHeader(titleText: context.localization!.selectWard), + mainAxisSize: MainAxisSize.min, + builder: (context) { + return LoadingFutureBuilder( + loadingWidget: const SizedBox(), + data: WardService().getWards(organizationId: organizationId), + thenWidgetBuilder: (context, wards) { + return Flexible( + child: ListView( + shrinkWrap: true, + children: wards + .map((ward) => ListTile( + onTap: () => onChange(ward), + title: Text( + ward.name, + style:TextStyle(decoration: ward.id == selectedWardId ? TextDecoration.underline : null), + ), + )) + .toList(), + ), + ); + }, + ); + }, + ); + } +} diff --git a/apps/tasks/lib/screens/landing_screen.dart b/apps/tasks/lib/screens/landing_screen.dart index 052622a1..b6d4a1ee 100644 --- a/apps/tasks/lib/screens/landing_screen.dart +++ b/apps/tasks/lib/screens/landing_screen.dart @@ -29,7 +29,7 @@ class LandingScreen extends StatelessWidget { .theme.colorScheme.onBackground))), child: Text( context.localization!.loginSlogan, - style: Theme.of(context).textTheme.labelLarge, + style: context.theme.textTheme.labelLarge, ), onPressed: () { userSessionController.login(); diff --git a/apps/tasks/lib/screens/main_screen.dart b/apps/tasks/lib/screens/main_screen.dart index d39b2564..c2ec10fb 100644 --- a/apps/tasks/lib/screens/main_screen.dart +++ b/apps/tasks/lib/screens/main_screen.dart @@ -5,6 +5,7 @@ import 'package:helpwave_service/auth.dart'; import 'package:helpwave_service/tasks.dart'; import 'package:helpwave_theme/constants.dart'; import 'package:helpwave_theme/theme.dart'; +import 'package:helpwave_theme/util.dart'; import 'package:helpwave_widget/bottom_sheets.dart'; import 'package:helpwave_widget/animation.dart'; import 'package:provider/provider.dart'; @@ -52,30 +53,19 @@ class _MainScreenState extends State { child: _TaskPatientFloatingActionButton(), ), bottomNavigationBar: NavigationBar( - indicatorColor: Theme.of(context).colorScheme.secondary, - backgroundColor: themeNotifier.getIsDarkNullSafe(context) ? Colors.white10 : Colors.white, + indicatorColor: context.theme.colorScheme.primary, + surfaceTintColor: Colors.transparent, + shadowColor: context.theme.colorScheme.shadow, destinations: [ NavigationDestination( - selectedIcon: Icon( - Icons.check_circle_outline, - color: Theme.of(context).colorScheme.onSecondary, - ), icon: const Icon(Icons.check_circle_outline), label: context.localization!.myTasks, ), NavigationDestination( - selectedIcon: Icon( - Icons.add_circle_outline, - color: Theme.of(context).colorScheme.onSecondary, - ), icon: const Icon(Icons.add_circle_outline), label: context.localization!.newTaskOrPatient, ), NavigationDestination( - selectedIcon: Icon( - Icons.person, - color: Theme.of(context).colorScheme.onSecondary, - ), icon: const Icon(Icons.person), label: context.localization!.patients, ), @@ -117,7 +107,7 @@ class _TaskPatientFloatingActionButton extends StatelessWidget { return Container( decoration: BoxDecoration( borderRadius: BorderRadius.circular(componentHeight), - color: Color.alphaBlend(Theme.of(context).colorScheme.secondary.withOpacity(0.7), Colors.black), + color: Color.alphaBlend(context.theme.colorScheme.primary.withOpacity(0.7), Colors.black), ), child: Padding( padding: const EdgeInsets.all(paddingSmall), @@ -128,8 +118,8 @@ class _TaskPatientFloatingActionButton extends StatelessWidget { ActionChip( labelPadding: EdgeInsets.zero, visualDensity: VisualDensity.compact, - backgroundColor: Theme.of(context).colorScheme.secondary, - labelStyle: TextStyle(color: Theme.of(context).colorScheme.onSecondary), + backgroundColor: context.theme.colorScheme.primary, + labelStyle: TextStyle(color: context.theme.colorScheme.onPrimary), side: BorderSide.none, shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(chipHeight), // Adjust the radius as needed @@ -152,8 +142,8 @@ class _TaskPatientFloatingActionButton extends StatelessWidget { ActionChip( labelPadding: EdgeInsets.zero, visualDensity: VisualDensity.compact, - backgroundColor: Theme.of(context).colorScheme.secondary, - labelStyle: TextStyle(color: Theme.of(context).colorScheme.onSecondary), + backgroundColor: context.theme.colorScheme.primary, + labelStyle: TextStyle(color: context.theme.colorScheme.onPrimary), side: BorderSide.none, shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(chipHeight), // Adjust the radius as needed diff --git a/apps/tasks/lib/screens/main_screen_subscreens/my_tasks_screen.dart b/apps/tasks/lib/screens/main_screen_subscreens/my_tasks_screen.dart index 54b2ca37..7db67b62 100644 --- a/apps/tasks/lib/screens/main_screen_subscreens/my_tasks_screen.dart +++ b/apps/tasks/lib/screens/main_screen_subscreens/my_tasks_screen.dart @@ -1,6 +1,7 @@ import 'package:flutter/material.dart'; import 'package:helpwave_service/tasks.dart'; import 'package:helpwave_theme/constants.dart'; +import 'package:helpwave_theme/util.dart'; import 'package:provider/provider.dart'; import 'package:helpwave_localization/localization.dart'; import 'package:tasks/components/task_expansion_tile.dart'; @@ -25,7 +26,7 @@ class _MyTasksScreenState extends State { state: tasksController.state, child: ListView(children: [ Theme( - data: Theme.of(context).copyWith( + data: context.theme.copyWith( dividerColor: Colors.transparent, listTileTheme: const ListTileThemeData(minLeadingWidth: 0, horizontalTitleGap: distanceSmall), ), diff --git a/apps/tasks/lib/screens/main_screen_subscreens/patient_screen.dart b/apps/tasks/lib/screens/main_screen_subscreens/patient_screen.dart index 77a4dd12..fdef6aa0 100644 --- a/apps/tasks/lib/screens/main_screen_subscreens/patient_screen.dart +++ b/apps/tasks/lib/screens/main_screen_subscreens/patient_screen.dart @@ -1,6 +1,7 @@ import 'package:flutter/material.dart'; import 'package:helpwave_localization/localization.dart'; import 'package:helpwave_theme/constants.dart'; +import 'package:helpwave_theme/util.dart'; import 'package:helpwave_widget/bottom_sheets.dart'; import 'package:helpwave_widget/loading.dart'; import 'package:provider/provider.dart'; @@ -40,7 +41,7 @@ class _PatientScreenState extends State { icon: Icon( Icons.search, size: iconSizeTiny, - color: Theme.of(context).searchBarTheme.textStyle!.resolve({MaterialState.selected})!.color, + color: context.theme.searchBarTheme.textStyle!.resolve({MaterialState.selected})!.color, ), ), ], @@ -84,7 +85,7 @@ class _PatientScreenState extends State { padding: const EdgeInsets.all(paddingTiny), child: Container( decoration: BoxDecoration( - color: Theme.of(context).colorScheme.secondary, + color: context.theme.colorScheme.primary, borderRadius: BorderRadius.circular(borderRadiusMedium), ), padding: const EdgeInsets.symmetric(horizontal: paddingMedium), diff --git a/apps/tasks/lib/screens/settings_screen.dart b/apps/tasks/lib/screens/settings_screen.dart index 02bc5588..e21ac3ae 100644 --- a/apps/tasks/lib/screens/settings_screen.dart +++ b/apps/tasks/lib/screens/settings_screen.dart @@ -2,9 +2,13 @@ import 'package:flutter/material.dart'; import 'package:helpwave_localization/localization.dart'; import 'package:helpwave_localization/localization_model.dart'; import 'package:helpwave_service/auth.dart'; +import 'package:helpwave_service/user.dart'; import 'package:helpwave_theme/constants.dart'; import 'package:helpwave_theme/theme.dart'; +import 'package:helpwave_theme/util.dart'; import 'package:helpwave_widget/bottom_sheets.dart'; +import 'package:helpwave_widget/lists.dart'; +import 'package:helpwave_widget/loading.dart'; import 'package:helpwave_widget/navigation.dart'; import 'package:provider/provider.dart'; import 'package:tasks/screens/login_screen.dart'; @@ -145,35 +149,133 @@ class SettingsScreen extends StatelessWidget { } } +class NavigationListTile extends StatelessWidget { + final IconData icon; + final Color? color; + final String title; + final void Function() onTap; + final String? trailingText; + + const NavigationListTile({ + super.key, + required this.icon, + this.color, + required this.title, + required this.onTap, + this.trailingText, + }); + + @override + Widget build(BuildContext context) { + return ListTile( + leading: Icon( + icon, + color: color ?? context.theme.colorScheme.primary, + ), + title: Text(title, style: TextStyle(fontWeight: FontWeight.bold, color: color)), + trailing: Row( + mainAxisSize: MainAxisSize.min, + mainAxisAlignment: MainAxisAlignment.end, + children: [ + trailingText != null + ? Text( + trailingText!, + style: context.theme.textTheme.labelLarge, + ) + : const SizedBox(), + Icon( + Icons.chevron_right_rounded, + color: context.theme.colorScheme.onBackground.withOpacity(0.7), + ), + ], + ), + ); + } +} + class SettingsBottomSheetPageBuilder with BottomSheetPageBuilder { + @override + BottomSheetHeader? headerBuilder(BuildContext context, NavigationController controller) { + return BottomSheetHeader( + titleText: context.localization!.settings, + ); + } + @override Widget build(BuildContext context, NavigationController controller) { + titleBuilder(String title) { + return Padding( + padding: const EdgeInsets.only(bottom: paddingSmall), + child: Text( + title, + style: const TextStyle(fontSize: 18, fontWeight: FontWeight.bold, fontFamily: "SpaceGrotesk"), + ), + ); + } + return Flexible( child: ListView( - children: ListTile.divideTiles( - context: context, - tiles: [ - ListTile( - leading: const Icon(Icons.brightness_medium), - title: Text(context.localization!.darkMode), - trailing: Consumer( - builder: (_, ThemeModel themeNotifier, __) { - return PopupMenuButton( - initialValue: themeNotifier.themeMode, - position: PopupMenuPosition.under, - itemBuilder: (context) => [ - PopupMenuItem(value: ThemeMode.dark, child: Text(context.localization!.darkMode)), - PopupMenuItem(value: ThemeMode.light, child: Text(context.localization!.lightMode)), - PopupMenuItem(value: ThemeMode.system, child: Text(context.localization!.system)), - ], - onSelected: (value) { - if (value == ThemeMode.system) { - themeNotifier.isDark = null; - } else { - themeNotifier.isDark = value == ThemeMode.dark; - } - }, - child: Material( + children: [ + titleBuilder(context.localization!.personalSettings), + RoundedListTiles( + items: [ + NavigationListTile( + icon: Icons.person, + title: context.localization!.personalData, + onTap: () {}, + ), + NavigationListTile( + icon: Icons.security_rounded, + title: context.localization!.passwordAndSecurity, + onTap: () {}, + ), + NavigationListTile( + icon: Icons.checklist_rounded, + title: context.localization!.myTaskTemplates, + onTap: () {}, + ), + ], + ), + const SizedBox(height: distanceMedium), + titleBuilder(context.localization!.myOrganizations), + LoadingFutureBuilder( + data: OrganizationService().getOrganizationsForUser(), + thenWidgetBuilder: (context, data) { + return RoundedListTiles( + items: data + .map((organization) => NavigationListTile( + icon: Icons.apartment_rounded, + title: organization.longName, + onTap: () {}, + )) + .toList(), + ); + }, + ), + const SizedBox(height: distanceMedium), + titleBuilder(context.localization!.appearance), + RoundedListTiles( + items: [ + ListTile( + leading: Icon(Icons.brightness_medium, color: context.theme.colorScheme.primary), + title: Text(context.localization!.darkMode, style: const TextStyle(fontWeight: FontWeight.bold)), + trailing: Consumer( + builder: (_, ThemeModel themeNotifier, __) { + return PopupMenuButton( + initialValue: themeNotifier.themeMode, + position: PopupMenuPosition.under, + itemBuilder: (context) => [ + PopupMenuItem(value: ThemeMode.dark, child: Text(context.localization!.darkMode)), + PopupMenuItem(value: ThemeMode.light, child: Text(context.localization!.lightMode)), + PopupMenuItem(value: ThemeMode.system, child: Text(context.localization!.system)), + ], + onSelected: (value) { + if (value == ThemeMode.system) { + themeNotifier.isDark = null; + } else { + themeNotifier.isDark = value == ThemeMode.dark; + } + }, child: Row( mainAxisSize: MainAxisSize.min, children: [ @@ -196,31 +298,32 @@ class SettingsBottomSheetPageBuilder with BottomSheetPageBuilder { ), ], ), - ), - ); - }, + ); + }, + ), ), - ), - Consumer( - builder: (context, languageModel, child) { - return ListTile( - leading: const Icon(Icons.language), - title: Text(context.localization!.language), - trailing: PopupMenuButton( - position: PopupMenuPosition.under, - initialValue: languageModel.local, - onSelected: (value) { - languageModel.setLanguage(value); - }, - itemBuilder: (BuildContext context) => getSupportedLocalsWithName() - .map((local) => PopupMenuItem( - value: local.local, - child: Text( - local.name, - ), - )) - .toList(), - child: Material( + Consumer( + builder: (context, languageModel, child) { + return ListTile( + leading: Icon(Icons.language, color: context.theme.colorScheme.primary), + title: Text( + context.localization!.language, + style: const TextStyle(fontWeight: FontWeight.bold), + ), + trailing: PopupMenuButton( + position: PopupMenuPosition.under, + initialValue: languageModel.local, + onSelected: (value) { + languageModel.setLanguage(value); + }, + itemBuilder: (BuildContext context) => getSupportedLocalsWithName() + .map((local) => PopupMenuItem( + value: local.local, + child: Text( + local.name, + ), + )) + .toList(), child: Row( mainAxisSize: MainAxisSize.min, children: [ @@ -239,33 +342,40 @@ class SettingsBottomSheetPageBuilder with BottomSheetPageBuilder { ], ), ), - ), - ); - }, - ), - ListTile( - leading: const Icon(Icons.info_outline), - title: Text(context.localization!.licenses), - trailing: const Icon(Icons.arrow_forward), - onTap: () => {showLicensePage(context: context)}, - ), - Consumer( - builder: (context, currentWardService, _) { - return ListTile( - leading: const Icon(Icons.logout), - title: Text(context.localization!.logout), - onTap: () { - UserSessionService().logout(); - currentWardService.clear(); - Navigator.of(context).pushReplacement( - MaterialPageRoute(builder: (_) => const LoginScreen()), - ); - }, - ); - }, - ), - ], - ).toList(), + ); + }, + ), + ], + ), + const SizedBox(height: distanceMedium), + titleBuilder(context.localization!.other), + RoundedListTiles( + items: [ + NavigationListTile( + icon: Icons.info_outline, + title: context.localization!.licenses, + onTap: () => {showLicensePage(context: context)}, + ), + Consumer( + builder: (context, currentWardService, _) { + return NavigationListTile( + icon: Icons.logout, + title: context.localization!.logout, + color: Colors.red.withOpacity(0.7), // TODO get this from theme + onTap: () { + // TODO add confirm dialog + UserSessionService().logout(); + currentWardService.clear(); + Navigator.of(context).pushReplacement( + MaterialPageRoute(builder: (_) => const LoginScreen()), + ); + }, + ); + }, + ), + ], + ), + ], ), ); } diff --git a/apps/tasks/pubspec.yaml b/apps/tasks/pubspec.yaml index 032d9e18..bc5ede94 100644 --- a/apps/tasks/pubspec.yaml +++ b/apps/tasks/pubspec.yaml @@ -69,6 +69,10 @@ flutter: uses-material-design: true assets: - assets/ + fonts: + - family: SpaceGrotesk + fonts: + - asset: packages/helpwave_theme/fonts/Space_Grotesk/SpaceGrotesk-VariableFont_wght.ttf flutter_native_splash: # see https://pub.dev/packages/flutter_native_splash diff --git a/packages/helpwave_localization/lib/l10n/app_de.arb b/packages/helpwave_localization/lib/l10n/app_de.arb index 18a2eeef..fc852afb 100644 --- a/packages/helpwave_localization/lib/l10n/app_de.arb +++ b/packages/helpwave_localization/lib/l10n/app_de.arb @@ -169,5 +169,11 @@ "lightMode": "Hell", "system": "System", "newTaskOrPatient": "Neu", - "remove": "Entfernen" + "remove": "Entfernen", + "appearance": "Aussehen", + "personalSettings": "Persönliche Einstellungen", + "personalData": "Persönliche Daten", + "passwordAndSecurity": "Passwort & Sicherheit", + "other": "Weiteres", + "myTaskTemplates": "Meine Task Templates" } diff --git a/packages/helpwave_localization/lib/l10n/app_en.arb b/packages/helpwave_localization/lib/l10n/app_en.arb index 0321789b..4d370c2c 100644 --- a/packages/helpwave_localization/lib/l10n/app_en.arb +++ b/packages/helpwave_localization/lib/l10n/app_en.arb @@ -169,5 +169,11 @@ "lightMode": "Light", "system": "System", "newTaskOrPatient": "New", - "remove": "Remove" + "remove": "Remove", + "appearance": "Appearance", + "personalSettings": "Personal Settings", + "personalData": "Personal Data", + "passwordAndSecurity": "Password & Security", + "other": "Other", + "myTaskTemplates": "My Task Templates" } diff --git a/packages/helpwave_service/lib/src/api/tasks/services/ward_service.dart b/packages/helpwave_service/lib/src/api/tasks/services/ward_service.dart index 3d28ca97..5dcf8e79 100644 --- a/packages/helpwave_service/lib/src/api/tasks/services/ward_service.dart +++ b/packages/helpwave_service/lib/src/api/tasks/services/ward_service.dart @@ -45,5 +45,20 @@ class WardService { .toList(); } + Future> getWards({String? organizationId}) async { + GetWardsRequest request = GetWardsRequest(); + GetWardsResponse response = await wardService.getWards( + request, + options: CallOptions(metadata: TasksAPIServiceClients().getMetaData(organizationId: organizationId)), + ); + + return response.wards + .map((ward) => WardMinimal( + id: ward.id, + name: ward.name, + )) + .toList(); + } + // TODO ward requests } diff --git a/packages/helpwave_theme/assets/fonts/Space_Grotesk/OFL.txt b/packages/helpwave_theme/lib/fonts/Space_Grotesk/OFL.txt similarity index 100% rename from packages/helpwave_theme/assets/fonts/Space_Grotesk/OFL.txt rename to packages/helpwave_theme/lib/fonts/Space_Grotesk/OFL.txt diff --git a/packages/helpwave_theme/assets/fonts/Space_Grotesk/README.txt b/packages/helpwave_theme/lib/fonts/Space_Grotesk/README.txt similarity index 100% rename from packages/helpwave_theme/assets/fonts/Space_Grotesk/README.txt rename to packages/helpwave_theme/lib/fonts/Space_Grotesk/README.txt diff --git a/packages/helpwave_theme/assets/fonts/Space_Grotesk/SpaceGrotesk-VariableFont_wght.ttf b/packages/helpwave_theme/lib/fonts/Space_Grotesk/SpaceGrotesk-VariableFont_wght.ttf similarity index 100% rename from packages/helpwave_theme/assets/fonts/Space_Grotesk/SpaceGrotesk-VariableFont_wght.ttf rename to packages/helpwave_theme/lib/fonts/Space_Grotesk/SpaceGrotesk-VariableFont_wght.ttf diff --git a/packages/helpwave_theme/assets/fonts/Space_Grotesk/static/SpaceGrotesk-Bold.ttf b/packages/helpwave_theme/lib/fonts/Space_Grotesk/static/SpaceGrotesk-Bold.ttf similarity index 100% rename from packages/helpwave_theme/assets/fonts/Space_Grotesk/static/SpaceGrotesk-Bold.ttf rename to packages/helpwave_theme/lib/fonts/Space_Grotesk/static/SpaceGrotesk-Bold.ttf diff --git a/packages/helpwave_theme/assets/fonts/Space_Grotesk/static/SpaceGrotesk-Light.ttf b/packages/helpwave_theme/lib/fonts/Space_Grotesk/static/SpaceGrotesk-Light.ttf similarity index 100% rename from packages/helpwave_theme/assets/fonts/Space_Grotesk/static/SpaceGrotesk-Light.ttf rename to packages/helpwave_theme/lib/fonts/Space_Grotesk/static/SpaceGrotesk-Light.ttf diff --git a/packages/helpwave_theme/assets/fonts/Space_Grotesk/static/SpaceGrotesk-Medium.ttf b/packages/helpwave_theme/lib/fonts/Space_Grotesk/static/SpaceGrotesk-Medium.ttf similarity index 100% rename from packages/helpwave_theme/assets/fonts/Space_Grotesk/static/SpaceGrotesk-Medium.ttf rename to packages/helpwave_theme/lib/fonts/Space_Grotesk/static/SpaceGrotesk-Medium.ttf diff --git a/packages/helpwave_theme/assets/fonts/Space_Grotesk/static/SpaceGrotesk-Regular.ttf b/packages/helpwave_theme/lib/fonts/Space_Grotesk/static/SpaceGrotesk-Regular.ttf similarity index 100% rename from packages/helpwave_theme/assets/fonts/Space_Grotesk/static/SpaceGrotesk-Regular.ttf rename to packages/helpwave_theme/lib/fonts/Space_Grotesk/static/SpaceGrotesk-Regular.ttf diff --git a/packages/helpwave_theme/assets/fonts/Space_Grotesk/static/SpaceGrotesk-SemiBold.ttf b/packages/helpwave_theme/lib/fonts/Space_Grotesk/static/SpaceGrotesk-SemiBold.ttf similarity index 100% rename from packages/helpwave_theme/assets/fonts/Space_Grotesk/static/SpaceGrotesk-SemiBold.ttf rename to packages/helpwave_theme/lib/fonts/Space_Grotesk/static/SpaceGrotesk-SemiBold.ttf diff --git a/packages/helpwave_theme/lib/src/constants.dart b/packages/helpwave_theme/lib/src/constants.dart index 291df3aa..214faf3b 100644 --- a/packages/helpwave_theme/lib/src/constants.dart +++ b/packages/helpwave_theme/lib/src/constants.dart @@ -1,4 +1,5 @@ import 'package:flutter/material.dart'; +import 'package:helpwave_theme/src/util/context_extension.dart'; /// Button attributes and @@ -134,4 +135,4 @@ const AppBarTheme sharedAppBarTheme = AppBarTheme(centerTitle: true); /// TextStyles TextStyle editableValueTextStyle(BuildContext context) => - TextStyle(color: Theme.of(context).colorScheme.secondary, fontSize: 16, fontWeight: FontWeight.bold); + TextStyle(color: context.theme.colorScheme.primary, fontSize: 16, fontWeight: FontWeight.bold); diff --git a/packages/helpwave_theme/lib/src/theme/dark_theme.dart b/packages/helpwave_theme/lib/src/theme/dark_theme.dart index 8953e56f..319d15c6 100644 --- a/packages/helpwave_theme/lib/src/theme/dark_theme.dart +++ b/packages/helpwave_theme/lib/src/theme/dark_theme.dart @@ -2,32 +2,31 @@ import 'package:flutter/material.dart'; import 'package:helpwave_theme/src/theme/theme.dart'; import '../constants.dart'; -const primaryColor = Color.fromARGB(255, 255, 255, 255); -const onPrimaryColor = Color.fromARGB(255, 0, 0, 0); -const inversePrimaryColor = Color.fromARGB(255, 30, 30, 30); -const secondaryColor = Color.fromARGB(255, 150, 129, 205); -const onSecondaryColor = Color.fromARGB(255, 255, 255, 255); -const tertiary = Color.fromARGB(255, 180, 180, 180); -const onTertiary = Color.fromARGB(255, 0, 0, 0); +const primaryColor = Color.fromARGB(255, 105, 75, 180); +const onPrimaryColor = Colors.white; +const inversePrimaryColor = Color.fromARGB(255, 150, 130, 200); +const secondaryColor = Color.fromARGB(255, 75, 160, 240); +const onSecondaryColor = Colors.white; +const tertiary = Color.fromARGB(255, 90, 90, 90); +const onTertiary = Colors.white; -const backgroundColor = Color.fromARGB(255, 27, 27, 27); +const backgroundColor = Color.fromARGB(255, 20, 20, 20); const onBackgroundColor = Color.fromARGB(255, 255, 255, 255); -const surface = Color.fromARGB(255, 50, 50, 50); -const onSurface = Color.fromARGB(255, 255, 255, 255); +const surface = Color.fromARGB(255, 40, 40, 40); +const onSurface = Colors.white; const surfaceVariant = Color.fromARGB(255, 80, 80, 80); -const onSurfaceVariant = Color.fromARGB(255, 255, 255, 255); +const onSurfaceVariant = Colors.white; const inverseSurface = Color.fromARGB(255, 180, 180, 180); -const onInverseSurface = Color.fromARGB(255, 0, 0, 0); +const onInverseSurface = Colors.black; -const primaryContainer = Color.fromARGB(255, 180, 180, 180); -const onPrimaryContainer = Color.fromARGB(255, 0, 0, 0); -const secondaryContainer = Color.fromARGB(255, 140, 140, 140); -const onSecondaryContainer = Color.fromARGB(255, 0, 0, 0); -const tertiaryContainer = Color.fromARGB(255, 80, 80, 80); -const onTertiaryContainer = Color.fromARGB(255, 255, 255, 255); +const primaryContainer = Color.fromARGB(255, 105, 75, 180); +const onPrimaryContainer = Colors.white; +const secondaryContainer = Color.fromARGB(255, 75, 160, 240); +const onSecondaryContainer = Colors.white; +const tertiaryContainer = Color.fromARGB(255, 0, 0, 0); +const onTertiaryContainer = Colors.white; -const surfaceTint = Colors.transparent; const shadow = Color.fromARGB(255, 60, 60, 60); const outline = Color.fromARGB(255, 255, 255, 255); const disabledColor = Color.fromARGB(255, 100, 100, 100); @@ -71,7 +70,6 @@ ThemeData darkTheme = makeTheme( onErrorContainer: onErrorColor, // other - surfaceTint: surfaceTint, shadow: shadow, outline: outline, disabledColor: disabledColor, @@ -82,9 +80,12 @@ ThemeData darkTheme = makeTheme( // additional brightness: Brightness.dark, - // flutter themes - appBarTheme: sharedAppBarTheme.copyWith( - backgroundColor: const Color.fromARGB(255, 15, 15, 15), - foregroundColor: Colors.white, - ) + // flutter themes + appBarTheme: sharedAppBarTheme.copyWith( + backgroundColor: const Color.fromARGB(255, 15, 15, 15), + foregroundColor: Colors.white, + ), + + // text + primaryTextColor: Colors.white, ); diff --git a/packages/helpwave_theme/lib/src/theme/light_theme.dart b/packages/helpwave_theme/lib/src/theme/light_theme.dart index 881e600d..50eba584 100644 --- a/packages/helpwave_theme/lib/src/theme/light_theme.dart +++ b/packages/helpwave_theme/lib/src/theme/light_theme.dart @@ -2,32 +2,31 @@ import 'package:flutter/material.dart'; import 'package:helpwave_theme/src/theme/theme.dart'; import '../constants.dart'; -const primaryColor = Color.fromARGB(255, 0, 0, 0); -const onPrimaryColor = Color.fromARGB(255, 255, 255, 255); -const inversePrimaryColor = Color.fromARGB(255, 210, 210, 210); -const secondaryColor = Color.fromARGB(255, 105, 75, 180); -const onSecondaryColor = Color.fromARGB(255, 255, 255, 255); -const tertiary = Color.fromARGB(255, 180, 180, 180); -const onTertiary = Color.fromARGB(255, 0, 0, 0); +const primaryColor = Color.fromARGB(255, 105, 75, 180); +const onPrimaryColor = Colors.white; +const inversePrimaryColor = Color.fromARGB(255, 150, 130, 200); +const secondaryColor = Color.fromARGB(255, 75, 160, 240); +const onSecondaryColor = Colors.white; +const tertiary = Color.fromARGB(255, 90, 90, 90); +const onTertiary = Colors.white; -const backgroundColor = Color.fromARGB(255, 242, 242, 242); -const onBackgroundColor = Color.fromARGB(255, 0, 0, 0); +const backgroundColor = Color.fromARGB(255, 237, 237, 237); +const onBackgroundColor = Colors.black; -const surface = Color.fromARGB(255, 220, 220, 220); -const onSurface = Color.fromARGB(255, 0, 0, 0); +const surface = Color.fromARGB(255, 255, 255, 255); +const onSurface = Colors.black; const surfaceVariant = Color.fromARGB(255, 170, 170, 170); -const onSurfaceVariant = Color.fromARGB(255, 0, 0, 0); -const inverseSurface = Color.fromARGB(255, 120, 120, 120); -const onInverseSurface = Color.fromARGB(255, 255, 255, 255); +const onSurfaceVariant = Colors.black; +const inverseSurface = Color.fromARGB(255, 90, 90, 90); +const onInverseSurface = Colors.white; -const primaryContainer = Color.fromARGB(255, 200, 200, 200); -const onPrimaryContainer = Color.fromARGB(255, 0, 0, 0); -const secondaryContainer = Color.fromARGB(255, 160, 160, 160); -const onSecondaryContainer = Color.fromARGB(255, 0, 0, 0); -const tertiaryContainer = Color.fromARGB(255, 120, 120, 120); -const onTertiaryContainer = Color.fromARGB(255, 255, 255, 255); +const primaryContainer = Color.fromARGB(255, 105, 75, 180); +const onPrimaryContainer = Colors.white; +const secondaryContainer = Color.fromARGB(255, 75, 160, 240); +const onSecondaryContainer = Colors.white; +const tertiaryContainer = Color.fromARGB(255, 255, 255, 255); +const onTertiaryContainer = Color.fromARGB(255, 0, 0, 0); -const surfaceTint = Colors.transparent; const shadow = Color.fromARGB(255, 60, 60, 60); const outline = Color.fromARGB(255, 30, 30, 30); const disabledColor = Color.fromARGB(255, 100, 100, 100); @@ -71,7 +70,6 @@ ThemeData lightTheme = makeTheme( onErrorContainer: onErrorColor, // other - surfaceTint: surfaceTint, shadow: shadow, outline: outline, disabledColor: disabledColor, @@ -86,5 +84,8 @@ ThemeData lightTheme = makeTheme( appBarTheme: sharedAppBarTheme.copyWith( backgroundColor: Colors.white, foregroundColor: Colors.black, - ) + ), + + // text + primaryTextColor: Colors.black, ); diff --git a/packages/helpwave_theme/lib/src/theme/theme.dart b/packages/helpwave_theme/lib/src/theme/theme.dart index 6c559af3..3454388d 100644 --- a/packages/helpwave_theme/lib/src/theme/theme.dart +++ b/packages/helpwave_theme/lib/src/theme/theme.dart @@ -38,7 +38,6 @@ ThemeData makeTheme({ required Color onErrorContainer, // other - required Color surfaceTint, required Color shadow, required Color outline, required Color disabledColor, @@ -49,15 +48,18 @@ ThemeData makeTheme({ // additional parameters required Brightness brightness, - // Flutter Themes + // flutter themes AppBarTheme appBarTheme = sharedAppBarTheme, + + // text + required Color primaryTextColor }) { return ThemeData( useMaterial3: true, disabledColor: disabledColor, - textSelectionTheme: const TextSelectionThemeData( - cursorColor: Colors.blue, - selectionHandleColor: Colors.blueAccent, + textSelectionTheme: TextSelectionThemeData( + cursorColor: secondaryColor, + selectionHandleColor: secondaryColor, ), scaffoldBackgroundColor: backgroundColor, bottomSheetTheme: BottomSheetThemeData( @@ -102,20 +104,20 @@ ThemeData makeTheme({ }), ), bottomNavigationBarTheme: BottomNavigationBarThemeData( - backgroundColor: backgroundColor, + backgroundColor: surface, ), listTileTheme: ListTileThemeData( - iconColor: focusedColor, + iconColor: primaryTextColor, ), appBarTheme: appBarTheme, elevatedButtonTheme: ElevatedButtonThemeData( style: buttonStyleSmall.copyWith( backgroundColor: resolveByStates( - defaultValue: secondaryColor, + defaultValue: primaryColor, disabled: disabledColor, ), foregroundColor: resolveByStates( - defaultValue: onSecondaryColor, + defaultValue: onPrimaryColor, disabled: onDisabledColor, ), ), @@ -139,24 +141,24 @@ ThemeData makeTheme({ textButtonTheme: TextButtonThemeData( style: buttonStyleSmall.copyWith( backgroundColor: resolveByStates( - defaultValue: secondaryColor, + defaultValue: primaryColor, disabled: disabledColor, ), foregroundColor: resolveByStates( - defaultValue: onSecondaryColor, + defaultValue: onPrimaryColor, disabled: onDisabledColor, ), ), ), iconTheme: IconThemeData( size: iconSizeSmall, - color: primaryColor, + color: primaryTextColor, ), chipTheme: chipTheme.copyWith( - selectedColor: secondaryColor, - secondaryLabelStyle: TextStyle(color: onSecondaryColor), + selectedColor: primaryColor, + secondaryLabelStyle: TextStyle(color: onPrimaryColor), // The TextStyle for selection - labelStyle: TextStyle(color: primaryColor), + labelStyle: TextStyle(color: primaryTextColor), ), cardTheme: CardTheme( elevation: 0, @@ -173,9 +175,11 @@ ThemeData makeTheme({ ), searchBarTheme: searchBarTheme, expansionTileTheme: ExpansionTileThemeData( - textColor: primaryColor, - iconColor: primaryColor, + textColor: primaryTextColor, + iconColor: primaryTextColor, ), + dividerTheme: DividerThemeData(color: primaryTextColor.withOpacity(0.4), space: 1, thickness: 1), + hintColor: primaryTextColor.withOpacity(0.7), colorScheme: ColorScheme( // General brightness: brightness, @@ -194,7 +198,6 @@ ThemeData makeTheme({ error: errorColor, onError: onErrorColor, // Surface - surfaceTint: surfaceTint, surface: surface, onSurface: onSurface, surfaceVariant: surfaceVariant, diff --git a/packages/helpwave_theme/lib/src/theme_model.dart b/packages/helpwave_theme/lib/src/theme_model.dart index 7360b8df..92363861 100644 --- a/packages/helpwave_theme/lib/src/theme_model.dart +++ b/packages/helpwave_theme/lib/src/theme_model.dart @@ -1,5 +1,6 @@ import 'package:flutter/material.dart'; import 'package:helpwave_theme/src/theme_preferences.dart'; +import 'package:helpwave_theme/src/util/context_extension.dart'; /// Model for the Color Theme /// @@ -40,7 +41,7 @@ class ThemeModel extends ChangeNotifier { /// Uses the [BuildContext] to determine the [Theme]'s brightness, which /// corresponds to the System setting bool getIsDarkNullSafe(BuildContext context) => - isDark ?? Theme.of(context).brightness == Brightness.dark; + isDark ?? context.theme.brightness == Brightness.dark; /// Load the preferences with the [ThemePreferences] getPreferences() async { diff --git a/packages/helpwave_theme/lib/src/util/material_state_color_resolver.dart b/packages/helpwave_theme/lib/src/util/material_state_color_resolver.dart index af44f6c2..f866a4d4 100644 --- a/packages/helpwave_theme/lib/src/util/material_state_color_resolver.dart +++ b/packages/helpwave_theme/lib/src/util/material_state_color_resolver.dart @@ -1,4 +1,5 @@ import 'package:flutter/material.dart'; +import 'package:helpwave_theme/src/util/context_extension.dart'; import 'package:helpwave_util/material_state.dart'; /// Resolves the background [Color] based on the [MaterialState] and the [ThemeData] @@ -14,7 +15,7 @@ MaterialStateProperty resolveByStatesAndContextBackground({ Color? hovered, Color? scrolledUnder, }) { - ThemeData theme = Theme.of(context); + ThemeData theme = context.theme; ColorScheme colorScheme = theme.colorScheme; return resolveByStates( defaultValue: defaultValue ?? colorScheme.primary, @@ -42,7 +43,7 @@ MaterialStateProperty resolveByStatesAndContextForeground({ Color? hovered, Color? scrolledUnder, }) { - ThemeData theme = Theme.of(context); + ThemeData theme = context.theme; ColorScheme colorScheme = theme.colorScheme; return resolveByStates( defaultValue: defaultValue ?? colorScheme.onPrimary, diff --git a/packages/helpwave_theme/pubspec.yaml b/packages/helpwave_theme/pubspec.yaml index 8f0e6bff..fdb63398 100644 --- a/packages/helpwave_theme/pubspec.yaml +++ b/packages/helpwave_theme/pubspec.yaml @@ -25,4 +25,4 @@ flutter: fonts: - family: SpaceGrotesk fonts: - - asset: assets/fonts/Space_Grotesk/SpaceGrotesk-VariableFont_wght.ttf + - asset: lib/fonts/Space_Grotesk/SpaceGrotesk-VariableFont_wght.ttf diff --git a/packages/helpwave_widget/lib/lists.dart b/packages/helpwave_widget/lib/lists.dart index 1a309ca1..6e351e70 100644 --- a/packages/helpwave_widget/lib/lists.dart +++ b/packages/helpwave_widget/lib/lists.dart @@ -1 +1 @@ -export 'package:helpwave_widget/src/lists/add_list.dart'; +export 'package:helpwave_widget/src/lists/index.dart'; diff --git a/packages/helpwave_widget/lib/src/bottom_sheets/bottom_sheet_base.dart b/packages/helpwave_widget/lib/src/bottom_sheets/bottom_sheet_base.dart index 451e5735..0e578d9a 100644 --- a/packages/helpwave_widget/lib/src/bottom_sheets/bottom_sheet_base.dart +++ b/packages/helpwave_widget/lib/src/bottom_sheets/bottom_sheet_base.dart @@ -1,5 +1,6 @@ import 'package:flutter/material.dart'; import 'package:helpwave_theme/constants.dart'; +import 'package:helpwave_theme/util.dart'; extension PushModalContextExtension on BuildContext { Future pushModal({ @@ -197,7 +198,7 @@ class BottomSheetHeader extends StatelessWidget { height: 5, width: 30, decoration: BoxDecoration( - color: Theme.of(context).colorScheme.onBackground.withOpacity(0.8), + color: context.theme.colorScheme.onBackground.withOpacity(0.8), borderRadius: BorderRadius.circular(5), ), ), diff --git a/packages/helpwave_widget/lib/src/content_selection/list_entry.dart b/packages/helpwave_widget/lib/src/content_selection/list_entry.dart index 60419f4f..4a6eb2e2 100644 --- a/packages/helpwave_widget/lib/src/content_selection/list_entry.dart +++ b/packages/helpwave_widget/lib/src/content_selection/list_entry.dart @@ -1,5 +1,6 @@ import 'package:flutter/material.dart'; import 'package:helpwave_theme/constants.dart'; +import 'package:helpwave_theme/util.dart'; import 'package:helpwave_widget/src/content_selection/content_selector.dart'; /// The ListEntry used by [ContentSelector] @@ -65,7 +66,7 @@ class ListEntry extends StatelessWidget { Expanded( child: Text( "- $name", - style: Theme.of(context).textTheme.bodyLarge, + style: context.theme.textTheme.bodyLarge, ), ), Row( diff --git a/packages/helpwave_widget/lib/src/content_selection/list_search.dart b/packages/helpwave_widget/lib/src/content_selection/list_search.dart index e57968bc..1961d444 100644 --- a/packages/helpwave_widget/lib/src/content_selection/list_search.dart +++ b/packages/helpwave_widget/lib/src/content_selection/list_search.dart @@ -1,6 +1,7 @@ import 'package:flutter/material.dart'; import 'package:helpwave_localization/localization.dart'; import 'package:helpwave_theme/constants.dart'; +import 'package:helpwave_theme/util.dart'; /// A customizable Search within a List /// @@ -203,7 +204,7 @@ class _ListSearchState extends State> { widget.elementNotFoundText != null ? widget.elementNotFoundText!(_searchController.text) : "${context.localization!.noItem} ${_searchController.text} ${context.localization!.found}", - style: Theme.of(context).textTheme.titleLarge, + style: context.theme.textTheme.titleLarge, ), const SizedBox(height: distanceDefault), widget.allowSelectAnyway @@ -220,7 +221,7 @@ class _ListSearchState extends State> { children = [ Icon( Icons.error_outline, - color: Theme.of(context).colorScheme.error, + color: context.theme.colorScheme.error, size: iconSizeBig, ), const SizedBox(height: distanceBig), diff --git a/packages/helpwave_widget/lib/src/lists/add_list.dart b/packages/helpwave_widget/lib/src/lists/add_list.dart index ea2bbf75..59251908 100644 --- a/packages/helpwave_widget/lib/src/lists/add_list.dart +++ b/packages/helpwave_widget/lib/src/lists/add_list.dart @@ -1,5 +1,6 @@ import 'package:flutter/material.dart'; import 'package:helpwave_theme/constants.dart'; +import 'package:helpwave_theme/util.dart'; import 'package:helpwave_widget/shapes.dart'; /// The default add icon for the [AddList] @@ -12,7 +13,7 @@ class _DefaultAddButton extends StatelessWidget { Widget build(BuildContext context) { const double subtaskAddIconSize = 21; return Circle( - color: Theme.of(context).colorScheme.secondary, + color: context.theme.colorScheme.primary, diameter: subtaskAddIconSize, child: IconButton( padding: EdgeInsets.zero, @@ -20,7 +21,7 @@ class _DefaultAddButton extends StatelessWidget { onPressed: onClick, icon: Icon( Icons.add_rounded, - color: Theme.of(context).colorScheme.onSecondary, + color: context.theme.colorScheme.onPrimary, ), ), ); diff --git a/packages/helpwave_widget/lib/src/lists/index.dart b/packages/helpwave_widget/lib/src/lists/index.dart new file mode 100644 index 00000000..d1f7fa13 --- /dev/null +++ b/packages/helpwave_widget/lib/src/lists/index.dart @@ -0,0 +1,2 @@ +export 'add_list.dart'; +export 'rounded_list_tiles.dart'; diff --git a/packages/helpwave_widget/lib/src/lists/rounded_list_tiles.dart b/packages/helpwave_widget/lib/src/lists/rounded_list_tiles.dart new file mode 100644 index 00000000..a87c0284 --- /dev/null +++ b/packages/helpwave_widget/lib/src/lists/rounded_list_tiles.dart @@ -0,0 +1,56 @@ +import 'package:flutter/material.dart'; +import 'package:helpwave_theme/constants.dart'; +import 'package:helpwave_theme/util.dart'; + +class RoundedListTiles extends StatelessWidget { + final List items; + + const RoundedListTiles({super.key, required this.items}); + + @override + Widget build(BuildContext context) { + const double borderRadius = borderRadiusMedium; + + return Container( + decoration: BoxDecoration( + color: context.theme.colorScheme.surface, + borderRadius: BorderRadius.circular(borderRadius), + boxShadow: [ + BoxShadow( + color: Colors.black.withOpacity(0.1), + blurRadius: 4, + spreadRadius: 1, + ) + ], + ), + child: Column( + mainAxisSize: MainAxisSize.min, + children: List.generate(items.length * 2 - 1, (index) { + if(index.isOdd){ + return const Divider(); + } + Widget item = items[index ~/ 2]; + if (index == 0 && items.length == 1) { + return ClipRRect( + borderRadius: BorderRadius.circular(borderRadius), + child: item, + ); + } + if (index == 0) { + return ClipRRect( + borderRadius: const BorderRadius.vertical(top: Radius.circular(borderRadius)), + child: item, + ); + } + if (index == items.length - 1) { + return ClipRRect( + borderRadius: const BorderRadius.vertical(bottom: Radius.circular(borderRadius)), + child: item, + ); + } + return item; + }), + ), + ); + } +} diff --git a/packages/helpwave_widget/lib/src/loading/loading_spinner.dart b/packages/helpwave_widget/lib/src/loading/loading_spinner.dart index b4e91925..6b6f05e3 100644 --- a/packages/helpwave_widget/lib/src/loading/loading_spinner.dart +++ b/packages/helpwave_widget/lib/src/loading/loading_spinner.dart @@ -1,6 +1,7 @@ import 'package:flutter/material.dart'; import 'package:helpwave_localization/localization.dart'; import 'package:helpwave_theme/constants.dart'; +import 'package:helpwave_theme/util.dart'; /// The Spinner to show when Loading data class LoadingSpinner extends StatelessWidget { @@ -36,7 +37,7 @@ class LoadingSpinner extends StatelessWidget { child: CircularProgressIndicator( strokeWidth: width, semanticsLabel: text ?? context.localization!.loading, - color: color ?? Theme.of(context).colorScheme.secondary, + color: color ?? context.theme.colorScheme.primary, ), ), const SizedBox(height: distanceBig), diff --git a/packages/helpwave_widget/lib/src/text_input/clickable_text_edit.dart b/packages/helpwave_widget/lib/src/text_input/clickable_text_edit.dart index 78fe47db..c3742ab7 100644 --- a/packages/helpwave_widget/lib/src/text_input/clickable_text_edit.dart +++ b/packages/helpwave_widget/lib/src/text_input/clickable_text_edit.dart @@ -1,5 +1,6 @@ import 'package:flutter/material.dart'; import 'package:helpwave_theme/constants.dart'; +import 'package:helpwave_theme/util.dart'; import 'package:helpwave_widget/src/text_input/text_form_field_with_timer.dart'; /// A Widget that is a normal [TextFormField], but has no border @@ -21,7 +22,7 @@ class ClickableTextEdit extends StatefulWidget { /// The default uses: /// /// ```dart - /// color: Theme.of(context).colorScheme.secondary, + /// color: context.theme.colorScheme.primary, /// fontSize: 20, /// fontWeight: FontWeight.w700 /// ``` @@ -75,10 +76,14 @@ class _ClickableTextEditState extends State { ); TextStyle usedTextStyle = widget.textStyle ?? - TextStyle(color: Theme.of(context).colorScheme.secondary, fontSize: 20, fontWeight: FontWeight.w700); + TextStyle( + color: context.theme.colorScheme.primary, + fontSize: 20, + fontWeight: FontWeight.w700, + ); return Theme( - data: Theme.of(context).copyWith( + data: context.theme.copyWith( inputDecorationTheme: const InputDecorationTheme( contentPadding: EdgeInsets.zero, border: borderTheme, From 59888d5e3fbb92a0b84849c123ce9155234cb47c Mon Sep 17 00:00:00 2001 From: Felix Thape Date: Thu, 19 Sep 2024 17:35:23 +0200 Subject: [PATCH 10/36] feat: add organization bottom sheet --- .../assignee_select.dart | 2 +- .../organization_bottom_sheet.dart | 112 ++++++ .../patient_bottom_sheet.dart | 4 +- .../task_bottom_sheet.dart | 4 +- .../bottom_sheet_pages/user_bottom_sheet.dart | 127 +++++++ .../ward_select_bottom_sheet.dart | 42 ++- .../lib/components/task_expansion_tile.dart | 2 +- .../lib/components/user_bottom_sheet.dart | 126 ------- apps/tasks/lib/components/user_header.dart | 5 +- .../lib/components/visibility_select.dart | 42 ++- apps/tasks/lib/main.dart | 4 +- apps/tasks/lib/screens/main_screen.dart | 4 +- .../patient_screen.dart | 4 +- apps/tasks/lib/screens/settings_screen.dart | 337 +++++++++--------- .../helpwave_localization/lib/l10n/app_de.arb | 10 +- .../helpwave_localization/lib/l10n/app_en.arb | 10 +- .../src/api/offline/offline_client_store.dart | 2 +- .../lib/src/api/user/controllers/index.dart | 1 + .../controllers/organization_controller.dart | 85 +++++ .../src/api/user/data_types/organization.dart | 24 ++ .../organization_offline_client.dart | 32 +- .../api/user/services/organization_svc.dart | 37 +- .../lib/src/auth/current_ward_svc.dart | 43 ++- .../lib/src/auth/user_session_service.dart | 5 +- .../src/bottom_sheets/bottom_sheet_base.dart | 65 +++- .../bottom_sheets/bottom_sheet_navigator.dart | 46 --- .../src/bottom_sheets/bottom_sheet_page.dart | 36 ++ .../bottom_sheet_page_builder.dart | 20 -- .../lib/src/bottom_sheets/index.dart | 3 +- .../lib/src/navigation/index.dart | 4 +- .../src/navigation/navigation_controller.dart | 27 -- .../lib/src/navigation/navigation_outlet.dart | 57 +++ .../navigation_stack_controller.dart | 39 ++ .../lib/src/navigation/simple_navigator.dart | 35 -- .../lib/src/text_input/index.dart | 3 + packages/helpwave_widget/lib/text_input.dart | 4 +- 36 files changed, 882 insertions(+), 521 deletions(-) rename apps/tasks/lib/components/{ => bottom_sheet_pages}/assignee_select.dart (97%) create mode 100644 apps/tasks/lib/components/bottom_sheet_pages/organization_bottom_sheet.dart rename apps/tasks/lib/components/{ => bottom_sheet_pages}/patient_bottom_sheet.dart (99%) rename apps/tasks/lib/components/{ => bottom_sheet_pages}/task_bottom_sheet.dart (99%) create mode 100644 apps/tasks/lib/components/bottom_sheet_pages/user_bottom_sheet.dart rename apps/tasks/lib/components/{ => bottom_sheet_pages}/ward_select_bottom_sheet.dart (57%) delete mode 100644 apps/tasks/lib/components/user_bottom_sheet.dart create mode 100644 packages/helpwave_service/lib/src/api/user/controllers/organization_controller.dart delete mode 100644 packages/helpwave_widget/lib/src/bottom_sheets/bottom_sheet_navigator.dart create mode 100644 packages/helpwave_widget/lib/src/bottom_sheets/bottom_sheet_page.dart delete mode 100644 packages/helpwave_widget/lib/src/bottom_sheets/bottom_sheet_page_builder.dart delete mode 100644 packages/helpwave_widget/lib/src/navigation/navigation_controller.dart create mode 100644 packages/helpwave_widget/lib/src/navigation/navigation_outlet.dart create mode 100644 packages/helpwave_widget/lib/src/navigation/navigation_stack_controller.dart delete mode 100644 packages/helpwave_widget/lib/src/navigation/simple_navigator.dart create mode 100644 packages/helpwave_widget/lib/src/text_input/index.dart diff --git a/apps/tasks/lib/components/assignee_select.dart b/apps/tasks/lib/components/bottom_sheet_pages/assignee_select.dart similarity index 97% rename from apps/tasks/lib/components/assignee_select.dart rename to apps/tasks/lib/components/bottom_sheet_pages/assignee_select.dart index 3e314f90..230a0dce 100644 --- a/apps/tasks/lib/components/assignee_select.dart +++ b/apps/tasks/lib/components/bottom_sheet_pages/assignee_select.dart @@ -25,7 +25,7 @@ class AssigneeSelectBottomSheet extends StatelessWidget { titleText: context.localization!.assignee, ), onClosing: () => {}, - builder: (context) => Column( + child: Column( children: [ TextButton( child: Text(context.localization!.remove), diff --git a/apps/tasks/lib/components/bottom_sheet_pages/organization_bottom_sheet.dart b/apps/tasks/lib/components/bottom_sheet_pages/organization_bottom_sheet.dart new file mode 100644 index 00000000..24a296d9 --- /dev/null +++ b/apps/tasks/lib/components/bottom_sheet_pages/organization_bottom_sheet.dart @@ -0,0 +1,112 @@ +import 'package:flutter/material.dart'; +import 'package:helpwave_localization/localization.dart'; +import 'package:helpwave_service/user.dart'; +import 'package:helpwave_theme/constants.dart'; +import 'package:helpwave_theme/util.dart'; +import 'package:helpwave_widget/bottom_sheets.dart'; +import 'package:helpwave_widget/lists.dart'; +import 'package:helpwave_widget/loading.dart'; +import 'package:helpwave_widget/text_input.dart'; +import 'package:provider/provider.dart'; +import 'package:tasks/screens/settings_screen.dart'; + +class OrganizationBottomSheetPage extends StatelessWidget { + final String organizationId; + + const OrganizationBottomSheetPage({super.key, required this.organizationId}); + + @override + Widget build(BuildContext context) { + return ChangeNotifierProvider( + create: (context) => OrganizationController(Organization.empty(organizationId)), + child: Consumer(builder: (context, controller, _) { + return BottomSheetPage( + header: BottomSheetHeader.navigation( + context, + title: LoadingAndErrorWidget.pulsing( + state: controller.state, + child: Text(controller.organization.combinedName), + ), + ), + child: Flexible( + child: LoadingAndErrorWidget( + state: controller.state, + child: ListView( + children: [ + Flexible( + child: ListView( + shrinkWrap: true, + children: [ + const SizedBox(height: distanceMedium), + Text(context.localization!.shortName), + TextFormFieldWithTimer( + initialValue: controller.organization.shortName, + onUpdate: (value) => controller.update(shortName: value), + ), + const SizedBox(height: distanceMedium), + Text(context.localization!.longName), + TextFormFieldWithTimer( + initialValue: controller.organization.longName, + onUpdate: (value) => controller.update(longName: value), + ), + const SizedBox(height: distanceMedium), + Text(context.localization!.contactEmail), + TextFormFieldWithTimer( + initialValue: controller.organization.email, + // TODO validation + onUpdate: (value) => controller.update(email: value), + ), + const SizedBox(height: distanceMedium), + Text(context.localization!.settings), + RoundedListTiles( + items: [ + NavigationListTile( + icon: Icons.house_rounded, + title: context.localization!.wards, + onTap: () { + // TODO navigate to ward page + }, + ), + NavigationListTile( + icon: Icons.person, + title: context.localization!.members, + onTap: () { + // TODO navigate to members page + }, + ), + NavigationListTile( + icon: Icons.label, + title: context.localization!.properties, + onTap: () { + // TODO navigate to properties page + }, + ) + ], + ), + const SizedBox(height: distanceMedium), + Text(context.localization!.dangerZone), + Text( + context.localization!.organizationDangerZoneDescription, + style: TextStyle(color: context.theme.hintColor), + ), + TextButton( + child: Text( + "${context.localization!.delete} ${context.localization!.organization}", + style: const TextStyle(color: Colors.red), // TODO get from theme + ), + onPressed: () { + // TODO show modal and delete organization + }, + ), + ], + ), + ), + ], + ), + ), + ), + ); + }), + ); + } +} diff --git a/apps/tasks/lib/components/patient_bottom_sheet.dart b/apps/tasks/lib/components/bottom_sheet_pages/patient_bottom_sheet.dart similarity index 99% rename from apps/tasks/lib/components/patient_bottom_sheet.dart rename to apps/tasks/lib/components/bottom_sheet_pages/patient_bottom_sheet.dart index f5a0cca3..68d7c21e 100644 --- a/apps/tasks/lib/components/patient_bottom_sheet.dart +++ b/apps/tasks/lib/components/bottom_sheet_pages/patient_bottom_sheet.dart @@ -9,7 +9,7 @@ import 'package:helpwave_widget/lists.dart'; import 'package:helpwave_widget/loading.dart'; import 'package:helpwave_widget/text_input.dart'; import 'package:provider/provider.dart'; -import 'package:tasks/components/task_bottom_sheet.dart'; +import 'package:tasks/components/bottom_sheet_pages/task_bottom_sheet.dart'; import 'package:tasks/components/task_expansion_tile.dart'; import 'package:helpwave_service/tasks.dart'; import 'package:helpwave_util/loading.dart'; @@ -147,7 +147,7 @@ class _PatientBottomSheetState extends State { )); }), ), - builder: (BuildContext context) => SingleChildScrollView( + child: SingleChildScrollView( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ diff --git a/apps/tasks/lib/components/task_bottom_sheet.dart b/apps/tasks/lib/components/bottom_sheet_pages/task_bottom_sheet.dart similarity index 99% rename from apps/tasks/lib/components/task_bottom_sheet.dart rename to apps/tasks/lib/components/bottom_sheet_pages/task_bottom_sheet.dart index 212311e5..c3f54fb2 100644 --- a/apps/tasks/lib/components/task_bottom_sheet.dart +++ b/apps/tasks/lib/components/bottom_sheet_pages/task_bottom_sheet.dart @@ -9,7 +9,7 @@ import 'package:helpwave_widget/bottom_sheets.dart'; import 'package:helpwave_widget/loading.dart'; import 'package:helpwave_widget/text_input.dart'; import 'package:provider/provider.dart'; -import 'package:tasks/components/assignee_select.dart'; +import 'package:tasks/components/bottom_sheet_pages/assignee_select.dart'; import 'package:tasks/components/subtask_list.dart'; import 'package:tasks/components/visibility_select.dart'; import 'package:helpwave_service/tasks.dart'; @@ -160,7 +160,7 @@ class _TaskBottomSheetState extends State { ) : const SizedBox(), ), - builder: (context) => Flexible( + child: Flexible( child: ListView( children: [ Center( diff --git a/apps/tasks/lib/components/bottom_sheet_pages/user_bottom_sheet.dart b/apps/tasks/lib/components/bottom_sheet_pages/user_bottom_sheet.dart new file mode 100644 index 00000000..b6c482fe --- /dev/null +++ b/apps/tasks/lib/components/bottom_sheet_pages/user_bottom_sheet.dart @@ -0,0 +1,127 @@ +import 'package:flutter/material.dart'; +import 'package:helpwave_localization/localization.dart'; +import 'package:helpwave_service/user.dart'; +import 'package:helpwave_theme/constants.dart'; +import 'package:helpwave_theme/util.dart'; +import 'package:helpwave_widget/bottom_sheets.dart'; +import 'package:helpwave_widget/lists.dart'; +import 'package:helpwave_widget/loading.dart'; +import 'package:helpwave_widget/navigation.dart'; +import 'package:provider/provider.dart'; +import 'package:helpwave_service/tasks.dart'; +import 'package:helpwave_service/auth.dart'; +import 'package:tasks/components/bottom_sheet_pages/ward_select_bottom_sheet.dart'; +import 'package:tasks/screens/login_screen.dart'; +import 'package:helpwave_widget/widgets.dart'; +import 'package:tasks/screens/settings_screen.dart'; + +class UserBottomSheetPage extends StatelessWidget { + const UserBottomSheetPage({super.key}); + + @override + Widget build(BuildContext context) { + return BottomSheetPage( + header: BottomSheetHeader.navigation( + context, + trailing: BottomSheetAction( + icon: Icons.settings, + onPressed: () => NavigationStackController.of(context).push(const SettingsBottomSheetPage()), + ), + ), + child: Flexible( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Padding( + padding: const EdgeInsets.all(paddingSmall).copyWith(top: paddingMedium), + child: LoadingFutureBuilder( + data: UserService().getSelf(), + thenWidgetBuilder: (context, data) { + return CircleAvatar( + backgroundColor: Colors.transparent, + radius: iconSizeMedium, + foregroundImage: NetworkImage(data.profileUrl.toString()), + ); + }, + loadingWidget: const FallbackAvatar(size: iconSizeMedium), + errorWidget: const FallbackAvatar(size: iconSizeMedium), + ), + ), + Consumer(builder: (context, userSessionController, _) { + return Text( + userSessionController.identity!.name, + style: const TextStyle(fontSize: fontSizeBig), + ); + }), + Consumer( + builder: (context, currentWardController, __) => Text( + currentWardController.currentWard?.organizationName ?? context.localization!.loading, + style: TextStyle( + fontSize: fontSizeSmall, + color: context.theme.hintColor, + ), + ), + ), + const SizedBox(height: distanceBig), + RoundedListTiles(items: [ + ListTile( + leading: Icon( + Icons.house_rounded, + color: context.theme.colorScheme.primary, + ), + title: Text(context.localization!.currentWard, style: const TextStyle(fontWeight: FontWeight.bold)), + trailing: Row( + mainAxisSize: MainAxisSize.min, + mainAxisAlignment: MainAxisAlignment.end, + children: [ + Consumer(builder: (context, currentWardController, __) { + return Text( + currentWardController.currentWard!.wardName, + style: context.theme.textTheme.labelLarge, + ); + }), + const Icon(Icons.chevron_right_rounded), + ], + ), + onTap: () => { + context.pushModal( + context: context, + builder: (context) => WardSelectBottomSheet( + selectedWardId: CurrentWardService().currentWard!.wardId, + onChange: (WardMinimal ward) { + CurrentWardService().currentWard = CurrentWardInformation( + ward, + CurrentWardService().currentWard!.organization, + ); + Navigator.pop(context); + }, + ), + ) + }, + ), + ]), + const Spacer(), + Padding( + padding: const EdgeInsets.only(bottom: distanceMedium), + child: Consumer(builder: (context, currentWardService, _) { + return TextButton( + style: buttonStyleMedium, + onPressed: () { + Navigator.of(context).pushReplacement( + MaterialPageRoute(builder: (_) => const LoginScreen()), + ); + UserSessionService().logout(); + currentWardService.clear(); + }, + child: Text( + context.localization!.logout, + ), + ); + }), + ), + ], + ), + ), + ); + } +} diff --git a/apps/tasks/lib/components/ward_select_bottom_sheet.dart b/apps/tasks/lib/components/bottom_sheet_pages/ward_select_bottom_sheet.dart similarity index 57% rename from apps/tasks/lib/components/ward_select_bottom_sheet.dart rename to apps/tasks/lib/components/bottom_sheet_pages/ward_select_bottom_sheet.dart index e84d083d..59624e5d 100644 --- a/apps/tasks/lib/components/ward_select_bottom_sheet.dart +++ b/apps/tasks/lib/components/bottom_sheet_pages/ward_select_bottom_sheet.dart @@ -26,28 +26,26 @@ class WardSelectBottomSheet extends StatelessWidget { onClosing: () {}, header: BottomSheetHeader(titleText: context.localization!.selectWard), mainAxisSize: MainAxisSize.min, - builder: (context) { - return LoadingFutureBuilder( - loadingWidget: const SizedBox(), - data: WardService().getWards(organizationId: organizationId), - thenWidgetBuilder: (context, wards) { - return Flexible( - child: ListView( - shrinkWrap: true, - children: wards - .map((ward) => ListTile( - onTap: () => onChange(ward), - title: Text( - ward.name, - style:TextStyle(decoration: ward.id == selectedWardId ? TextDecoration.underline : null), - ), - )) - .toList(), - ), - ); - }, - ); - }, + child: LoadingFutureBuilder( + loadingWidget: const SizedBox(), + data: WardService().getWards(organizationId: organizationId), + thenWidgetBuilder: (context, wards) { + return Flexible( + child: ListView( + shrinkWrap: true, + children: wards + .map((ward) => ListTile( + onTap: () => onChange(ward), + title: Text( + ward.name, + style: TextStyle(decoration: ward.id == selectedWardId ? TextDecoration.underline : null), + ), + )) + .toList(), + ), + ); + }, + ), ); } } diff --git a/apps/tasks/lib/components/task_expansion_tile.dart b/apps/tasks/lib/components/task_expansion_tile.dart index 5b9e5725..4b050704 100644 --- a/apps/tasks/lib/components/task_expansion_tile.dart +++ b/apps/tasks/lib/components/task_expansion_tile.dart @@ -4,7 +4,7 @@ import 'package:helpwave_theme/util.dart'; import 'package:helpwave_widget/bottom_sheets.dart'; import 'package:helpwave_widget/shapes.dart'; import 'package:provider/provider.dart'; -import 'package:tasks/components/task_bottom_sheet.dart'; +import 'package:tasks/components/bottom_sheet_pages/task_bottom_sheet.dart'; import 'package:tasks/components/task_card.dart'; import 'package:helpwave_service/tasks.dart'; diff --git a/apps/tasks/lib/components/user_bottom_sheet.dart b/apps/tasks/lib/components/user_bottom_sheet.dart deleted file mode 100644 index f3f649e2..00000000 --- a/apps/tasks/lib/components/user_bottom_sheet.dart +++ /dev/null @@ -1,126 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:helpwave_localization/localization.dart'; -import 'package:helpwave_service/user.dart'; -import 'package:helpwave_theme/constants.dart'; -import 'package:helpwave_theme/util.dart'; -import 'package:helpwave_widget/bottom_sheets.dart'; -import 'package:helpwave_widget/lists.dart'; -import 'package:helpwave_widget/loading.dart'; -import 'package:helpwave_widget/navigation.dart'; -import 'package:provider/provider.dart'; -import 'package:helpwave_service/tasks.dart'; -import 'package:helpwave_service/auth.dart'; -import 'package:tasks/components/ward_select_bottom_sheet.dart'; -import 'package:tasks/screens/login_screen.dart'; -import 'package:helpwave_widget/widgets.dart'; -import 'package:tasks/screens/settings_screen.dart'; - -class UserBottomSheetPageBuilder with BottomSheetPageBuilder { - @override - BottomSheetHeader? headerBuilder(BuildContext context, NavigationController controller) { - return BottomSheetHeader( - trailing: BottomSheetAction( - icon: Icons.settings, - onPressed: () => controller.push(SettingsBottomSheetPageBuilder()), - ), - ); - } - - @override - Widget build(BuildContext context, NavigationController controller) { - return Flexible( - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Padding( - padding: const EdgeInsets.all(paddingSmall).copyWith(top: paddingMedium), - child: LoadingFutureBuilder( - data: UserService().getSelf(), - thenWidgetBuilder: (context, data) { - return CircleAvatar( - backgroundColor: Colors.transparent, - radius: iconSizeMedium, - foregroundImage: NetworkImage(data.profileUrl.toString()), - ); - }, - loadingWidget: const FallbackAvatar(size: iconSizeMedium), - errorWidget: const FallbackAvatar(size: iconSizeMedium), - ), - ), - Consumer(builder: (context, userSessionController, _) { - return Text( - userSessionController.identity!.name, - style: const TextStyle(fontSize: fontSizeBig), - ); - }), - Consumer( - builder: (context, currentWardController, __) => Text( - currentWardController.currentWard?.organizationName ?? context.localization!.loading, - style: TextStyle( - fontSize: fontSizeSmall, - color: context.theme.hintColor, - ), - ), - ), - const SizedBox(height: distanceBig), - RoundedListTiles(items: [ - ListTile( - leading: Icon( - Icons.house_rounded, - color: context.theme.colorScheme.primary, - ), - title: Text(context.localization!.currentWard, style: const TextStyle(fontWeight: FontWeight.bold)), - trailing: Row( - mainAxisSize: MainAxisSize.min, - mainAxisAlignment: MainAxisAlignment.end, - children: [ - Consumer(builder: (context, currentWardController, __) { - return Text( - currentWardController.currentWard!.wardName, - style: context.theme.textTheme.labelLarge, - ); - }), - const Icon(Icons.chevron_right_rounded), - ], - ), - onTap: () => { - context.pushModal( - context: context, - builder: (context) => WardSelectBottomSheet( - selectedWardId: CurrentWardService().currentWard!.wardId, - onChange: (WardMinimal ward) { - CurrentWardService().currentWard = CurrentWardInformation( - ward, - CurrentWardService().currentWard!.organization, - ); - Navigator.pop(context); - }, - ), - ) - }, - ), - ]), - const Spacer(), - Padding( - padding: const EdgeInsets.only(bottom: distanceMedium), - child: Consumer(builder: (context, currentWardService, _) { - return TextButton( - style: buttonStyleMedium, - onPressed: () { - Navigator.of(context).pushReplacement( - MaterialPageRoute(builder: (_) => const LoginScreen()), - ); - UserSessionService().logout(); - currentWardService.clear(); - }, - child: Text( - context.localization!.logout, - ), - ); - }), - ), - ], - ), - ); - } -} diff --git a/apps/tasks/lib/components/user_header.dart b/apps/tasks/lib/components/user_header.dart index 1e942115..ecd98ff3 100644 --- a/apps/tasks/lib/components/user_header.dart +++ b/apps/tasks/lib/components/user_header.dart @@ -6,9 +6,10 @@ import 'package:helpwave_theme/constants.dart'; import 'package:helpwave_theme/util.dart'; import 'package:helpwave_widget/bottom_sheets.dart'; import 'package:helpwave_widget/loading.dart'; +import 'package:helpwave_widget/navigation.dart'; import 'package:helpwave_widget/widgets.dart'; import 'package:provider/provider.dart'; -import 'package:tasks/components/user_bottom_sheet.dart'; +import 'package:tasks/components/bottom_sheet_pages/user_bottom_sheet.dart'; /// A [AppBar] for displaying the current [User], [Organization] and [Ward] class UserHeader extends StatelessWidget implements PreferredSizeWidget { @@ -45,7 +46,7 @@ class UserHeader extends StatelessWidget implements PreferredSizeWidget { onTap: () { context.pushModal( context: context, - builder: (context) => BottomSheetNavigator(initialPageBuilder: UserBottomSheetPageBuilder()), + builder: (context) => NavigationOutlet(initialValue: const UserBottomSheetPage()), ); }, title: Text( diff --git a/apps/tasks/lib/components/visibility_select.dart b/apps/tasks/lib/components/visibility_select.dart index 2ae8ca04..fd4494fd 100644 --- a/apps/tasks/lib/components/visibility_select.dart +++ b/apps/tasks/lib/components/visibility_select.dart @@ -28,28 +28,26 @@ class _VisibilityBottomSheet extends StatelessWidget { header: BottomSheetHeader( titleText: context.localization!.visibility, ), - builder: (context) { - return ListView( - children: [ - const SizedBox(height: distanceSmall), - GestureDetector( - onTap: () { - onChange(true); - Navigator.of(context).pop(); - }, - child: Text(context.localization!.public, style: style), - ), - const SizedBox(height: distanceSmall), - GestureDetector( - onTap: () { - onChange(false); - Navigator.of(context).pop(); - }, - child: Text(context.localization!.private, style: style), - ), - ], - ); - }, + child: ListView( + children: [ + const SizedBox(height: distanceSmall), + GestureDetector( + onTap: () { + onChange(true); + Navigator.of(context).pop(); + }, + child: Text(context.localization!.public, style: style), + ), + const SizedBox(height: distanceSmall), + GestureDetector( + onTap: () { + onChange(false); + Navigator.of(context).pop(); + }, + child: Text(context.localization!.private, style: style), + ), + ], + ), ); } } diff --git a/apps/tasks/lib/main.dart b/apps/tasks/lib/main.dart index 0350f9c2..d88d62a2 100644 --- a/apps/tasks/lib/main.dart +++ b/apps/tasks/lib/main.dart @@ -11,13 +11,15 @@ import 'package:helpwave_service/tasks.dart'; import 'package:tasks/screens/login_screen.dart'; void main() { - UserSessionService().changeMode(devMode); + WidgetsFlutterBinding.ensureInitialized(); + CurrentWardService().devMode = true; TasksAPIServiceClients() ..apiUrl = usedAPIURL ..offlineMode = true; UserAPIServiceClients() ..apiUrl = usedAPIURL ..offlineMode = true; + UserSessionService().changeMode(devMode); runApp(const MyApp()); } diff --git a/apps/tasks/lib/screens/main_screen.dart b/apps/tasks/lib/screens/main_screen.dart index c2ec10fb..e6befd3c 100644 --- a/apps/tasks/lib/screens/main_screen.dart +++ b/apps/tasks/lib/screens/main_screen.dart @@ -9,8 +9,8 @@ import 'package:helpwave_theme/util.dart'; import 'package:helpwave_widget/bottom_sheets.dart'; import 'package:helpwave_widget/animation.dart'; import 'package:provider/provider.dart'; -import 'package:tasks/components/patient_bottom_sheet.dart'; -import 'package:tasks/components/task_bottom_sheet.dart'; +import 'package:tasks/components/bottom_sheet_pages/patient_bottom_sheet.dart'; +import 'package:tasks/components/bottom_sheet_pages/task_bottom_sheet.dart'; import 'package:tasks/components/user_header.dart'; import 'package:tasks/screens/main_screen_subscreens/my_tasks_screen.dart'; import 'package:tasks/screens/main_screen_subscreens/patient_screen.dart'; diff --git a/apps/tasks/lib/screens/main_screen_subscreens/patient_screen.dart b/apps/tasks/lib/screens/main_screen_subscreens/patient_screen.dart index fdef6aa0..663d89f8 100644 --- a/apps/tasks/lib/screens/main_screen_subscreens/patient_screen.dart +++ b/apps/tasks/lib/screens/main_screen_subscreens/patient_screen.dart @@ -5,10 +5,10 @@ import 'package:helpwave_theme/util.dart'; import 'package:helpwave_widget/bottom_sheets.dart'; import 'package:helpwave_widget/loading.dart'; import 'package:provider/provider.dart'; -import 'package:tasks/components/patient_bottom_sheet.dart'; +import 'package:tasks/components/bottom_sheet_pages/patient_bottom_sheet.dart'; import 'package:tasks/components/patient_card.dart'; import 'package:tasks/components/patient_status_chip_select.dart'; -import 'package:tasks/components/task_bottom_sheet.dart'; +import 'package:tasks/components/bottom_sheet_pages/task_bottom_sheet.dart'; import 'package:helpwave_service/tasks.dart'; /// A screen for showing a all [Patient]s by certain user-selectable filter properties diff --git a/apps/tasks/lib/screens/settings_screen.dart b/apps/tasks/lib/screens/settings_screen.dart index e21ac3ae..c39f213a 100644 --- a/apps/tasks/lib/screens/settings_screen.dart +++ b/apps/tasks/lib/screens/settings_screen.dart @@ -11,6 +11,7 @@ import 'package:helpwave_widget/lists.dart'; import 'package:helpwave_widget/loading.dart'; import 'package:helpwave_widget/navigation.dart'; import 'package:provider/provider.dart'; +import 'package:tasks/components/bottom_sheet_pages/organization_bottom_sheet.dart'; import 'package:tasks/screens/login_screen.dart'; /// Screen for settings and other app options @@ -172,6 +173,7 @@ class NavigationListTile extends StatelessWidget { icon, color: color ?? context.theme.colorScheme.primary, ), + onTap: onTap, title: Text(title, style: TextStyle(fontWeight: FontWeight.bold, color: color)), trailing: Row( mainAxisSize: MainAxisSize.min, @@ -193,16 +195,11 @@ class NavigationListTile extends StatelessWidget { } } -class SettingsBottomSheetPageBuilder with BottomSheetPageBuilder { - @override - BottomSheetHeader? headerBuilder(BuildContext context, NavigationController controller) { - return BottomSheetHeader( - titleText: context.localization!.settings, - ); - } +class SettingsBottomSheetPage extends StatelessWidget { + const SettingsBottomSheetPage({super.key}); @override - Widget build(BuildContext context, NavigationController controller) { + Widget build(BuildContext context) { titleBuilder(String title) { return Padding( padding: const EdgeInsets.only(bottom: paddingSmall), @@ -213,169 +210,179 @@ class SettingsBottomSheetPageBuilder with BottomSheetPageBuilder { ); } - return Flexible( - child: ListView( - children: [ - titleBuilder(context.localization!.personalSettings), - RoundedListTiles( - items: [ - NavigationListTile( - icon: Icons.person, - title: context.localization!.personalData, - onTap: () {}, - ), - NavigationListTile( - icon: Icons.security_rounded, - title: context.localization!.passwordAndSecurity, - onTap: () {}, - ), - NavigationListTile( - icon: Icons.checklist_rounded, - title: context.localization!.myTaskTemplates, - onTap: () {}, - ), - ], - ), - const SizedBox(height: distanceMedium), - titleBuilder(context.localization!.myOrganizations), - LoadingFutureBuilder( - data: OrganizationService().getOrganizationsForUser(), - thenWidgetBuilder: (context, data) { - return RoundedListTiles( - items: data - .map((organization) => NavigationListTile( - icon: Icons.apartment_rounded, - title: organization.longName, - onTap: () {}, - )) - .toList(), - ); - }, - ), - const SizedBox(height: distanceMedium), - titleBuilder(context.localization!.appearance), - RoundedListTiles( - items: [ - ListTile( - leading: Icon(Icons.brightness_medium, color: context.theme.colorScheme.primary), - title: Text(context.localization!.darkMode, style: const TextStyle(fontWeight: FontWeight.bold)), - trailing: Consumer( - builder: (_, ThemeModel themeNotifier, __) { - return PopupMenuButton( - initialValue: themeNotifier.themeMode, - position: PopupMenuPosition.under, - itemBuilder: (context) => [ - PopupMenuItem(value: ThemeMode.dark, child: Text(context.localization!.darkMode)), - PopupMenuItem(value: ThemeMode.light, child: Text(context.localization!.lightMode)), - PopupMenuItem(value: ThemeMode.system, child: Text(context.localization!.system)), - ], - onSelected: (value) { - if (value == ThemeMode.system) { - themeNotifier.isDark = null; - } else { - themeNotifier.isDark = value == ThemeMode.dark; - } - }, - child: Row( - mainAxisSize: MainAxisSize.min, - children: [ - Text( - { - ThemeMode.dark: context.localization!.darkMode, - ThemeMode.light: context.localization!.lightMode, - ThemeMode.system: context.localization!.system, - }[themeNotifier.themeMode]!, - style: const TextStyle( - fontSize: 14, - fontWeight: FontWeight.w800, - )), - const SizedBox( - width: distanceTiny, - ), - const Icon( - Icons.expand_more_rounded, - size: iconSizeTiny, - ), + return BottomSheetPage( + header: BottomSheetHeader.navigation( + context, + titleText: context.localization!.settings, + ), + child: Flexible( + child: ListView( + children: [ + titleBuilder(context.localization!.personalSettings), + RoundedListTiles( + items: [ + NavigationListTile( + icon: Icons.person, + title: context.localization!.personalData, + onTap: () {}, + ), + NavigationListTile( + icon: Icons.security_rounded, + title: context.localization!.passwordAndSecurity, + onTap: () {}, + ), + NavigationListTile( + icon: Icons.checklist_rounded, + title: context.localization!.myTaskTemplates, + onTap: () {}, + ), + ], + ), + const SizedBox(height: distanceMedium), + titleBuilder(context.localization!.myOrganizations), + LoadingFutureBuilder( + data: OrganizationService().getOrganizationsForUser(), + thenWidgetBuilder: (context, data) { + return RoundedListTiles( + items: data + .map((organization) => NavigationListTile( + icon: Icons.apartment_rounded, + title: organization.longName, + onTap: () { + print("tap"); + NavigationStackController.of(context) + .push(OrganizationBottomSheetPage(organizationId: organization.id)); + }, + )) + .toList(), + ); + }, + ), + const SizedBox(height: distanceMedium), + titleBuilder(context.localization!.appearance), + RoundedListTiles( + items: [ + ListTile( + leading: Icon(Icons.brightness_medium, color: context.theme.colorScheme.primary), + title: Text(context.localization!.darkMode, style: const TextStyle(fontWeight: FontWeight.bold)), + trailing: Consumer( + builder: (_, ThemeModel themeNotifier, __) { + return PopupMenuButton( + initialValue: themeNotifier.themeMode, + position: PopupMenuPosition.under, + itemBuilder: (context) => [ + PopupMenuItem(value: ThemeMode.dark, child: Text(context.localization!.darkMode)), + PopupMenuItem(value: ThemeMode.light, child: Text(context.localization!.lightMode)), + PopupMenuItem(value: ThemeMode.system, child: Text(context.localization!.system)), ], + onSelected: (value) { + if (value == ThemeMode.system) { + themeNotifier.isDark = null; + } else { + themeNotifier.isDark = value == ThemeMode.dark; + } + }, + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Text( + { + ThemeMode.dark: context.localization!.darkMode, + ThemeMode.light: context.localization!.lightMode, + ThemeMode.system: context.localization!.system, + }[themeNotifier.themeMode]!, + style: const TextStyle( + fontSize: 14, + fontWeight: FontWeight.w800, + )), + const SizedBox( + width: distanceTiny, + ), + const Icon( + Icons.expand_more_rounded, + size: iconSizeTiny, + ), + ], + ), + ); + }, + ), + ), + Consumer( + builder: (context, languageModel, child) { + return ListTile( + leading: Icon(Icons.language, color: context.theme.colorScheme.primary), + title: Text( + context.localization!.language, + style: const TextStyle(fontWeight: FontWeight.bold), + ), + trailing: PopupMenuButton( + position: PopupMenuPosition.under, + initialValue: languageModel.local, + onSelected: (value) { + languageModel.setLanguage(value); + }, + itemBuilder: (BuildContext context) => getSupportedLocalsWithName() + .map((local) => PopupMenuItem( + value: local.local, + child: Text( + local.name, + ), + )) + .toList(), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Text(languageModel.name, + style: const TextStyle( + fontSize: 14, + fontWeight: FontWeight.w800, + )), + const SizedBox( + width: distanceTiny, + ), + const Icon( + Icons.expand_more_rounded, + size: iconSizeTiny, + ), + ], + ), ), ); }, ), - ), - Consumer( - builder: (context, languageModel, child) { - return ListTile( - leading: Icon(Icons.language, color: context.theme.colorScheme.primary), - title: Text( - context.localization!.language, - style: const TextStyle(fontWeight: FontWeight.bold), - ), - trailing: PopupMenuButton( - position: PopupMenuPosition.under, - initialValue: languageModel.local, - onSelected: (value) { - languageModel.setLanguage(value); + ], + ), + const SizedBox(height: distanceMedium), + titleBuilder(context.localization!.other), + RoundedListTiles( + items: [ + NavigationListTile( + icon: Icons.info_outline, + title: context.localization!.licenses, + onTap: () => {showLicensePage(context: context)}, + ), + Consumer( + builder: (context, currentWardService, _) { + return NavigationListTile( + icon: Icons.logout, + title: context.localization!.logout, + color: Colors.red.withOpacity(0.7), // TODO get this from theme + onTap: () { + // TODO add confirm dialog + UserSessionService().logout(); + currentWardService.clear(); + Navigator.of(context).pushReplacement( + MaterialPageRoute(builder: (_) => const LoginScreen()), + ); }, - itemBuilder: (BuildContext context) => getSupportedLocalsWithName() - .map((local) => PopupMenuItem( - value: local.local, - child: Text( - local.name, - ), - )) - .toList(), - child: Row( - mainAxisSize: MainAxisSize.min, - children: [ - Text(languageModel.name, - style: const TextStyle( - fontSize: 14, - fontWeight: FontWeight.w800, - )), - const SizedBox( - width: distanceTiny, - ), - const Icon( - Icons.expand_more_rounded, - size: iconSizeTiny, - ), - ], - ), - ), - ); - }, - ), - ], - ), - const SizedBox(height: distanceMedium), - titleBuilder(context.localization!.other), - RoundedListTiles( - items: [ - NavigationListTile( - icon: Icons.info_outline, - title: context.localization!.licenses, - onTap: () => {showLicensePage(context: context)}, - ), - Consumer( - builder: (context, currentWardService, _) { - return NavigationListTile( - icon: Icons.logout, - title: context.localization!.logout, - color: Colors.red.withOpacity(0.7), // TODO get this from theme - onTap: () { - // TODO add confirm dialog - UserSessionService().logout(); - currentWardService.clear(); - Navigator.of(context).pushReplacement( - MaterialPageRoute(builder: (_) => const LoginScreen()), - ); - }, - ); - }, - ), - ], - ), - ], + ); + }, + ), + ], + ), + ], + ), ), ); } diff --git a/packages/helpwave_localization/lib/l10n/app_de.arb b/packages/helpwave_localization/lib/l10n/app_de.arb index fc852afb..225dba5f 100644 --- a/packages/helpwave_localization/lib/l10n/app_de.arb +++ b/packages/helpwave_localization/lib/l10n/app_de.arb @@ -149,6 +149,7 @@ "loginFailed": "Login fehlgeschlagen", "selectWard": "Station auswählen", "ward": "Station", + "wards": "Stationen", "organization": "Organisation", "none": "None", "switch_": "Wechseln", @@ -175,5 +176,12 @@ "personalData": "Persönliche Daten", "passwordAndSecurity": "Passwort & Sicherheit", "other": "Weiteres", - "myTaskTemplates": "Meine Task Templates" + "myTaskTemplates": "Meine Task Templates", + "shortName": "Abbkürzung", + "longName": "Name", + "contactEmail": "Kontakt E-Mail", + "members": "Mitglieder", + "properties": "Eigenschaften", + "dangerZone": "Gefahrenzone", + "organizationDangerZoneDescription": "Vorsicht, das löschen einer Organization ist permanent und kann nicht rückgängig gemacht werden!" } diff --git a/packages/helpwave_localization/lib/l10n/app_en.arb b/packages/helpwave_localization/lib/l10n/app_en.arb index 4d370c2c..60386f6c 100644 --- a/packages/helpwave_localization/lib/l10n/app_en.arb +++ b/packages/helpwave_localization/lib/l10n/app_en.arb @@ -149,6 +149,7 @@ "loginFailed": "Login failed", "selectWard": "Select Ward", "ward": "Ward", + "wards": "Wards", "organization": "Organization", "none": "None", "switch_": "Switch", @@ -175,5 +176,12 @@ "personalData": "Personal Data", "passwordAndSecurity": "Password & Security", "other": "Other", - "myTaskTemplates": "My Task Templates" + "myTaskTemplates": "My Task Templates", + "shortName": "Short Name", + "longName": "Long Name", + "contactEmail": "Contact E-Mail", + "members": "Members", + "properties": "Properties", + "dangerZone": "Danger Zone", + "organizationDangerZoneDescription": "Deleting the organization is a permanent action and cannot be undone be careful!" } diff --git a/packages/helpwave_service/lib/src/api/offline/offline_client_store.dart b/packages/helpwave_service/lib/src/api/offline/offline_client_store.dart index 5f62cd5a..4f386fce 100644 --- a/packages/helpwave_service/lib/src/api/offline/offline_client_store.dart +++ b/packages/helpwave_service/lib/src/api/offline/offline_client_store.dart @@ -132,7 +132,7 @@ class OfflineClientStore { factory OfflineClientStore() => _instance; - final OrganizationOfflineClientStore organizationStore = OrganizationOfflineClientStore(); + final OrganizationOfflineService organizationStore = OrganizationOfflineService(); final UserOfflineService userStore = UserOfflineService(); final WardOfflineService wardStore = WardOfflineService(); diff --git a/packages/helpwave_service/lib/src/api/user/controllers/index.dart b/packages/helpwave_service/lib/src/api/user/controllers/index.dart index dcd72ff4..3a08f297 100644 --- a/packages/helpwave_service/lib/src/api/user/controllers/index.dart +++ b/packages/helpwave_service/lib/src/api/user/controllers/index.dart @@ -1 +1,2 @@ export 'user_controller.dart'; +export 'organization_controller.dart'; diff --git a/packages/helpwave_service/lib/src/api/user/controllers/organization_controller.dart b/packages/helpwave_service/lib/src/api/user/controllers/organization_controller.dart new file mode 100644 index 00000000..0c1816fa --- /dev/null +++ b/packages/helpwave_service/lib/src/api/user/controllers/organization_controller.dart @@ -0,0 +1,85 @@ +import 'package:helpwave_service/auth.dart'; +import 'package:helpwave_util/loading.dart'; +import '../../../../user.dart'; + +/// The Controller for managing a [Organization] +class OrganizationController extends LoadingChangeNotifier { + /// The current [Organization] + Organization _organization; + + /// The current [Organization] + Organization get organization => _organization; + + set organization(Organization value) { + _organization = value; + changeState(LoadingState.loaded); + } + + OrganizationController(this._organization) { + load(); + } + + /// A function to load the [Organization] + Future load() async { + loadOrganization() async { + organization = await OrganizationService().getOrganization(id: organization.id); + } + + loadHandler( + future: loadOrganization(), + ); + } + + Future update({ + String? shortName, + String? longName, + String? email, + bool? isPersonal, + String? avatarUrl, + }) async { + changeOrganization() async { + await OrganizationService().update( + id: organization.id, + longName: longName, + shortName: shortName, + isPersonal: isPersonal, + email: email, + avatarUrl: avatarUrl, + ); + organization = organization.copyWith( + longName: longName, + shortName: shortName, + isPersonal: isPersonal, + email: email, + avatarURL: avatarUrl, + ); + + bool affectsCurrentWardService = shortName != null || longName != null; + + if (affectsCurrentWardService && CurrentWardService().currentWard?.organizationId == organization.id) { + CurrentWardService().currentWard = CurrentWardService().currentWard!.copyWith( + organization: CurrentWardService().currentWard!.organization.copyWith( + shortName: shortName, + longName: longName, + ), + ); + } + } + + loadHandler( + future: changeOrganization(), + ); + } + + Future delete() async { + deleteOrganization() async { + await OrganizationService().delete(organization.id).then((_) { + // TODO do something here + }); + } + + loadHandler(future: deleteOrganization()); + } + +// TODO create method and isCreating handling +} diff --git a/packages/helpwave_service/lib/src/api/user/data_types/organization.dart b/packages/helpwave_service/lib/src/api/user/data_types/organization.dart index 9bc349a6..6ab7bb0e 100644 --- a/packages/helpwave_service/lib/src/api/user/data_types/organization.dart +++ b/packages/helpwave_service/lib/src/api/user/data_types/organization.dart @@ -8,6 +8,18 @@ class OrganizationMinimal { required this.shortName, required this.longName, }); + + OrganizationMinimal copyWith({ + String? id, + String? shortName, + String? longName, + }) { + return OrganizationMinimal( + id: id ?? this.id, + shortName: shortName ?? this.shortName, + longName: longName ?? this.longName, + ); + } } /// data class for [Organization] @@ -27,6 +39,16 @@ class Organization extends OrganizationMinimal { required this.isVerified, }); + factory Organization.empty(String? id) => Organization( + id: id ?? "", + shortName: "", + longName: "", + avatarURL: "", + email: "", + isPersonal: false, + isVerified: false, + ); + Organization copyWith({ String? id, String? shortName, @@ -47,6 +69,8 @@ class Organization extends OrganizationMinimal { ); } + String get combinedName => "$longName ($shortName)"; + @override String toString() { return "{id: $id, name: $longName, shortName: $shortName}"; diff --git a/packages/helpwave_service/lib/src/api/user/offline_clients/organization_offline_client.dart b/packages/helpwave_service/lib/src/api/user/offline_clients/organization_offline_client.dart index f5173556..6b01f74a 100644 --- a/packages/helpwave_service/lib/src/api/user/offline_clients/organization_offline_client.dart +++ b/packages/helpwave_service/lib/src/api/user/offline_clients/organization_offline_client.dart @@ -6,23 +6,23 @@ import 'package:helpwave_service/user.dart'; class OrganizationUpdate { String id; - String shortName; - String longName; - String email; - bool isPersonal; - String avatarURL; + String? shortName; + String? longName; + String? email; + bool? isPersonal; + String? avatarURL; OrganizationUpdate({ required this.id, - required this.shortName, - required this.longName, - required this.email, - required this.isPersonal, - required this.avatarURL, + this.shortName, + this.longName, + this.email, + this.isPersonal, + this.avatarURL, }); } -class OrganizationOfflineClientStore { +class OrganizationOfflineService { List organizations = []; Organization? find(String id) { @@ -179,11 +179,11 @@ class OrganizationOfflineClient extends OrganizationServiceClient { {CallOptions? options}) { final update = OrganizationUpdate( id: request.id, - shortName: request.shortName, - longName: request.longName, - email: request.contactEmail, - avatarURL: request.avatarUrl, - isPersonal: request.isPersonal, + shortName: request.hasShortName() ? request.shortName : null, + longName: request.hasLongName() ? request.longName : null, + email: request.hasContactEmail() ? request.contactEmail : null, + avatarURL: request.hasAvatarUrl() ? request.avatarUrl : null, + isPersonal: request.hasIsPersonal() ? request.isPersonal : null, ); try { diff --git a/packages/helpwave_service/lib/src/api/user/services/organization_svc.dart b/packages/helpwave_service/lib/src/api/user/services/organization_svc.dart index 39c23315..7120ae90 100644 --- a/packages/helpwave_service/lib/src/api/user/services/organization_svc.dart +++ b/packages/helpwave_service/lib/src/api/user/services/organization_svc.dart @@ -69,7 +69,8 @@ class OrganizationService { List users = response.members .map((member) => User( id: member.userId, - name: member.nickname, // TODO replace this + name: member.nickname, + // TODO replace this nickName: member.nickname, email: member.email, profileUrl: Uri.parse(member.avatarUrl), @@ -77,4 +78,38 @@ class OrganizationService { .toList(); return users; } + + Future update({ + required String id, + String? shortName, + String? longName, + String? email, + bool? isPersonal, + String? avatarUrl, + }) async { + UpdateOrganizationRequest request = UpdateOrganizationRequest( + id: id, + longName: longName, + shortName: shortName, + isPersonal: isPersonal, + contactEmail: email, + avatarUrl: avatarUrl, + ); + await organizationService.updateOrganization( + request, + options: CallOptions( + metadata: UserAPIServiceClients().getMetaData(organizationId: id), + ), + ); + } + + Future delete(String id) async { + DeleteOrganizationRequest request = DeleteOrganizationRequest(id: id); + await organizationService.deleteOrganization( + request, + options: CallOptions( + metadata: UserAPIServiceClients().getMetaData(organizationId: id), + ), + ); + } } diff --git a/packages/helpwave_service/lib/src/auth/current_ward_svc.dart b/packages/helpwave_service/lib/src/auth/current_ward_svc.dart index bca9ab3f..aa7bcb6e 100644 --- a/packages/helpwave_service/lib/src/auth/current_ward_svc.dart +++ b/packages/helpwave_service/lib/src/auth/current_ward_svc.dart @@ -1,4 +1,5 @@ import 'package:flutter/foundation.dart'; +import 'package:helpwave_service/src/api/offline/offline_client_store.dart'; import 'package:shared_preferences/shared_preferences.dart'; import 'package:helpwave_service/src/api/tasks/index.dart'; import 'package:helpwave_service/src/api/user/index.dart'; @@ -19,12 +20,22 @@ class CurrentWardInformation { String get organizationName => "${organization.longName} (${organization.shortName})"; - bool get isEmpty => wardId == "" || organizationId == ""; + bool get isEmpty => wardId == "" || organizationId == ""; bool get hasFullInformation => ward.name != "" && organization.longName != ""; CurrentWardInformation(this.ward, this.organization); + CurrentWardInformation copyWith({ + WardMinimal? ward, + OrganizationMinimal? organization, + }) { + return CurrentWardInformation( + ward ?? this.ward, + organization ?? this.organization, + ); + } + @override String toString() { return "CurrentWardInformation: {ward: ${ward.toString()}, organization: ${organization.toString()}"; @@ -73,7 +84,18 @@ class _CurrentWardPreferences { /// /// Changes the [CurrentWardInformation] globally class CurrentWardService extends Listenable { - bool devMode = false; // TODO remove + bool _devMode = false; // TODO remove + + bool get devMode => _devMode; + + set devMode(bool value) { + _devMode = value; + if (UserAPIServiceClients().offlineMode) { + Ward firstWard = OfflineClientStore().wardStore.wards[0]; + Organization firstOrganization = OfflineClientStore().organizationStore.organizations[0]; + currentWard = CurrentWardInformation(firstWard, firstOrganization); + } + } /// A storage for the current ward final _CurrentWardPreferences _preferences = _CurrentWardPreferences(); @@ -91,9 +113,7 @@ class CurrentWardService extends Listenable { final List _listeners = []; CurrentWardService._initialize() { - if (!devMode) { - load(); - } + load(); } static final CurrentWardService _currentWardService = CurrentWardService._initialize(); @@ -109,10 +129,10 @@ class CurrentWardService extends Listenable { } } _currentWard = currentWard; - if(!isLoaded){ + if (!isLoaded) { fetch(); } - if(kDebugMode){ + if (kDebugMode) { print(currentWard); } notifyListeners(); @@ -122,13 +142,16 @@ class CurrentWardService extends Listenable { /// Load the preferences with the [_CurrentWardPreferences] Future load() async { - // everything is done in the setter - currentWard = devMode ? null : await _preferences.getInformation(); + await _preferences.getInformation().then((value) { + if (!devMode) { + currentWard = value; + } + }); } /// Fetch [Ward] and [Organization] from backend Future fetch() async { - if(!isInitialized){ + if (!isInitialized) { return; } Organization organization = await OrganizationService().getOrganization(id: currentWard!.organizationId); diff --git a/packages/helpwave_service/lib/src/auth/user_session_service.dart b/packages/helpwave_service/lib/src/auth/user_session_service.dart index 1e4a40a7..1fa59daf 100644 --- a/packages/helpwave_service/lib/src/auth/user_session_service.dart +++ b/packages/helpwave_service/lib/src/auth/user_session_service.dart @@ -28,7 +28,10 @@ class UserSessionService { bool get devMode => _devMode; /// **Only use this** once before using the service - changeMode(bool isDevMode) => _devMode = isDevMode; + changeMode(bool isDevMode) { + _devMode = isDevMode; + CurrentWardService().devMode = isDevMode; + } /// Logs a User in by using the stored tokens /// diff --git a/packages/helpwave_widget/lib/src/bottom_sheets/bottom_sheet_base.dart b/packages/helpwave_widget/lib/src/bottom_sheets/bottom_sheet_base.dart index 0e578d9a..6894b5aa 100644 --- a/packages/helpwave_widget/lib/src/bottom_sheets/bottom_sheet_base.dart +++ b/packages/helpwave_widget/lib/src/bottom_sheets/bottom_sheet_base.dart @@ -1,6 +1,7 @@ import 'package:flutter/material.dart'; import 'package:helpwave_theme/constants.dart'; import 'package:helpwave_theme/util.dart'; +import 'package:helpwave_widget/navigation.dart'; extension PushModalContextExtension on BuildContext { Future pushModal({ @@ -135,6 +136,21 @@ class BottomSheetAction { final Function() onPressed; BottomSheetAction({required this.icon, required this.onPressed}); + + static BottomSheetAction navigationForBottomSheetPage(BuildContext context) { + StackController controller = NavigationStackController.of(context); + + return BottomSheetAction( + icon: controller.isAtNavigationStart ? Icons.close : Icons.chevron_left_rounded, + onPressed: () { + if (controller.canPop) { + controller.pop(); + } else { + Navigator.pop(context); + } + }, + ); + } } class BottomSheetHeader extends StatelessWidget { @@ -177,6 +193,41 @@ class BottomSheetHeader extends StatelessWidget { this.padding = const EdgeInsets.only(bottom: paddingSmall), }); + factory BottomSheetHeader.navigation(BuildContext context, { + BottomSheetAction? trailing, + Widget? title, + String? titleText, + bool isShowingDragHandler = false, + EdgeInsets padding = const EdgeInsets.only(bottom: paddingSmall), + }) { + return BottomSheetHeader( + leading: BottomSheetAction.navigationForBottomSheetPage(context), + trailing: trailing, + title: title, + titleText: titleText, + isShowingDragHandler: isShowingDragHandler, + padding: padding, + ); + } + + BottomSheetHeader copyWith({ + BottomSheetAction? leading, + BottomSheetAction? trailing, + Widget? title, + String? titleText, + bool? isShowingDragHandler, + EdgeInsets? padding, + }) { + return BottomSheetHeader( + leading: leading ?? this.leading, + trailing: trailing ?? this.trailing, + title: title ?? this.title, + titleText: titleText ?? this.titleText, + isShowingDragHandler: isShowingDragHandler ?? this.isShowingDragHandler, + padding: padding ?? this.padding, + ); + } + @override Widget build(BuildContext context) { BottomSheetAction usedLeading = @@ -259,13 +310,13 @@ class BottomSheetBase extends StatefulWidget { /// The function to call when closing the [BottomSheetBase] final void Function() onClosing; - /// The builder function call to build the content of the [BottomSheetBase] - final Widget Function(BuildContext context) builder; + /// The main [Widget] of the [BottomSheetBase] + final Widget child; /// A header [Widget] above the builder content /// /// Defaults to the [BottomSheetHeader] - final Widget header; + final Widget? header; /// The bottom [Widget] below the [builder] final Widget? bottomWidget; @@ -278,10 +329,10 @@ class BottomSheetBase extends StatefulWidget { const BottomSheetBase({ super.key, required this.onClosing, - required this.builder, + required this.child, this.padding = const EdgeInsets.all(paddingMedium), this.bottomWidget, - this.header = const BottomSheetHeader(), + this.header, this.mainAxisSize = MainAxisSize.min, }); @@ -304,8 +355,8 @@ class _BottomSheetBase extends State with TickerProviderStateMi child: Column( mainAxisSize: widget.mainAxisSize, children: [ - widget.header, - widget.builder(context), + widget.header ?? const SizedBox(), + widget.child, widget.bottomWidget ?? const SizedBox(), ], ), diff --git a/packages/helpwave_widget/lib/src/bottom_sheets/bottom_sheet_navigator.dart b/packages/helpwave_widget/lib/src/bottom_sheets/bottom_sheet_navigator.dart deleted file mode 100644 index 50544009..00000000 --- a/packages/helpwave_widget/lib/src/bottom_sheets/bottom_sheet_navigator.dart +++ /dev/null @@ -1,46 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:helpwave_widget/navigation.dart'; -import '../../bottom_sheets.dart'; - -class BottomSheetNavigator extends StatelessWidget { - final BottomSheetPageBuilder initialPageBuilder; - - const BottomSheetNavigator({super.key, required this.initialPageBuilder}); - - @override - Widget build(BuildContext context) { - return SimpleNavigator( - initialPage: initialPageBuilder, - builder: (context, navigationController) { - BottomSheetPageBuilder page = navigationController.currentPage; - - BottomSheetHeader? computedHeader = page.headerBuilder(context, navigationController); - - BottomSheetHeader header = BottomSheetHeader( - title: computedHeader?.title, - titleText: computedHeader?.titleText, - isShowingDragHandler: computedHeader?.isShowingDragHandler ?? false, - trailing: computedHeader?.trailing, - leading: BottomSheetAction( - icon: navigationController.isInitialPage ? Icons.close : Icons.chevron_left_rounded, - onPressed: () { - if (navigationController.canPop) { - navigationController.pop(); - } else { - Navigator.pop(context); - } - }, - ), - ); - - return BottomSheetBase( - onClosing: () {}, - builder: (context) => page.build(context, navigationController), - header: header, - bottomWidget: page.bottomWidgetBuilder(context, navigationController), - mainAxisSize: MainAxisSize.max, - ); - }, - ); - } -} diff --git a/packages/helpwave_widget/lib/src/bottom_sheets/bottom_sheet_page.dart b/packages/helpwave_widget/lib/src/bottom_sheets/bottom_sheet_page.dart new file mode 100644 index 00000000..d9763c50 --- /dev/null +++ b/packages/helpwave_widget/lib/src/bottom_sheets/bottom_sheet_page.dart @@ -0,0 +1,36 @@ +import 'package:flutter/material.dart'; +import 'package:helpwave_widget/navigation.dart'; +import '../../bottom_sheets.dart'; + +/// A wrapper for a [BottomSheetBase] to ensure uniform styling among these +/// +/// These pages rely on a [NavigationStackController] to be navigated to and **can only be used** when a +/// **[NavigationStackController] is provided** or the header is overwritten +class BottomSheetPage extends StatelessWidget { + /// The header of the [BottomSheetBase], + /// + /// Defaults to [BottomSheetHeader.navigation] + final Widget? header; + + final Widget child; + + final Widget? bottom; + + const BottomSheetPage({ + super.key, + this.header, + required this.child, + this.bottom, + }); + + @override + Widget build(BuildContext context) { + return BottomSheetBase( + onClosing: () {}, + header: header ?? BottomSheetHeader.navigation(context), + bottomWidget: bottom, + mainAxisSize: MainAxisSize.max, + child: child, + ); + } +} diff --git a/packages/helpwave_widget/lib/src/bottom_sheets/bottom_sheet_page_builder.dart b/packages/helpwave_widget/lib/src/bottom_sheets/bottom_sheet_page_builder.dart deleted file mode 100644 index 0af34f0c..00000000 --- a/packages/helpwave_widget/lib/src/bottom_sheets/bottom_sheet_page_builder.dart +++ /dev/null @@ -1,20 +0,0 @@ -import 'package:flutter/cupertino.dart'; -import 'package:helpwave_widget/navigation.dart'; -import '../../bottom_sheets.dart'; - -mixin BottomSheetPageBuilder { - /// The [BottomSheetHeader] of the [BottomSheetPageBuilder] - /// - /// The leading icon will always be ignored to - BottomSheetHeader? headerBuilder(BuildContext context, NavigationController controller) { - return null; - } - - /// The [BottomSheetBase] bottom widget to display for the [BottomSheetPageBuilder] - Widget? bottomWidgetBuilder(BuildContext context, NavigationController controller) { - return null; - } - - /// The builder function call to build the content of the [BottomSheetBase] - Widget build(BuildContext context, NavigationController controller); -} diff --git a/packages/helpwave_widget/lib/src/bottom_sheets/index.dart b/packages/helpwave_widget/lib/src/bottom_sheets/index.dart index 5277adaf..ed31d71a 100644 --- a/packages/helpwave_widget/lib/src/bottom_sheets/index.dart +++ b/packages/helpwave_widget/lib/src/bottom_sheets/index.dart @@ -1,3 +1,2 @@ export 'bottom_sheet_base.dart'; -export 'bottom_sheet_navigator.dart'; -export 'bottom_sheet_page_builder.dart'; +export 'bottom_sheet_page.dart'; diff --git a/packages/helpwave_widget/lib/src/navigation/index.dart b/packages/helpwave_widget/lib/src/navigation/index.dart index 42ae72d6..92b58a95 100644 --- a/packages/helpwave_widget/lib/src/navigation/index.dart +++ b/packages/helpwave_widget/lib/src/navigation/index.dart @@ -1,2 +1,2 @@ -export 'navigation_controller.dart'; -export 'simple_navigator.dart'; +export 'navigation_stack_controller.dart'; +export 'navigation_outlet.dart'; diff --git a/packages/helpwave_widget/lib/src/navigation/navigation_controller.dart b/packages/helpwave_widget/lib/src/navigation/navigation_controller.dart deleted file mode 100644 index 8d540f8f..00000000 --- a/packages/helpwave_widget/lib/src/navigation/navigation_controller.dart +++ /dev/null @@ -1,27 +0,0 @@ -import 'package:flutter/cupertino.dart'; - -class NavigationController extends ChangeNotifier { - List stack = []; - - T get currentPage => stack.last; - - bool get canPop => stack.length > 1; - - bool get isInitialPage => stack.length == 1; - - NavigationController({ - required T initialPage, - }) { - stack.add(initialPage); - } - - void push(T page) { - stack.add(page); - notifyListeners(); - } - - void pop() { - stack.removeLast(); - notifyListeners(); - } -} diff --git a/packages/helpwave_widget/lib/src/navigation/navigation_outlet.dart b/packages/helpwave_widget/lib/src/navigation/navigation_outlet.dart new file mode 100644 index 00000000..0bd53d6f --- /dev/null +++ b/packages/helpwave_widget/lib/src/navigation/navigation_outlet.dart @@ -0,0 +1,57 @@ +import 'package:flutter/material.dart'; +import 'package:helpwave_widget/src/navigation/index.dart'; +import 'package:provider/provider.dart'; + +/// A navigation [Widget] which can be used withing the flutter [Navigator] +/// +/// Provided with an [initialValue] +class NavigationStack extends StatelessWidget { + final T initialValue; + + /// Whether the flutter default [Navigator] should be disabled. + /// + /// True: All pop events are intercepted and only if the last page is shown will the pop happen + /// + /// False: The pop events are propagated to the flutter [Navigator] and the page is popped + final bool disableNavigator; + + /// A custom handler that specifies how the generic values of the [NavigationStackState] are mapped + /// to a [Widget] + final Widget Function(BuildContext context, StackController navigationController) builder; + + const NavigationStack({ + super.key, + required this.initialValue, + required this.builder, + this.disableNavigator = true, + }); + + @override + Widget build(BuildContext context) { + return ChangeNotifierProvider( + create: (context) => StackController( + initialValue: initialValue, + ), + builder: (context, child) => Consumer>( + builder: (context, navigationController, _) { + return PopScope( + // If the navigation controller cannot pop to another bottom sheet, a normal pop is correct + canPop: !navigationController.canPop, + onPopInvoked: (didPop) { + if (navigationController.canPop) { + navigationController.pop(); + } + }, + child: builder(context, navigationController), + ); + }, + ), + ); + } +} + +/// A wrapper for the [NavigationStack] that allows the stacking of arbitrary many [Widget]s +class NavigationOutlet extends NavigationStack { + NavigationOutlet({super.key, required super.initialValue, super.disableNavigator}) + : super(builder: (context, navigationController) => navigationController.currentItem); +} diff --git a/packages/helpwave_widget/lib/src/navigation/navigation_stack_controller.dart b/packages/helpwave_widget/lib/src/navigation/navigation_stack_controller.dart new file mode 100644 index 00000000..a48fc14a --- /dev/null +++ b/packages/helpwave_widget/lib/src/navigation/navigation_stack_controller.dart @@ -0,0 +1,39 @@ +import 'package:flutter/cupertino.dart'; +import 'package:provider/provider.dart'; + +class StackController extends ChangeNotifier { + List stack = []; + + T get currentItem => stack.last; + + bool get canPop => stack.length > 1; + + bool get isAtNavigationStart => stack.length == 1; + + StackController({ + required T initialValue, + }) { + stack.add(initialValue); + } + + void push(T page) { + stack.add(page); + notifyListeners(); + } + + void pop() { + stack.removeLast(); + notifyListeners(); + } + + static StackController of(BuildContext context) => Provider.of>(context, listen: false); +} + +class NavigationStackController extends StackController { + NavigationStackController({required super.initialValue}); + + static StackController of(BuildContext context) => Provider.of>( + context, + listen: false, + ); +} diff --git a/packages/helpwave_widget/lib/src/navigation/simple_navigator.dart b/packages/helpwave_widget/lib/src/navigation/simple_navigator.dart deleted file mode 100644 index de114744..00000000 --- a/packages/helpwave_widget/lib/src/navigation/simple_navigator.dart +++ /dev/null @@ -1,35 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:helpwave_widget/src/navigation/index.dart'; -import 'package:provider/provider.dart'; - -/// A navigation widget which can be used withing the flutter [Navigator] -class SimpleNavigator extends StatelessWidget { - final T initialPage; - - final Widget Function(BuildContext context, NavigationController navigationController) builder; - - const SimpleNavigator({super.key, required this.initialPage, required this.builder}); - - @override - Widget build(BuildContext context) { - return ChangeNotifierProvider( - create: (context) => NavigationController( - initialPage: initialPage, - ), - child: Consumer>( - builder: (context, navigationController, _) { - return PopScope( - // If the navigation controller cannot pop to another bottom sheet, a normal pop is correct - canPop: !navigationController.canPop, - onPopInvoked: (didPop) { - if (navigationController.canPop) { - navigationController.pop(); - } - }, - child: builder(context, navigationController), - ); - }, - ), - ); - } -} diff --git a/packages/helpwave_widget/lib/src/text_input/index.dart b/packages/helpwave_widget/lib/src/text_input/index.dart new file mode 100644 index 00000000..28c8394e --- /dev/null +++ b/packages/helpwave_widget/lib/src/text_input/index.dart @@ -0,0 +1,3 @@ +export 'clickable_text_edit.dart'; +export 'password_text_editing_field.dart'; +export 'text_form_field_with_timer.dart'; diff --git a/packages/helpwave_widget/lib/text_input.dart b/packages/helpwave_widget/lib/text_input.dart index 34bd8846..0114df77 100644 --- a/packages/helpwave_widget/lib/text_input.dart +++ b/packages/helpwave_widget/lib/text_input.dart @@ -1,3 +1 @@ -export 'package:helpwave_widget/src/text_input/password_text_editing_field.dart'; -export 'package:helpwave_widget/src/text_input/clickable_text_edit.dart'; -export 'package:helpwave_widget/src/text_input/text_form_field_with_timer.dart'; +export 'package:helpwave_widget/src/text_input/index.dart'; From 930200fe5195bd1014142d5145fa5f00c70be7b7 Mon Sep 17 00:00:00 2001 From: Felix Thape Date: Fri, 20 Sep 2024 00:53:03 +0200 Subject: [PATCH 11/36] feat: add wards bottom sheet --- .../organization_bottom_sheet.dart | 32 +++-- .../patient_bottom_sheet.dart | 6 +- .../bottom_sheet_pages/task_bottom_sheet.dart | 17 +-- .../bottom_sheet_pages/user_bottom_sheet.dart | 2 +- .../wards_bottom_sheet_page.dart | 70 +++++++++++ apps/tasks/lib/components/subtask_list.dart | 8 +- .../helpwave_theme/lib/src/theme/theme.dart | 116 +++++++++--------- .../lib/src/widgets/index.dart | 1 + .../lib/src/widgets/pressable_text.dart | 35 ++++++ 9 files changed, 189 insertions(+), 98 deletions(-) create mode 100644 apps/tasks/lib/components/bottom_sheet_pages/wards_bottom_sheet_page.dart create mode 100644 packages/helpwave_widget/lib/src/widgets/pressable_text.dart diff --git a/apps/tasks/lib/components/bottom_sheet_pages/organization_bottom_sheet.dart b/apps/tasks/lib/components/bottom_sheet_pages/organization_bottom_sheet.dart index 24a296d9..6ab3b5ad 100644 --- a/apps/tasks/lib/components/bottom_sheet_pages/organization_bottom_sheet.dart +++ b/apps/tasks/lib/components/bottom_sheet_pages/organization_bottom_sheet.dart @@ -6,8 +6,11 @@ import 'package:helpwave_theme/util.dart'; import 'package:helpwave_widget/bottom_sheets.dart'; import 'package:helpwave_widget/lists.dart'; import 'package:helpwave_widget/loading.dart'; +import 'package:helpwave_widget/navigation.dart'; import 'package:helpwave_widget/text_input.dart'; +import 'package:helpwave_widget/widgets.dart'; import 'package:provider/provider.dart'; +import 'package:tasks/components/bottom_sheet_pages/wards_bottom_sheet_page.dart'; import 'package:tasks/screens/settings_screen.dart'; class OrganizationBottomSheetPage extends StatelessWidget { @@ -25,7 +28,10 @@ class OrganizationBottomSheetPage extends StatelessWidget { context, title: LoadingAndErrorWidget.pulsing( state: controller.state, - child: Text(controller.organization.combinedName), + child: Text( + controller.organization.combinedName, + style: const TextStyle(fontWeight: FontWeight.w800, fontSize: 16), + ), ), ), child: Flexible( @@ -38,33 +44,37 @@ class OrganizationBottomSheetPage extends StatelessWidget { shrinkWrap: true, children: [ const SizedBox(height: distanceMedium), - Text(context.localization!.shortName), + Text(context.localization!.shortName, style: context.theme.textTheme.titleMedium), + const SizedBox(height: distanceTiny), TextFormFieldWithTimer( initialValue: controller.organization.shortName, onUpdate: (value) => controller.update(shortName: value), ), const SizedBox(height: distanceMedium), - Text(context.localization!.longName), + Text(context.localization!.longName, style: context.theme.textTheme.titleMedium), + const SizedBox(height: distanceTiny), TextFormFieldWithTimer( initialValue: controller.organization.longName, onUpdate: (value) => controller.update(longName: value), ), const SizedBox(height: distanceMedium), - Text(context.localization!.contactEmail), + Text(context.localization!.contactEmail, style: context.theme.textTheme.titleMedium), + const SizedBox(height: distanceTiny), TextFormFieldWithTimer( initialValue: controller.organization.email, // TODO validation onUpdate: (value) => controller.update(email: value), ), const SizedBox(height: distanceMedium), - Text(context.localization!.settings), + Text(context.localization!.settings, style: context.theme.textTheme.titleMedium), + const SizedBox(height: distanceTiny), RoundedListTiles( items: [ NavigationListTile( icon: Icons.house_rounded, title: context.localization!.wards, onTap: () { - // TODO navigate to ward page + NavigationStackController.of(context).push(WardsBottomSheetPage(organizationId: organizationId)); }, ), NavigationListTile( @@ -84,16 +94,14 @@ class OrganizationBottomSheetPage extends StatelessWidget { ], ), const SizedBox(height: distanceMedium), - Text(context.localization!.dangerZone), + Text(context.localization!.dangerZone, style: context.theme.textTheme.titleMedium), Text( context.localization!.organizationDangerZoneDescription, style: TextStyle(color: context.theme.hintColor), ), - TextButton( - child: Text( - "${context.localization!.delete} ${context.localization!.organization}", - style: const TextStyle(color: Colors.red), // TODO get from theme - ), + PressableText( + text: "${context.localization!.delete} ${context.localization!.organization}", + style: const TextStyle(color: Colors.red, fontWeight: FontWeight.w700), // TODO get from theme onPressed: () { // TODO show modal and delete organization }, diff --git a/apps/tasks/lib/components/bottom_sheet_pages/patient_bottom_sheet.dart b/apps/tasks/lib/components/bottom_sheet_pages/patient_bottom_sheet.dart index 68d7c21e..c04f5a35 100644 --- a/apps/tasks/lib/components/bottom_sheet_pages/patient_bottom_sheet.dart +++ b/apps/tasks/lib/components/bottom_sheet_pages/patient_bottom_sheet.dart @@ -85,7 +85,7 @@ class _PatientBottomSheetState extends State { patientController.isCreating ? MainAxisAlignment.end : MainAxisAlignment.spaceBetween, children: patientController.isCreating ? [ - TextButton( + FilledButton( style: buttonStyleBig, onPressed: patientController.create, child: Text(context.localization!.create), @@ -95,7 +95,7 @@ class _PatientBottomSheetState extends State { SizedBox( width: width * 0.4, // TODO make this state checking easier and more readable - child: TextButton( + child: FilledButton( onPressed: patientController.patient.isNotAssignedToBed ? null : () { @@ -115,7 +115,7 @@ class _PatientBottomSheetState extends State { ), SizedBox( width: width * 0.4, - child: TextButton( + child: FilledButton( // TODO check whether the patient is active onPressed: patientController.patient.isDischarged ? null diff --git a/apps/tasks/lib/components/bottom_sheet_pages/task_bottom_sheet.dart b/apps/tasks/lib/components/bottom_sheet_pages/task_bottom_sheet.dart index c3f54fb2..4d8989ed 100644 --- a/apps/tasks/lib/components/bottom_sheet_pages/task_bottom_sheet.dart +++ b/apps/tasks/lib/components/bottom_sheet_pages/task_bottom_sheet.dart @@ -143,7 +143,7 @@ class _TaskBottomSheetState extends State { padding: const EdgeInsets.only(top: paddingSmall), child: Align( alignment: Alignment.topRight, - child: TextButton( + child: FilledButton( style: buttonStyleBig, onPressed: taskController.isReadyForCreate ? () { @@ -262,25 +262,10 @@ class _TaskBottomSheetState extends State { initialDate: taskController.task.dueDate ?? DateTime.now(), firstDate: DateTime(1960), lastDate: DateTime.now().add(const Duration(days: 365 * 5)), - builder: (context, child) { - // Overwrite the Theme - ThemeData pickerTheme = - context.theme.copyWith(textButtonTheme: const TextButtonThemeData()); - return Theme(data: pickerTheme, child: child ?? const SizedBox()); - }, ).then((date) async { await showTimePicker( context: context, initialTime: TimeOfDay.fromDateTime(taskController.task.dueDate ?? DateTime.now()), - builder: (context, child) { - ThemeData originalTheme = context.theme; - - // Temporarily set a default theme for the picker - ThemeData pickerTheme = ThemeData.fallback().copyWith( - colorScheme: originalTheme.colorScheme, - ); - return Theme(data: pickerTheme, child: child ?? const SizedBox()); - }, ).then((time) { if (date == null && time == null) { return; diff --git a/apps/tasks/lib/components/bottom_sheet_pages/user_bottom_sheet.dart b/apps/tasks/lib/components/bottom_sheet_pages/user_bottom_sheet.dart index b6c482fe..2effb3b0 100644 --- a/apps/tasks/lib/components/bottom_sheet_pages/user_bottom_sheet.dart +++ b/apps/tasks/lib/components/bottom_sheet_pages/user_bottom_sheet.dart @@ -104,7 +104,7 @@ class UserBottomSheetPage extends StatelessWidget { Padding( padding: const EdgeInsets.only(bottom: distanceMedium), child: Consumer(builder: (context, currentWardService, _) { - return TextButton( + return FilledButton( style: buttonStyleMedium, onPressed: () { Navigator.of(context).pushReplacement( diff --git a/apps/tasks/lib/components/bottom_sheet_pages/wards_bottom_sheet_page.dart b/apps/tasks/lib/components/bottom_sheet_pages/wards_bottom_sheet_page.dart new file mode 100644 index 00000000..5d974fad --- /dev/null +++ b/apps/tasks/lib/components/bottom_sheet_pages/wards_bottom_sheet_page.dart @@ -0,0 +1,70 @@ +import 'package:flutter/material.dart'; +import 'package:helpwave_localization/localization.dart'; +import 'package:helpwave_service/tasks.dart'; +import 'package:helpwave_service/user.dart'; +import 'package:helpwave_theme/constants.dart'; +import 'package:helpwave_theme/util.dart'; +import 'package:helpwave_widget/bottom_sheets.dart'; +import 'package:helpwave_widget/lists.dart'; +import 'package:helpwave_widget/loading.dart'; +import 'package:tasks/screens/settings_screen.dart'; + +class WardsBottomSheetPage extends StatelessWidget { + final String organizationId; + + const WardsBottomSheetPage({super.key, required this.organizationId}); + + @override + Widget build(BuildContext context) { + return BottomSheetPage( + header: BottomSheetHeader.navigation( + context, + title: LoadingFutureBuilder( + data: OrganizationService().getOrganization(id: organizationId), + thenWidgetBuilder: (context, data) => Column( + mainAxisSize: MainAxisSize.min, + children: [ + Text( + context.localization!.wards, + style: const TextStyle(fontWeight: FontWeight.w800, fontSize: 16), + ), + Text( + data.combinedName, + style: TextStyle(color: context.theme.hintColor), + ), + ], + ), + ), + ), + child: Flexible( + child: LoadingFutureBuilder( + data: WardService().getWards(organizationId: organizationId), + thenWidgetBuilder: (context, data) => ListView( + children: [ + Flexible( + child: ListView( + shrinkWrap: true, + children: [ + const SizedBox(height: distanceMedium), + RoundedListTiles( + items: data + .map( + (ward) => NavigationListTile( + icon: Icons.house_rounded, + title: ward.name, + onTap: () { + // TODO navigate to ward view + }, + ), + ) + .toList()), + ], + ), + ), + ], + ), + ), + ), + ); + } +} diff --git a/apps/tasks/lib/components/subtask_list.dart b/apps/tasks/lib/components/subtask_list.dart index f1db6772..77037ca6 100644 --- a/apps/tasks/lib/components/subtask_list.dart +++ b/apps/tasks/lib/components/subtask_list.dart @@ -135,13 +135,7 @@ class _SubTaskChangeDialogState extends State { children: [ TextButton( onPressed: () => Navigator.pop(context, null), - style: ButtonStyle( - backgroundColor: resolveByStatesAndContextBackground( - context: context, - defaultValue: negativeColor, - ), - ), - child: Text(context.localization!.cancel), + child: Text(context.localization!.cancel, style: TextStyle(color: context.theme.hintColor),), ), TextButton( onPressed: () => Navigator.pop(context, updatedName), diff --git a/packages/helpwave_theme/lib/src/theme/theme.dart b/packages/helpwave_theme/lib/src/theme/theme.dart index 3454388d..bd38d4e0 100644 --- a/packages/helpwave_theme/lib/src/theme/theme.dart +++ b/packages/helpwave_theme/lib/src/theme/theme.dart @@ -3,57 +3,57 @@ import 'package:helpwave_util/material_state.dart'; import '../../constants.dart'; // A function to map incoming colors to a theme -ThemeData makeTheme({ - // main colors - required Color primaryColor, - required Color onPrimaryColor, - required Color inversePrimaryColor, - required Color secondaryColor, - required Color onSecondaryColor, - required Color tertiary, - required Color onTertiary, - required Color errorColor, - required Color onErrorColor, +ThemeData makeTheme( + { + // main colors + required Color primaryColor, + required Color onPrimaryColor, + required Color inversePrimaryColor, + required Color secondaryColor, + required Color onSecondaryColor, + required Color tertiary, + required Color onTertiary, + required Color errorColor, + required Color onErrorColor, - // background - required Color backgroundColor, - required Color onBackgroundColor, + // background + required Color backgroundColor, + required Color onBackgroundColor, - // surfaces - required Color surface, - required Color onSurface, - required Color surfaceVariant, - required Color onSurfaceVariant, - required Color inverseSurface, - required Color onInverseSurface, + // surfaces + required Color surface, + required Color onSurface, + required Color surfaceVariant, + required Color onSurfaceVariant, + required Color inverseSurface, + required Color onInverseSurface, - // container - required Color primaryContainer, - required Color onPrimaryContainer, - required Color secondaryContainer, - required Color onSecondaryContainer, - required Color tertiaryContainer, - required Color onTertiaryContainer, - required Color errorContainer, - required Color onErrorContainer, + // container + required Color primaryContainer, + required Color onPrimaryContainer, + required Color secondaryContainer, + required Color onSecondaryContainer, + required Color tertiaryContainer, + required Color onTertiaryContainer, + required Color errorContainer, + required Color onErrorContainer, - // other - required Color shadow, - required Color outline, - required Color disabledColor, - required Color onDisabledColor, - required Color focusedColor, - required Color defaultColor, + // other + required Color shadow, + required Color outline, + required Color disabledColor, + required Color onDisabledColor, + required Color focusedColor, + required Color defaultColor, - // additional parameters - required Brightness brightness, + // additional parameters + required Brightness brightness, - // flutter themes - AppBarTheme appBarTheme = sharedAppBarTheme, + // flutter themes + AppBarTheme appBarTheme = sharedAppBarTheme, - // text - required Color primaryTextColor -}) { + // text + required Color primaryTextColor}) { return ThemeData( useMaterial3: true, disabledColor: disabledColor, @@ -68,12 +68,14 @@ ThemeData makeTheme({ constraints: null, ), inputDecorationTheme: InputDecorationTheme( - hintStyle: const TextStyle(color: Color.fromARGB(255, 100, 100, 100)), + contentPadding: const EdgeInsets.all(16), + fillColor: surface, + filled: true, focusColor: focusedColor, - focusedBorder: defaultOutlineInputBorder.copyWith(borderSide: BorderSide(color: focusedColor)), - enabledBorder: defaultOutlineInputBorder.copyWith(borderSide: BorderSide(color: defaultColor)), - errorBorder: defaultOutlineInputBorder.copyWith(borderSide: BorderSide(color: errorColor)), - focusedErrorBorder: defaultOutlineInputBorder.copyWith(borderSide: BorderSide(color: errorColor)), + focusedBorder: defaultOutlineInputBorder.copyWith(borderSide: BorderSide.none), + enabledBorder: defaultOutlineInputBorder.copyWith(borderSide: BorderSide.none), + errorBorder: defaultOutlineInputBorder.copyWith(borderSide: BorderSide.none), + focusedErrorBorder: defaultOutlineInputBorder.copyWith(borderSide: BorderSide.none), iconColor: MaterialStateColor.resolveWith((states) { if (states.contains(MaterialState.focused)) { return focusedColor; @@ -139,16 +141,7 @@ ThemeData makeTheme({ ), ), textButtonTheme: TextButtonThemeData( - style: buttonStyleSmall.copyWith( - backgroundColor: resolveByStates( - defaultValue: primaryColor, - disabled: disabledColor, - ), - foregroundColor: resolveByStates( - defaultValue: onPrimaryColor, - disabled: onDisabledColor, - ), - ), + style: buttonStyleSmall, ), iconTheme: IconThemeData( size: iconSizeSmall, @@ -180,6 +173,11 @@ ThemeData makeTheme({ ), dividerTheme: DividerThemeData(color: primaryTextColor.withOpacity(0.4), space: 1, thickness: 1), hintColor: primaryTextColor.withOpacity(0.7), + textTheme: const TextTheme( + titleLarge: TextStyle(fontWeight: FontWeight.bold, fontFamily: "SpaceGrotesk", fontSize: 22), + titleMedium: TextStyle(fontWeight: FontWeight.w700, fontFamily: "SpaceGrotesk", fontSize: 18), + titleSmall: TextStyle(fontWeight: FontWeight.w700, fontFamily: "SpaceGrotesk", fontSize: 16), + ), colorScheme: ColorScheme( // General brightness: brightness, diff --git a/packages/helpwave_widget/lib/src/widgets/index.dart b/packages/helpwave_widget/lib/src/widgets/index.dart index 5cbf43ab..8e0ad7fc 100644 --- a/packages/helpwave_widget/lib/src/widgets/index.dart +++ b/packages/helpwave_widget/lib/src/widgets/index.dart @@ -1,2 +1,3 @@ export 'fallback_avatar.dart'; export 'list_tile_card.dart'; +export 'pressable_text.dart'; diff --git a/packages/helpwave_widget/lib/src/widgets/pressable_text.dart b/packages/helpwave_widget/lib/src/widgets/pressable_text.dart new file mode 100644 index 00000000..2e178ebd --- /dev/null +++ b/packages/helpwave_widget/lib/src/widgets/pressable_text.dart @@ -0,0 +1,35 @@ +import 'package:flutter/cupertino.dart'; + +class PressableText extends StatelessWidget { + final String text; + final TextStyle? style; + final void Function() onPressed; + final double? width; + final double? height; + + const PressableText({ + super.key, + required this.text, + this.style, + required this.onPressed, + this.width, + this.height, + }); + + @override + Widget build(BuildContext context) { + return GestureDetector( + onTap: onPressed, + child: ConstrainedBox( + constraints: BoxConstraints.tightForFinite( + width: width ?? double.infinity, + height: height ?? double.infinity, + ), + child: Text( + text, + style: style, + ), + ), + ); + } +} From 820b26c9e504395a760d8fe2991a642ffcced289 Mon Sep 17 00:00:00 2001 From: Felix Thape Date: Wed, 25 Sep 2024 00:51:41 +0200 Subject: [PATCH 12/36] feat: add organization_members_bottom_sheet --- .../organization_bottom_sheet.dart | 5 +- .../organization_members_bottom_sheet.dart | 110 ++++++++++++ .../bottom_sheet_pages/user_bottom_sheet.dart | 2 +- .../wards_bottom_sheet_page.dart | 2 +- apps/tasks/lib/screens/settings_screen.dart | 8 +- .../helpwave_localization/lib/l10n/app_de.arb | 6 +- .../helpwave_localization/lib/l10n/app_en.arb | 6 +- .../src/api/offline/offline_client_store.dart | 2 + .../patient_offline_client.dart | 4 +- .../offline_clients/task_offline_client.dart | 10 +- .../src/api/tasks/services/patient_svc.dart | 4 +- .../lib/src/api/tasks/services/task_svc.dart | 10 +- .../api/tasks/util/task_status_mapping.dart | 2 +- .../lib/src/api/user/controllers/index.dart | 1 + .../organization_member_controller.dart | 84 ++++++++++ .../src/api/user/data_types/invitation.dart | 48 ++++++ .../invitation_offline_service.dart | 40 +++++ .../organization_offline_client.dart | 114 +++++++++++-- .../api/user/services/organization_svc.dart | 156 +++++++++++++++--- .../lib/src/api/user/util/type_converter.dart | 34 ++++ .../src/bottom_sheets/bottom_sheet_page.dart | 2 + .../lib/src/lists/rounded_list_tiles.dart | 26 ++- 22 files changed, 611 insertions(+), 65 deletions(-) create mode 100644 apps/tasks/lib/components/bottom_sheet_pages/organization_members_bottom_sheet.dart create mode 100644 packages/helpwave_service/lib/src/api/user/controllers/organization_member_controller.dart create mode 100644 packages/helpwave_service/lib/src/api/user/data_types/invitation.dart create mode 100644 packages/helpwave_service/lib/src/api/user/offline_clients/invitation_offline_service.dart create mode 100644 packages/helpwave_service/lib/src/api/user/util/type_converter.dart diff --git a/apps/tasks/lib/components/bottom_sheet_pages/organization_bottom_sheet.dart b/apps/tasks/lib/components/bottom_sheet_pages/organization_bottom_sheet.dart index 6ab3b5ad..43b29345 100644 --- a/apps/tasks/lib/components/bottom_sheet_pages/organization_bottom_sheet.dart +++ b/apps/tasks/lib/components/bottom_sheet_pages/organization_bottom_sheet.dart @@ -10,6 +10,7 @@ import 'package:helpwave_widget/navigation.dart'; import 'package:helpwave_widget/text_input.dart'; import 'package:helpwave_widget/widgets.dart'; import 'package:provider/provider.dart'; +import 'package:tasks/components/bottom_sheet_pages/organization_members_bottom_sheet.dart'; import 'package:tasks/components/bottom_sheet_pages/wards_bottom_sheet_page.dart'; import 'package:tasks/screens/settings_screen.dart'; @@ -69,7 +70,7 @@ class OrganizationBottomSheetPage extends StatelessWidget { Text(context.localization!.settings, style: context.theme.textTheme.titleMedium), const SizedBox(height: distanceTiny), RoundedListTiles( - items: [ + children: [ NavigationListTile( icon: Icons.house_rounded, title: context.localization!.wards, @@ -81,7 +82,7 @@ class OrganizationBottomSheetPage extends StatelessWidget { icon: Icons.person, title: context.localization!.members, onTap: () { - // TODO navigate to members page + NavigationStackController.of(context).push(OrganizationMembersBottomSheetPage(organizationId: organizationId)); }, ), NavigationListTile( diff --git a/apps/tasks/lib/components/bottom_sheet_pages/organization_members_bottom_sheet.dart b/apps/tasks/lib/components/bottom_sheet_pages/organization_members_bottom_sheet.dart new file mode 100644 index 00000000..4f3e1be4 --- /dev/null +++ b/apps/tasks/lib/components/bottom_sheet_pages/organization_members_bottom_sheet.dart @@ -0,0 +1,110 @@ +import 'package:flutter/material.dart'; +import 'package:helpwave_localization/localization.dart'; +import 'package:helpwave_service/auth.dart'; +import 'package:helpwave_service/user.dart'; +import 'package:helpwave_theme/constants.dart'; +import 'package:helpwave_theme/util.dart'; +import 'package:helpwave_widget/bottom_sheets.dart'; +import 'package:helpwave_widget/lists.dart'; +import 'package:helpwave_widget/loading.dart'; +import 'package:provider/provider.dart'; + +class OrganizationMembersBottomSheetPage extends StatelessWidget { + final String organizationId; + + const OrganizationMembersBottomSheetPage({super.key, required this.organizationId}); + + @override + Widget build(BuildContext context) { + return ChangeNotifierProvider( + create: (context) => OrganizationMemberController(organizationId), + child: BottomSheetPage( + header: BottomSheetHeader.navigation( + context, + title: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Text(context.localization!.members, style: context.theme.textTheme.titleMedium), + Text(CurrentWardService().currentWard?.organizationName ?? "", + style: TextStyle(color: context.theme.hintColor)), + ], + ), + trailing: BottomSheetAction( + icon: Icons.add, + onPressed: () { + // TODO show dialog + }, + ), + ), + child: Flexible( + child: ListView( + children: [ + Consumer(builder: (context, controller, child) { + return LoadingAndErrorWidget( + state: controller.state, + child: RoundedListTiles( + children: controller.members + .map( + (member) => ListTile( + leading: Icon( + Icons.person_rounded, + color: context.theme.colorScheme.primary, + ), + title: Text(member.name, style: const TextStyle(fontWeight: FontWeight.w700),), + subtitle: Text(member.email, style: TextStyle(color: context.theme.hintColor),), + trailing: IconButton( + onPressed: () { + controller.removeMember(organizationId: organizationId, userId: member.id); + }, + icon: const Icon( + Icons.remove, + color: Colors.red, // TODO use theme + ), + ), + ), + ) + .toList(), + ), + ); + }), + Padding( + padding: const EdgeInsets.only(top: distanceMedium, bottom: distanceTiny), + child: Text( + context.localization!.invitations, + style: context.theme.textTheme.titleMedium, + ), + ), + Consumer(builder: (context, controller, child) { + return LoadingAndErrorWidget( + state: controller.state, + child: RoundedListTiles( + children: controller.invitations + .map( + (invitation) => ListTile( + leading: Icon( + Icons.email_rounded, + color: context.theme.colorScheme.primary, + ), + title: Text(invitation.email), + trailing: IconButton( + onPressed: () { + controller.revokeInvitation(invitationId: invitation.id); + }, + icon: const Icon( + Icons.remove, + color: Colors.red, // TODO use theme + ), + ), + ), + ) + .toList(), + ), + ); + }), + ], + ), + ), + ), + ); + } +} diff --git a/apps/tasks/lib/components/bottom_sheet_pages/user_bottom_sheet.dart b/apps/tasks/lib/components/bottom_sheet_pages/user_bottom_sheet.dart index 2effb3b0..20893229 100644 --- a/apps/tasks/lib/components/bottom_sheet_pages/user_bottom_sheet.dart +++ b/apps/tasks/lib/components/bottom_sheet_pages/user_bottom_sheet.dart @@ -63,7 +63,7 @@ class UserBottomSheetPage extends StatelessWidget { ), ), const SizedBox(height: distanceBig), - RoundedListTiles(items: [ + RoundedListTiles(children: [ ListTile( leading: Icon( Icons.house_rounded, diff --git a/apps/tasks/lib/components/bottom_sheet_pages/wards_bottom_sheet_page.dart b/apps/tasks/lib/components/bottom_sheet_pages/wards_bottom_sheet_page.dart index 5d974fad..858dd283 100644 --- a/apps/tasks/lib/components/bottom_sheet_pages/wards_bottom_sheet_page.dart +++ b/apps/tasks/lib/components/bottom_sheet_pages/wards_bottom_sheet_page.dart @@ -47,7 +47,7 @@ class WardsBottomSheetPage extends StatelessWidget { children: [ const SizedBox(height: distanceMedium), RoundedListTiles( - items: data + children: data .map( (ward) => NavigationListTile( icon: Icons.house_rounded, diff --git a/apps/tasks/lib/screens/settings_screen.dart b/apps/tasks/lib/screens/settings_screen.dart index c39f213a..87cc7951 100644 --- a/apps/tasks/lib/screens/settings_screen.dart +++ b/apps/tasks/lib/screens/settings_screen.dart @@ -220,7 +220,7 @@ class SettingsBottomSheetPage extends StatelessWidget { children: [ titleBuilder(context.localization!.personalSettings), RoundedListTiles( - items: [ + children: [ NavigationListTile( icon: Icons.person, title: context.localization!.personalData, @@ -244,7 +244,7 @@ class SettingsBottomSheetPage extends StatelessWidget { data: OrganizationService().getOrganizationsForUser(), thenWidgetBuilder: (context, data) { return RoundedListTiles( - items: data + children: data .map((organization) => NavigationListTile( icon: Icons.apartment_rounded, title: organization.longName, @@ -261,7 +261,7 @@ class SettingsBottomSheetPage extends StatelessWidget { const SizedBox(height: distanceMedium), titleBuilder(context.localization!.appearance), RoundedListTiles( - items: [ + children: [ ListTile( leading: Icon(Icons.brightness_medium, color: context.theme.colorScheme.primary), title: Text(context.localization!.darkMode, style: const TextStyle(fontWeight: FontWeight.bold)), @@ -356,7 +356,7 @@ class SettingsBottomSheetPage extends StatelessWidget { const SizedBox(height: distanceMedium), titleBuilder(context.localization!.other), RoundedListTiles( - items: [ + children: [ NavigationListTile( icon: Icons.info_outline, title: context.localization!.licenses, diff --git a/packages/helpwave_localization/lib/l10n/app_de.arb b/packages/helpwave_localization/lib/l10n/app_de.arb index 225dba5f..27199d1e 100644 --- a/packages/helpwave_localization/lib/l10n/app_de.arb +++ b/packages/helpwave_localization/lib/l10n/app_de.arb @@ -180,8 +180,12 @@ "shortName": "Abbkürzung", "longName": "Name", "contactEmail": "Kontakt E-Mail", + "member": "Mitglied", "members": "Mitglieder", "properties": "Eigenschaften", "dangerZone": "Gefahrenzone", - "organizationDangerZoneDescription": "Vorsicht, das löschen einer Organization ist permanent und kann nicht rückgängig gemacht werden!" + "organizationDangerZoneDescription": "Vorsicht, das löschen einer Organization ist permanent und kann nicht rückgängig gemacht werden!", + "invitations": "Einladungen", + "revoke": "Zurückziehen", + "nothingYet": "Bis jetzt noch nichts" } diff --git a/packages/helpwave_localization/lib/l10n/app_en.arb b/packages/helpwave_localization/lib/l10n/app_en.arb index 60386f6c..51b7e6a4 100644 --- a/packages/helpwave_localization/lib/l10n/app_en.arb +++ b/packages/helpwave_localization/lib/l10n/app_en.arb @@ -180,8 +180,12 @@ "shortName": "Short Name", "longName": "Long Name", "contactEmail": "Contact E-Mail", + "member": "Member", "members": "Members", "properties": "Properties", "dangerZone": "Danger Zone", - "organizationDangerZoneDescription": "Deleting the organization is a permanent action and cannot be undone be careful!" + "organizationDangerZoneDescription": "Deleting the organization is a permanent action and cannot be undone be careful!", + "invitations": "Invitations", + "revoke": "Revoke", + "nothingYet": "Nothing yet" } diff --git a/packages/helpwave_service/lib/src/api/offline/offline_client_store.dart b/packages/helpwave_service/lib/src/api/offline/offline_client_store.dart index 4f386fce..9f61072c 100644 --- a/packages/helpwave_service/lib/src/api/offline/offline_client_store.dart +++ b/packages/helpwave_service/lib/src/api/offline/offline_client_store.dart @@ -5,6 +5,7 @@ import 'package:helpwave_service/src/api/tasks/offline_clients/room_offline_clie import 'package:helpwave_service/src/api/tasks/offline_clients/task_offline_client.dart'; import 'package:helpwave_service/src/api/tasks/offline_clients/template_offline_client.dart'; import 'package:helpwave_service/src/api/tasks/offline_clients/ward_offline_client.dart'; +import 'package:helpwave_service/src/api/user/offline_clients/invitation_offline_service.dart'; import 'package:helpwave_service/src/api/user/offline_clients/organization_offline_client.dart'; import 'package:helpwave_service/src/api/user/offline_clients/user_offline_client.dart'; import 'package:helpwave_util/lists.dart'; @@ -143,6 +144,7 @@ class OfflineClientStore { final SubtaskOfflineService subtaskStore = SubtaskOfflineService(); final TaskTemplateOfflineService taskTemplateStore = TaskTemplateOfflineService(); final TaskTemplateSubtaskOfflineService taskTemplateSubtaskStore = TaskTemplateSubtaskOfflineService(); + final InvitationOfflineService invitationStore = InvitationOfflineService(); void reset() { organizationStore.organizations = initialOrganizations; diff --git a/packages/helpwave_service/lib/src/api/tasks/offline_clients/patient_offline_client.dart b/packages/helpwave_service/lib/src/api/tasks/offline_clients/patient_offline_client.dart index 97e62b46..af78180b 100644 --- a/packages/helpwave_service/lib/src/api/tasks/offline_clients/patient_offline_client.dart +++ b/packages/helpwave_service/lib/src/api/tasks/offline_clients/patient_offline_client.dart @@ -156,7 +156,7 @@ class PatientOfflineClient extends PatientServiceClient { assignedUserId: task.assigneeId, description: task.notes, public: task.isPublicVisible, - status: GRPCTypeConverter.taskStatusToGRPC(task.status), + status: TasksGRPCTypeConverter.taskStatusToGRPC(task.status), subtasks: OfflineClientStore() .subtaskStore .findSubtasks(task.id) @@ -215,7 +215,7 @@ class PatientOfflineClient extends PatientServiceClient { assignedUserId: task.assigneeId, description: task.notes, public: task.isPublicVisible, - status: GRPCTypeConverter.taskStatusToGRPC(task.status), + status: TasksGRPCTypeConverter.taskStatusToGRPC(task.status), subtasks: OfflineClientStore() .subtaskStore .findSubtasks(task.id) diff --git a/packages/helpwave_service/lib/src/api/tasks/offline_clients/task_offline_client.dart b/packages/helpwave_service/lib/src/api/tasks/offline_clients/task_offline_client.dart index 27b1fae0..7aad563b 100644 --- a/packages/helpwave_service/lib/src/api/tasks/offline_clients/task_offline_client.dart +++ b/packages/helpwave_service/lib/src/api/tasks/offline_clients/task_offline_client.dart @@ -210,7 +210,7 @@ class TaskOfflineClient extends TaskServiceClient { id: task.id, name: task.name, description: task.notes, - status: GRPCTypeConverter.taskStatusToGRPC(task.status), + status: TasksGRPCTypeConverter.taskStatusToGRPC(task.status), dueAt: task.dueDate == null ? null : Timestamp.fromDateTime(task.dueDate!), createdBy: task.createdBy, createdAt: task.creationDate == null ? null : Timestamp.fromDateTime(task.creationDate!), @@ -230,7 +230,7 @@ class TaskOfflineClient extends TaskServiceClient { id: task.id, name: task.name, description: task.notes, - status: GRPCTypeConverter.taskStatusToGRPC(task.status), + status: TasksGRPCTypeConverter.taskStatusToGRPC(task.status), dueAt: task.dueDate == null ? null : Timestamp.fromDateTime(task.dueDate!), createdBy: task.createdBy, createdAt: task.creationDate == null ? null : Timestamp.fromDateTime(task.creationDate!), @@ -260,7 +260,7 @@ class TaskOfflineClient extends TaskServiceClient { id: task.id, name: task.name, description: task.notes, - status: GRPCTypeConverter.taskStatusToGRPC(task.status), + status: TasksGRPCTypeConverter.taskStatusToGRPC(task.status), dueAt: task.dueDate == null ? null : Timestamp.fromDateTime(task.dueDate!), createdBy: task.createdBy, createdAt: task.creationDate == null ? null : Timestamp.fromDateTime(task.creationDate!), @@ -337,7 +337,7 @@ class TaskOfflineClient extends TaskServiceClient { creationDate: DateTime.now(), createdBy: OfflineClientStore().userStore.users[0].id, dueDate: request.hasDueAt() ? request.dueAt.toDateTime() : null, - status: GRPCTypeConverter.taskStatusFromGRPC(request.initialStatus), + status: TasksGRPCTypeConverter.taskStatusFromGRPC(request.initialStatus), isPublicVisible: request.public, assigneeId: request.assignedUserId, ); @@ -362,7 +362,7 @@ class TaskOfflineClient extends TaskServiceClient { final update = TaskUpdate( id: request.id, name: request.name, - status: GRPCTypeConverter.taskStatusFromGRPC(request.status), + status: TasksGRPCTypeConverter.taskStatusFromGRPC(request.status), isPublicVisible: request.public, notes: request.description, dueDate: request.hasDueAt() ? request.dueAt.toDateTime() : null, diff --git a/packages/helpwave_service/lib/src/api/tasks/services/patient_svc.dart b/packages/helpwave_service/lib/src/api/tasks/services/patient_svc.dart index 5695ddbe..06c3ba6d 100644 --- a/packages/helpwave_service/lib/src/api/tasks/services/patient_svc.dart +++ b/packages/helpwave_service/lib/src/api/tasks/services/patient_svc.dart @@ -32,7 +32,7 @@ class PatientService { id: task.id, name: task.name, notes: task.description, - status: GRPCTypeConverter.taskStatusFromGRPC(task.status), + status: TasksGRPCTypeConverter.taskStatusFromGRPC(task.status), isPublicVisible: task.public, assigneeId: task.assignedUserId, subtasks: task.subtasks @@ -103,7 +103,7 @@ class PatientService { name: task.name, notes: task.description, assigneeId: task.assignedUserId, - status: GRPCTypeConverter.taskStatusFromGRPC(task.status), + status: TasksGRPCTypeConverter.taskStatusFromGRPC(task.status), isPublicVisible: task.public, subtasks: task.subtasks .map((subtask) => Subtask( diff --git a/packages/helpwave_service/lib/src/api/tasks/services/task_svc.dart b/packages/helpwave_service/lib/src/api/tasks/services/task_svc.dart index f32e175f..5f7665ea 100644 --- a/packages/helpwave_service/lib/src/api/tasks/services/task_svc.dart +++ b/packages/helpwave_service/lib/src/api/tasks/services/task_svc.dart @@ -26,7 +26,7 @@ class TaskService { name: task.name, notes: task.description, isPublicVisible: task.public, - status: GRPCTypeConverter.taskStatusFromGRPC(task.status), + status: TasksGRPCTypeConverter.taskStatusFromGRPC(task.status), assigneeId: task.assignedUserId, dueDate: task.dueAt.toDateTime(), subtasks: task.subtasks @@ -56,7 +56,7 @@ class TaskService { name: response.name, notes: response.description, isPublicVisible: response.public, - status: GRPCTypeConverter.taskStatusFromGRPC(response.status), + status: TasksGRPCTypeConverter.taskStatusFromGRPC(response.status), assigneeId: response.assignedUserId, dueDate: response.dueAt.toDateTime(), patient: PatientMinimal(id: response.patient.id, name: response.patient.humanReadableIdentifier), @@ -88,7 +88,7 @@ class TaskService { name: task.name, notes: task.description, isPublicVisible: task.public, - status: GRPCTypeConverter.taskStatusFromGRPC(task.status), + status: TasksGRPCTypeConverter.taskStatusFromGRPC(task.status), assigneeId: task.assignedUserId, dueDate: task.dueAt.toDateTime(), patient: PatientMinimal(id: task.patient.id, name: task.patient.humanReadableIdentifier), @@ -111,7 +111,7 @@ class TaskService { CreateTaskRequest request = CreateTaskRequest( name: task.name, description: task.notes, - initialStatus: GRPCTypeConverter.taskStatusToGRPC(task.status), + initialStatus: TasksGRPCTypeConverter.taskStatusToGRPC(task.status), dueAt: task.dueDate != null ? Timestamp.fromDateTime(task.dueDate!) : null, patientId: !task.patient.isCreating ? task.patient.id : null, public: task.isPublicVisible, @@ -202,7 +202,7 @@ class TaskService { description: notes, dueAt: dueDate != null ? Timestamp.fromDateTime(dueDate) : null, public: isPublic, - status: status != null ? GRPCTypeConverter.taskStatusToGRPC(status) : null, + status: status != null ? TasksGRPCTypeConverter.taskStatusToGRPC(status) : null, ); UpdateTaskResponse response = await taskService.updateTask( diff --git a/packages/helpwave_service/lib/src/api/tasks/util/task_status_mapping.dart b/packages/helpwave_service/lib/src/api/tasks/util/task_status_mapping.dart index 97b65819..ccec18fd 100644 --- a/packages/helpwave_service/lib/src/api/tasks/util/task_status_mapping.dart +++ b/packages/helpwave_service/lib/src/api/tasks/util/task_status_mapping.dart @@ -1,7 +1,7 @@ import 'package:helpwave_service/src/api/tasks/data_types/task.dart' as task_lib; import 'package:helpwave_proto_dart/services/tasks_svc/v1/types.pbenum.dart' as proto; -class GRPCTypeConverter { +class TasksGRPCTypeConverter { static proto.TaskStatus taskStatusToGRPC(task_lib.TaskStatus status) { switch (status) { case task_lib.TaskStatus.todo: diff --git a/packages/helpwave_service/lib/src/api/user/controllers/index.dart b/packages/helpwave_service/lib/src/api/user/controllers/index.dart index 3a08f297..bc371af8 100644 --- a/packages/helpwave_service/lib/src/api/user/controllers/index.dart +++ b/packages/helpwave_service/lib/src/api/user/controllers/index.dart @@ -1,2 +1,3 @@ export 'user_controller.dart'; export 'organization_controller.dart'; +export 'organization_member_controller.dart'; diff --git a/packages/helpwave_service/lib/src/api/user/controllers/organization_member_controller.dart b/packages/helpwave_service/lib/src/api/user/controllers/organization_member_controller.dart new file mode 100644 index 00000000..fc1094bd --- /dev/null +++ b/packages/helpwave_service/lib/src/api/user/controllers/organization_member_controller.dart @@ -0,0 +1,84 @@ +import 'package:helpwave_service/src/api/user/data_types/invitation.dart'; +import 'package:helpwave_util/loading.dart'; +import '../../../../user.dart'; + +/// The Controller for managing a [Organization]'s [User]s +class OrganizationMemberController extends LoadingChangeNotifier { + /// The id of the [Organization] + final String organizationId; + + /// The current [User]s + List _members = []; + + /// The current [User]s + List get members => _members; + + set members(List value) { + _members = value; + changeState(LoadingState.loaded); + } + + /// The current [Invitation]s + List _invitations = []; + + /// The current [Invitation]s + List get invitations => _invitations; + + set invitations(List value) { + _invitations = value; + changeState(LoadingState.loaded); + } + + OrganizationMemberController(this.organizationId) { + load(); + } + + /// A function to load the [Organization] + Future load() async { + loadMembers() async { + _members = await OrganizationService().getMembersByOrganization(organizationId); + _invitations = await OrganizationService().getInvitationsByOrganization(organizationId: organizationId); + changeState(LoadingState.loaded); + } + + loadHandler( + future: loadMembers(), + ); + } + + Future inviteMember({required String organizationId, required String email}) async { + inviteMember() async { + await OrganizationService().inviteMember(organizationId: organizationId, email: email).then((value) => + load() + ); + } + + loadHandler( + future: inviteMember(), + ); + } + + Future removeMember({required String organizationId, required String userId}) async { + remove() async { + await OrganizationService().removeMember(organizationId: organizationId, userId: userId).then((value) => + load() + ); + } + + loadHandler( + future: remove(), + ); + } + + Future revokeInvitation({required String invitationId}) async { + revokeInvite() async { + await OrganizationService().revokeInvitation(invitationId: invitationId).then((value) => + invitations = invitations.where((element) => element.id != invitationId).toList() + ); + } + + loadHandler( + future: revokeInvite(), + ); + } +} diff --git a/packages/helpwave_service/lib/src/api/user/data_types/invitation.dart b/packages/helpwave_service/lib/src/api/user/data_types/invitation.dart new file mode 100644 index 00000000..6aae8bdd --- /dev/null +++ b/packages/helpwave_service/lib/src/api/user/data_types/invitation.dart @@ -0,0 +1,48 @@ +import '../../../../user.dart'; + +enum InvitationState { + unspecified, + pending, + accepted, + rejected, + revoked, +} + +class Invitation { + String id; + InvitationState state; + + /// The email of the invited [User] + String email; + + /// The identifier of the [Organization] to which the user is invited + String organizationId; + + /// The [Organization] + Organization? organization; + + Invitation({ + required this.id, + required this.organizationId, + required this.email, + required this.state, + this.organization + }) : assert(organization == null || organizationId == organization.id); + + // CopyWith method + Invitation copyWith({ + String? id, + InvitationState? state, + String? email, + String? organizationId, + Organization? organization, + }) { + return Invitation( + id: id ?? this.id, + state: state ?? this.state, + email: email ?? this.email, + organizationId: organizationId ?? this.organizationId, + organization: organization ?? this.organization, + ); + } +} diff --git a/packages/helpwave_service/lib/src/api/user/offline_clients/invitation_offline_service.dart b/packages/helpwave_service/lib/src/api/user/offline_clients/invitation_offline_service.dart new file mode 100644 index 00000000..797bcb7c --- /dev/null +++ b/packages/helpwave_service/lib/src/api/user/offline_clients/invitation_offline_service.dart @@ -0,0 +1,40 @@ +import '../data_types/invitation.dart'; + +class InvitationOfflineService { + List invitations = []; + + Invitation? find(String id) { + int index = invitations.indexWhere((org) => org.id == id); + if (index == -1) { + return null; + } + return invitations[index]; + } + + List findInvitations() { + return invitations; + } + + void create(Invitation invitation) { + invitations.add(invitation); + } + + void changeState(String invitationId, InvitationState state) { + bool found = false; + invitations = invitations.map((invite) { + if (invite.id == invitationId) { + found = true; + return invite.copyWith(state: state); + } + return invite; + }).toList(); + + if (!found) { + throw Exception("ChangeState: Could not find invitation with id $invitationId"); + } + } + + void delete(String invitationId) { + invitations.removeWhere((org) => org.id == invitationId); + } +} diff --git a/packages/helpwave_service/lib/src/api/user/offline_clients/organization_offline_client.dart b/packages/helpwave_service/lib/src/api/user/offline_clients/organization_offline_client.dart index 6b01f74a..2c52669f 100644 --- a/packages/helpwave_service/lib/src/api/user/offline_clients/organization_offline_client.dart +++ b/packages/helpwave_service/lib/src/api/user/offline_clients/organization_offline_client.dart @@ -2,7 +2,9 @@ import 'package:grpc/grpc.dart'; import 'package:helpwave_proto_dart/services/user_svc/v1/organization_svc.pbgrpc.dart'; import 'package:helpwave_service/src/api/offline/offline_client_store.dart'; import 'package:helpwave_service/src/api/offline/util.dart'; +import 'package:helpwave_service/src/api/user/util/type_converter.dart'; import 'package:helpwave_service/user.dart'; +import '../data_types/invitation.dart' as invite_types; class OrganizationUpdate { String id; @@ -152,7 +154,8 @@ class OrganizationOfflineClient extends OrganizationServiceClient { } @override - ResponseFuture getOrganizationsForUser(GetOrganizationsForUserRequest request, {CallOptions? options}) { + ResponseFuture getOrganizationsForUser(GetOrganizationsForUserRequest request, + {CallOptions? options}) { final organizations = OfflineClientStore().organizationStore.findOrganizations().map((org) { final members = OfflineClientStore() .userStore @@ -171,7 +174,6 @@ class OrganizationOfflineClient extends OrganizationServiceClient { }).toList(); return MockResponseFuture.value(GetOrganizationsForUserResponse()..organizations.addAll(organizations)); - } @override @@ -230,42 +232,128 @@ class OrganizationOfflineClient extends OrganizationServiceClient { } @override - ResponseFuture getInvitationsByOrganization(GetInvitationsByOrganizationRequest request, {CallOptions? options}) { - return MockResponseFuture.value(GetInvitationsByOrganizationResponse()); + ResponseFuture getInvitationsByOrganization( + GetInvitationsByOrganizationRequest request, + {CallOptions? options}) { + List invitations = OfflineClientStore() + .invitationStore + .invitations + .where((element) => element.organizationId == request.organizationId) + .toList(); + if (request.hasState()) { + final invitationState = UserGRPCTypeConverter.invitationStateFromGRPC(request.state); + invitations = invitations.where((element) => element.state == invitationState).toList(); + } + + final response = GetInvitationsByOrganizationResponse( + invitations: invitations.map((invitation) => GetInvitationsByOrganizationResponse_Invitation( + id: invitation.id, + organizationId: invitation.organizationId, + email: invitation.email, + state: UserGRPCTypeConverter.invitationStateToGRPC(invitation.state), + ))); + + return MockResponseFuture.value(response); } @override - ResponseFuture getInvitationsByUser(GetInvitationsByUserRequest request, {CallOptions? options}) { - return MockResponseFuture.value(GetInvitationsByUserResponse()); + ResponseFuture getInvitationsByUser(GetInvitationsByUserRequest request, + {CallOptions? options}) { + final user = OfflineClientStore().userStore.users[0]; + List invitations = + OfflineClientStore().invitationStore.invitations.where((element) => element.organizationId == user.id).toList(); + if (request.hasState()) { + final invitationState = UserGRPCTypeConverter.invitationStateFromGRPC(request.state); + invitations = invitations.where((element) => element.state == invitationState).toList(); + } + + final response = GetInvitationsByUserResponse(invitations: invitations.map((invitation) { + final invite = GetInvitationsByUserResponse_Invitation( + id: invitation.id, + email: invitation.email, + state: UserGRPCTypeConverter.invitationStateToGRPC(invitation.state), + ); + final organization = OfflineClientStore().organizationStore.find(invitation.organizationId)!; + invite.organization = GetInvitationsByUserResponse_Invitation_Organization( + id: organization.id, + longName: organization.longName, + avatarUrl: organization.avatarURL, + ); + return invite; + })); + + return MockResponseFuture.value(response); } @override ResponseFuture inviteMember(InviteMemberRequest request, {CallOptions? options}) { - throw UnimplementedError('Not implemented yet'); + assert(OfflineClientStore().userStore.users.indexWhere((element) => element.email == request.email) != -1); + assert(OfflineClientStore().invitationStore.invitations.indexWhere( + (element) => element.organizationId == request.organizationId && element.email == request.email) == -1); + + final invite = invite_types.Invitation( + id: DateTime.now().toString(), + organizationId: request.organizationId, + email: request.email, + state: invite_types.InvitationState.pending, + ); + + return MockResponseFuture.value(InviteMemberResponse(id: invite.id)); } @override ResponseFuture revokeInvitation(RevokeInvitationRequest request, {CallOptions? options}) { - throw UnimplementedError('Not implemented yet'); + final invite = OfflineClientStore().invitationStore.find(request.invitationId); + + if(invite == null){ + throw "Invitation with id ${request.invitationId} not found"; + } + if(invite_types.InvitationState.pending != invite.state){ + throw "Only pending Invitations can be revoked"; + } + OfflineClientStore().invitationStore.changeState(request.invitationId, invite_types.InvitationState.accepted); + + return MockResponseFuture.value(RevokeInvitationResponse()); } @override ResponseFuture acceptInvitation(AcceptInvitationRequest request, {CallOptions? options}) { - throw UnimplementedError('Not implemented yet'); + final invite = OfflineClientStore().invitationStore.find(request.invitationId); + + if(invite == null){ + throw "Invitation with id ${request.invitationId} not found"; + } + if(invite_types.InvitationState.pending != invite.state){ + throw "Only pending Invitations can be accepted"; + } + OfflineClientStore().invitationStore.changeState(request.invitationId, invite_types.InvitationState.accepted); + + return MockResponseFuture.value(AcceptInvitationResponse()); } @override - ResponseFuture declineInvitation(DeclineInvitationRequest request, {CallOptions? options}) { - throw UnimplementedError('Not implemented yet'); + ResponseFuture declineInvitation(DeclineInvitationRequest request, + {CallOptions? options}) { + final invite = OfflineClientStore().invitationStore.find(request.invitationId); + + if(invite == null){ + throw "Invitation with id ${request.invitationId} not found"; + } + if(invite_types.InvitationState.pending != invite.state){ + throw "Only pending Invitations can be de"; + } + OfflineClientStore().invitationStore.changeState(request.invitationId, invite_types.InvitationState.accepted); + + return MockResponseFuture.value(DeclineInvitationResponse()); } @override ResponseFuture addMember(AddMemberRequest request, {CallOptions? options}) { - throw UnimplementedError('Not implemented yet'); + return MockResponseFuture.value(AddMemberResponse()); } @override ResponseFuture removeMember(RemoveMemberRequest request, {CallOptions? options}) { - throw UnimplementedError('Not implemented yet'); + return MockResponseFuture.value(RemoveMemberResponse()); } } diff --git a/packages/helpwave_service/lib/src/api/user/services/organization_svc.dart b/packages/helpwave_service/lib/src/api/user/services/organization_svc.dart index 7120ae90..7e0956eb 100644 --- a/packages/helpwave_service/lib/src/api/user/services/organization_svc.dart +++ b/packages/helpwave_service/lib/src/api/user/services/organization_svc.dart @@ -1,7 +1,9 @@ import 'package:grpc/grpc.dart'; import 'package:helpwave_proto_dart/services/user_svc/v1/organization_svc.pbgrpc.dart'; import 'package:helpwave_service/auth.dart'; +import 'package:helpwave_service/src/api/user/data_types/invitation.dart' as invitation; import 'package:helpwave_service/src/api/user/user_api_service_clients.dart'; +import 'package:helpwave_service/src/api/user/util/type_converter.dart'; import '../data_types/index.dart'; /// The GRPC Service for [Organization]s @@ -56,6 +58,40 @@ class OrganizationService { return organizations; } + Future update({ + required String id, + String? shortName, + String? longName, + String? email, + bool? isPersonal, + String? avatarUrl, + }) async { + UpdateOrganizationRequest request = UpdateOrganizationRequest( + id: id, + longName: longName, + shortName: shortName, + isPersonal: isPersonal, + contactEmail: email, + avatarUrl: avatarUrl, + ); + await organizationService.updateOrganization( + request, + options: CallOptions( + metadata: UserAPIServiceClients().getMetaData(organizationId: id), + ), + ); + } + + Future delete(String id) async { + DeleteOrganizationRequest request = DeleteOrganizationRequest(id: id); + await organizationService.deleteOrganization( + request, + options: CallOptions( + metadata: UserAPIServiceClients().getMetaData(organizationId: id), + ), + ); + } + /// Loads the members of an [Organization] as [User]s Future> getMembersByOrganization(String organizationId) async { GetMembersByOrganizationRequest request = GetMembersByOrganizationRequest(id: organizationId); @@ -79,36 +115,116 @@ class OrganizationService { return users; } - Future update({ - required String id, - String? shortName, - String? longName, - String? email, - bool? isPersonal, - String? avatarUrl, + Future addMember({required String organizationId, required String userId}) async { + AddMemberRequest request = AddMemberRequest(id: organizationId, userId: userId); + await organizationService.addMember( + request, + options: CallOptions( + metadata: UserAPIServiceClients().getMetaData(organizationId: organizationId), + ), + ); + } + + Future removeMember({required String organizationId, required String userId}) async { + RemoveMemberRequest request = RemoveMemberRequest(id: organizationId, userId: userId); + await organizationService.removeMember( + request, + options: CallOptions( + metadata: UserAPIServiceClients().getMetaData(organizationId: organizationId), + ), + ); + } + + Future inviteMember({required String organizationId, required String email}) async { + InviteMemberRequest request = InviteMemberRequest(organizationId: organizationId, email: email); + InviteMemberResponse response = await organizationService.inviteMember( + request, + options: CallOptions( + metadata: UserAPIServiceClients().getMetaData(organizationId: organizationId), + ), + ); + + return invitation.Invitation( + id: response.id, + organizationId: organizationId, + email: email, + state: invitation.InvitationState.pending, + ); + } + + Future> getInvitationsByOrganization({ + required String organizationId, + invitation.InvitationState? state, }) async { - UpdateOrganizationRequest request = UpdateOrganizationRequest( - id: id, - longName: longName, - shortName: shortName, - isPersonal: isPersonal, - contactEmail: email, - avatarUrl: avatarUrl, + GetInvitationsByOrganizationRequest request = GetInvitationsByOrganizationRequest( + organizationId: organizationId, + state: state == null ? null : UserGRPCTypeConverter.invitationStateToGRPC(state), ); - await organizationService.updateOrganization( + + GetInvitationsByOrganizationResponse response = await organizationService.getInvitationsByOrganization( request, options: CallOptions( - metadata: UserAPIServiceClients().getMetaData(organizationId: id), + metadata: UserAPIServiceClients().getMetaData(organizationId: organizationId), ), ); + + return response.invitations.map((invite) => invitation.Invitation( + id: invite.id, + organizationId: organizationId, + email: invite.email, + state: UserGRPCTypeConverter.invitationStateFromGRPC(invite.state), + )).toList(); } - Future delete(String id) async { - DeleteOrganizationRequest request = DeleteOrganizationRequest(id: id); - await organizationService.deleteOrganization( + Future> getInvitationsByUser({ + required String organizationId, + invitation.InvitationState? state, + }) async { + GetInvitationsByUserRequest request = GetInvitationsByUserRequest( + state: state == null ? null : UserGRPCTypeConverter.invitationStateToGRPC(state), + ); + + GetInvitationsByUserResponse response = await organizationService.getInvitationsByUser( request, options: CallOptions( - metadata: UserAPIServiceClients().getMetaData(organizationId: id), + metadata: UserAPIServiceClients().getMetaData(organizationId: organizationId), + ), + ); + + return response.invitations.map((invite) => invitation.Invitation( + id: invite.id, + organizationId: organizationId, + email: invite.email, + state: UserGRPCTypeConverter.invitationStateFromGRPC(invite.state), + )).toList(); + } + + Future acceptInvitation({required String invitationId}) async { + AcceptInvitationRequest request = AcceptInvitationRequest(invitationId: invitationId); + await organizationService.acceptInvitation( + request, + options: CallOptions( + metadata: UserAPIServiceClients().getMetaData(organizationId: CurrentWardService().currentWard!.organizationId), + ), + ); + } + + Future declineInvitation({required String invitationId}) async { + DeclineInvitationRequest request = DeclineInvitationRequest(invitationId: invitationId); + await organizationService.declineInvitation( + request, + options: CallOptions( + metadata: UserAPIServiceClients().getMetaData(organizationId: CurrentWardService().currentWard!.organizationId), + ), + ); + } + + Future revokeInvitation({required String invitationId}) async { + RevokeInvitationRequest request = RevokeInvitationRequest(invitationId: invitationId); + await organizationService.revokeInvitation( + request, + options: CallOptions( + metadata: UserAPIServiceClients().getMetaData(organizationId: CurrentWardService().currentWard!.organizationId), ), ); } diff --git a/packages/helpwave_service/lib/src/api/user/util/type_converter.dart b/packages/helpwave_service/lib/src/api/user/util/type_converter.dart new file mode 100644 index 00000000..4b210a88 --- /dev/null +++ b/packages/helpwave_service/lib/src/api/user/util/type_converter.dart @@ -0,0 +1,34 @@ +import 'package:helpwave_service/src/api/user/data_types/invitation.dart' as user_lib; +import 'package:helpwave_proto_dart/services/user_svc/v1/organization_svc.pbenum.dart' as proto; + +class UserGRPCTypeConverter { + static proto.InvitationState invitationStateToGRPC(user_lib.InvitationState status) { + switch (status) { + case user_lib.InvitationState.unspecified: + return proto.InvitationState.INVITATION_STATE_UNSPECIFIED; + case user_lib.InvitationState.pending: + return proto.InvitationState.INVITATION_STATE_PENDING; + case user_lib.InvitationState.accepted: + return proto.InvitationState.INVITATION_STATE_ACCEPTED; + case user_lib.InvitationState.rejected: + return proto.InvitationState.INVITATION_STATE_REJECTED; + case user_lib.InvitationState.revoked: + return proto.InvitationState.INVITATION_STATE_REVOKED; + } + } + + static user_lib.InvitationState invitationStateFromGRPC(proto.InvitationState status) { + switch (status) { + case proto.InvitationState.INVITATION_STATE_PENDING: + return user_lib.InvitationState.pending; + case proto.InvitationState.INVITATION_STATE_ACCEPTED: + return user_lib.InvitationState.accepted; + case proto.InvitationState.INVITATION_STATE_REJECTED: + return user_lib.InvitationState.rejected; + case proto.InvitationState.INVITATION_STATE_REVOKED: + return user_lib.InvitationState.revoked; + default: + return user_lib.InvitationState.unspecified; + } + } +} diff --git a/packages/helpwave_widget/lib/src/bottom_sheets/bottom_sheet_page.dart b/packages/helpwave_widget/lib/src/bottom_sheets/bottom_sheet_page.dart index d9763c50..691f63a2 100644 --- a/packages/helpwave_widget/lib/src/bottom_sheets/bottom_sheet_page.dart +++ b/packages/helpwave_widget/lib/src/bottom_sheets/bottom_sheet_page.dart @@ -1,4 +1,5 @@ import 'package:flutter/material.dart'; +import 'package:helpwave_theme/constants.dart'; import 'package:helpwave_widget/navigation.dart'; import '../../bottom_sheets.dart'; @@ -30,6 +31,7 @@ class BottomSheetPage extends StatelessWidget { header: header ?? BottomSheetHeader.navigation(context), bottomWidget: bottom, mainAxisSize: MainAxisSize.max, + padding: const EdgeInsets.all(paddingMedium).copyWith(bottom: 0), child: child, ); } diff --git a/packages/helpwave_widget/lib/src/lists/rounded_list_tiles.dart b/packages/helpwave_widget/lib/src/lists/rounded_list_tiles.dart index a87c0284..f6a16060 100644 --- a/packages/helpwave_widget/lib/src/lists/rounded_list_tiles.dart +++ b/packages/helpwave_widget/lib/src/lists/rounded_list_tiles.dart @@ -1,16 +1,28 @@ import 'package:flutter/material.dart'; +import 'package:helpwave_localization/localization.dart'; import 'package:helpwave_theme/constants.dart'; import 'package:helpwave_theme/util.dart'; class RoundedListTiles extends StatelessWidget { - final List items; + final List children; - const RoundedListTiles({super.key, required this.items}); + const RoundedListTiles({super.key, required this.children}); @override Widget build(BuildContext context) { const double borderRadius = borderRadiusMedium; + final usedChildren = children.isEmpty + ? [ + SizedBox( + height: 60, + child: Center( + child: Text(context.localization!.nothingYet, style: const TextStyle(fontWeight: FontWeight.w700),), + ), + ) + ] + : children; + return Container( decoration: BoxDecoration( color: context.theme.colorScheme.surface, @@ -25,12 +37,12 @@ class RoundedListTiles extends StatelessWidget { ), child: Column( mainAxisSize: MainAxisSize.min, - children: List.generate(items.length * 2 - 1, (index) { - if(index.isOdd){ + children: List.generate(usedChildren.length * 2 - 1, (index) { + if (index.isOdd) { return const Divider(); } - Widget item = items[index ~/ 2]; - if (index == 0 && items.length == 1) { + Widget item = usedChildren[index ~/ 2]; + if (index == 0 && usedChildren.length == 1) { return ClipRRect( borderRadius: BorderRadius.circular(borderRadius), child: item, @@ -42,7 +54,7 @@ class RoundedListTiles extends StatelessWidget { child: item, ); } - if (index == items.length - 1) { + if (index == usedChildren.length - 1) { return ClipRRect( borderRadius: const BorderRadius.vertical(bottom: Radius.circular(borderRadius)), child: item, From 9a5bc10d29d27eb567d98d5e8c779a094b5c66ab Mon Sep 17 00:00:00 2001 From: Felix Thape Date: Wed, 2 Oct 2024 15:20:21 +0200 Subject: [PATCH 13/36] feat: add draft for room and bed screen --- .../patient_bottom_sheet.dart | 4 +- .../room_overview_bottom_sheet.dart | 66 +++++++++++ .../bottom_sheet_pages/task_bottom_sheet.dart | 2 +- .../bottom_sheet_pages/user_bottom_sheet.dart | 2 +- .../ward_overview_bottom_sheet.dart | 75 ++++++++++++ .../ward_select_bottom_sheet.dart | 2 +- .../wards_bottom_sheet_page.dart | 9 +- apps/tasks/lib/components/user_header.dart | 2 +- apps/tasks/lib/config/config.dart | 6 +- apps/tasks/lib/main.dart | 10 +- apps/tasks/lib/screens/settings_screen.dart | 3 +- .../tasks/lib/screens/ward_select_screen.dart | 9 +- apps/tasks/pubspec.lock | 4 +- .../helpwave_localization/lib/l10n/app_de.arb | 6 +- .../helpwave_localization/lib/l10n/app_en.arb | 6 +- .../src/api/offline/offline_client_store.dart | 4 +- .../tasks/controllers/beds_controller.dart | 107 +++++++++++++++++ .../lib/src/api/tasks/controllers/index.dart | 2 + .../tasks/controllers/rooms_controller.dart | 107 +++++++++++++++++ .../lib/src/api/tasks/data_types/bed.dart | 16 ++- .../lib/src/api/tasks/data_types/room.dart | 4 +- .../offline_clients/bed_offline_client.dart | 18 +-- .../patient_offline_client.dart | 1 - .../lib/src/api/tasks/services/bed_svc.dart | 110 ++++++++++++++++++ .../src/api/tasks/services/patient_svc.dart | 3 +- .../lib/src/api/tasks/services/room_svc.dart | 69 ++++++++++- .../api/tasks/tasks_api_service_clients.dart | 7 +- packages/helpwave_service/pubspec.yaml | 2 +- .../src/content_selection/list_select.dart | 2 +- .../src/loading/loading_future_builder.dart | 8 +- 30 files changed, 609 insertions(+), 57 deletions(-) create mode 100644 apps/tasks/lib/components/bottom_sheet_pages/room_overview_bottom_sheet.dart create mode 100644 apps/tasks/lib/components/bottom_sheet_pages/ward_overview_bottom_sheet.dart create mode 100644 packages/helpwave_service/lib/src/api/tasks/controllers/beds_controller.dart create mode 100644 packages/helpwave_service/lib/src/api/tasks/controllers/rooms_controller.dart diff --git a/apps/tasks/lib/components/bottom_sheet_pages/patient_bottom_sheet.dart b/apps/tasks/lib/components/bottom_sheet_pages/patient_bottom_sheet.dart index c04f5a35..bf28f3ed 100644 --- a/apps/tasks/lib/components/bottom_sheet_pages/patient_bottom_sheet.dart +++ b/apps/tasks/lib/components/bottom_sheet_pages/patient_bottom_sheet.dart @@ -32,7 +32,7 @@ class _PatientBottomSheetState extends State { List flattenedRooms = []; for (RoomWithBedWithMinimalPatient room in rooms) { - for (BedWithMinimalPatient bed in room.beds) { + for (Bed bed in room.beds) { // TODO reconsider the filter to allow switching to bed the patient is in if (bed.patient == null || bed.patient?.id == patientId) { flattenedRooms.add(RoomWithBedFlat(room: room, bed: bed)); @@ -157,7 +157,7 @@ class _PatientBottomSheetState extends State { data: loadRoomsWithBeds(patientController.patient.id), // TODO use a better loading widget loadingWidget: const SizedBox(), - thenWidgetBuilder: (context, beds) { + thenBuilder: (context, beds) { if (beds.isEmpty) { return Text( context.localization!.noFreeBeds, diff --git a/apps/tasks/lib/components/bottom_sheet_pages/room_overview_bottom_sheet.dart b/apps/tasks/lib/components/bottom_sheet_pages/room_overview_bottom_sheet.dart new file mode 100644 index 00000000..389d46bf --- /dev/null +++ b/apps/tasks/lib/components/bottom_sheet_pages/room_overview_bottom_sheet.dart @@ -0,0 +1,66 @@ +import 'package:flutter/material.dart'; +import 'package:helpwave_localization/localization.dart'; +import 'package:helpwave_service/tasks.dart'; +import 'package:helpwave_theme/util.dart'; +import 'package:helpwave_util/loading.dart'; +import 'package:helpwave_widget/bottom_sheets.dart'; +import 'package:helpwave_widget/lists.dart'; +import 'package:helpwave_widget/loading.dart'; +import 'package:provider/provider.dart'; +import 'package:tasks/screens/settings_screen.dart'; + +class RoomOverviewBottomSheetPage extends StatelessWidget { + final String roomId; + + const RoomOverviewBottomSheetPage({super.key, required this.roomId}); + + @override + Widget build(BuildContext context) { + return ChangeNotifierProvider( + create: (_) => BedsController(roomId: roomId), + child: BottomSheetPage( + header: BottomSheetHeader.navigation( + context, + title: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Text(context.localization!.rooms, style: context.theme.textTheme.titleMedium), + LoadingFutureBuilder( + data: RoomService().get(roomId: roomId), + thenBuilder: (context, room) { + return Text(room.name, style: TextStyle(color: context.theme.hintColor)); + }, + loadingWidget: LoadingAndErrorWidget.pulsing(state: LoadingState.loading, child: SizedBox()), + ), + ], + ), + ), + child: Flexible( + child: ListView( + children: [ + Consumer( + builder: (context, controller, _) { + return LoadingAndErrorWidget( + state: controller.state, + child: RoundedListTiles( + children: controller.beds + .map( + (bed) => NavigationListTile( + icon: Icons.bed_rounded, + title: bed.name, + trailingText: bed.patient?.name ?? context.localization!.unassigned, + onTap: () {}, + ), + ) + .toList(), + ), + ); + }, + ), + ], + ), + ), + ), + ); + } +} diff --git a/apps/tasks/lib/components/bottom_sheet_pages/task_bottom_sheet.dart b/apps/tasks/lib/components/bottom_sheet_pages/task_bottom_sheet.dart index 4d8989ed..a4056951 100644 --- a/apps/tasks/lib/components/bottom_sheet_pages/task_bottom_sheet.dart +++ b/apps/tasks/lib/components/bottom_sheet_pages/task_bottom_sheet.dart @@ -174,7 +174,7 @@ class _TaskBottomSheetState extends State { : LoadingFutureBuilder( data: PatientService().getPatientList(), loadingWidget: const PulsingContainer(), - thenWidgetBuilder: (context, patientList) { + thenBuilder: (context, patientList) { List patients = patientList.active + patientList.unassigned; return DropdownButton( underline: const SizedBox(), diff --git a/apps/tasks/lib/components/bottom_sheet_pages/user_bottom_sheet.dart b/apps/tasks/lib/components/bottom_sheet_pages/user_bottom_sheet.dart index 20893229..4e92a999 100644 --- a/apps/tasks/lib/components/bottom_sheet_pages/user_bottom_sheet.dart +++ b/apps/tasks/lib/components/bottom_sheet_pages/user_bottom_sheet.dart @@ -36,7 +36,7 @@ class UserBottomSheetPage extends StatelessWidget { padding: const EdgeInsets.all(paddingSmall).copyWith(top: paddingMedium), child: LoadingFutureBuilder( data: UserService().getSelf(), - thenWidgetBuilder: (context, data) { + thenBuilder: (context, data) { return CircleAvatar( backgroundColor: Colors.transparent, radius: iconSizeMedium, diff --git a/apps/tasks/lib/components/bottom_sheet_pages/ward_overview_bottom_sheet.dart b/apps/tasks/lib/components/bottom_sheet_pages/ward_overview_bottom_sheet.dart new file mode 100644 index 00000000..44eeefc2 --- /dev/null +++ b/apps/tasks/lib/components/bottom_sheet_pages/ward_overview_bottom_sheet.dart @@ -0,0 +1,75 @@ +import 'package:flutter/material.dart'; +import 'package:helpwave_localization/localization.dart'; +import 'package:helpwave_service/tasks.dart'; +import 'package:helpwave_theme/util.dart'; +import 'package:helpwave_widget/bottom_sheets.dart'; +import 'package:helpwave_widget/lists.dart'; +import 'package:helpwave_widget/loading.dart'; +import 'package:helpwave_widget/navigation.dart'; +import 'package:provider/provider.dart'; +import 'package:tasks/components/bottom_sheet_pages/room_overview_bottom_sheet.dart'; +import 'package:tasks/screens/settings_screen.dart'; + +class WardOverviewBottomSheetPage extends StatelessWidget { + final String wardId; + + const WardOverviewBottomSheetPage({super.key, required this.wardId}); + + @override + Widget build(BuildContext context) { + return ChangeNotifierProvider( + create: (_) => RoomsController(wardId: wardId), + child: Consumer( + builder: (context, controller, _) { + return BottomSheetPage( + header: BottomSheetHeader.navigation( + context, + title: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Text(context.localization!.rooms, style: context.theme.textTheme.titleMedium), + LoadingFutureBuilder( + data: WardService().getWard(id: wardId), + thenBuilder: (context, ward) { + return Text(ward.name, style: TextStyle(color: context.theme.hintColor)); + }), + ], + ), + trailing: BottomSheetAction( + icon: Icons.add, + onPressed: () { + controller + .create(RoomWithBedWithMinimalPatient(id: "", name: context.localization!.newRoom, beds: [])); + }, + ), + ), + child: Flexible( + child: ListView( + children: [ + LoadingAndErrorWidget( + state: controller.state, + child: RoundedListTiles( + children: controller.rooms + .map( + (room) => NavigationListTile( + icon: Icons.meeting_room_rounded, + title: room.name, + trailingText: "${room.beds.length} ${context.localization!.beds}", + onTap: () { + NavigationStackController.of(context) + .push(RoomOverviewBottomSheetPage(roomId: room.id)); + }, + ), + ) + .toList(), + ), + ), + ], + ), + ), + ); + }, + ), + ); + } +} diff --git a/apps/tasks/lib/components/bottom_sheet_pages/ward_select_bottom_sheet.dart b/apps/tasks/lib/components/bottom_sheet_pages/ward_select_bottom_sheet.dart index 59624e5d..f83d34ed 100644 --- a/apps/tasks/lib/components/bottom_sheet_pages/ward_select_bottom_sheet.dart +++ b/apps/tasks/lib/components/bottom_sheet_pages/ward_select_bottom_sheet.dart @@ -29,7 +29,7 @@ class WardSelectBottomSheet extends StatelessWidget { child: LoadingFutureBuilder( loadingWidget: const SizedBox(), data: WardService().getWards(organizationId: organizationId), - thenWidgetBuilder: (context, wards) { + thenBuilder: (context, wards) { return Flexible( child: ListView( shrinkWrap: true, diff --git a/apps/tasks/lib/components/bottom_sheet_pages/wards_bottom_sheet_page.dart b/apps/tasks/lib/components/bottom_sheet_pages/wards_bottom_sheet_page.dart index 858dd283..35149794 100644 --- a/apps/tasks/lib/components/bottom_sheet_pages/wards_bottom_sheet_page.dart +++ b/apps/tasks/lib/components/bottom_sheet_pages/wards_bottom_sheet_page.dart @@ -7,6 +7,8 @@ import 'package:helpwave_theme/util.dart'; import 'package:helpwave_widget/bottom_sheets.dart'; import 'package:helpwave_widget/lists.dart'; import 'package:helpwave_widget/loading.dart'; +import 'package:helpwave_widget/navigation.dart'; +import 'package:tasks/components/bottom_sheet_pages/ward_overview_bottom_sheet.dart'; import 'package:tasks/screens/settings_screen.dart'; class WardsBottomSheetPage extends StatelessWidget { @@ -21,7 +23,7 @@ class WardsBottomSheetPage extends StatelessWidget { context, title: LoadingFutureBuilder( data: OrganizationService().getOrganization(id: organizationId), - thenWidgetBuilder: (context, data) => Column( + thenBuilder: (context, data) => Column( mainAxisSize: MainAxisSize.min, children: [ Text( @@ -39,7 +41,7 @@ class WardsBottomSheetPage extends StatelessWidget { child: Flexible( child: LoadingFutureBuilder( data: WardService().getWards(organizationId: organizationId), - thenWidgetBuilder: (context, data) => ListView( + thenBuilder: (context, data) => ListView( children: [ Flexible( child: ListView( @@ -53,7 +55,8 @@ class WardsBottomSheetPage extends StatelessWidget { icon: Icons.house_rounded, title: ward.name, onTap: () { - // TODO navigate to ward view + NavigationStackController.of(context) + .push(WardOverviewBottomSheetPage(wardId: ward.id)); }, ), ) diff --git a/apps/tasks/lib/components/user_header.dart b/apps/tasks/lib/components/user_header.dart index ecd98ff3..f28001a0 100644 --- a/apps/tasks/lib/components/user_header.dart +++ b/apps/tasks/lib/components/user_header.dart @@ -32,7 +32,7 @@ class UserHeader extends StatelessWidget implements PreferredSizeWidget { height: iconSizeSmall, child: LoadingFutureBuilder( data: UserService().getSelf(), - thenWidgetBuilder: (context, data) { + thenBuilder: (context, data) { return CircleAvatar( backgroundColor: Colors.transparent, radius: iconSizeSmall / 2, diff --git a/apps/tasks/lib/config/config.dart b/apps/tasks/lib/config/config.dart index a0f1e628..7bf0bc48 100644 --- a/apps/tasks/lib/config/config.dart +++ b/apps/tasks/lib/config/config.dart @@ -4,7 +4,9 @@ const minimumPasswordCharacters = 6; /// Whether the development mode should be enabled /// /// Shortens the login -const bool devMode = true; +const bool isUsingDevModeLogin = true; + +const bool isUsingOfflineClients = false; /// The API for testing const String stagingAPIURL = "staging.api.helpwave.de"; @@ -13,4 +15,4 @@ const String stagingAPIURL = "staging.api.helpwave.de"; const String productionAPIURL = "api.helpwave.de"; /// The API to be used -const String usedAPIURL = devMode ? stagingAPIURL : productionAPIURL; +const String usedAPIURL = isUsingDevModeLogin ? stagingAPIURL : productionAPIURL; diff --git a/apps/tasks/lib/main.dart b/apps/tasks/lib/main.dart index d88d62a2..2914d8dd 100644 --- a/apps/tasks/lib/main.dart +++ b/apps/tasks/lib/main.dart @@ -12,14 +12,14 @@ import 'package:tasks/screens/login_screen.dart'; void main() { WidgetsFlutterBinding.ensureInitialized(); - CurrentWardService().devMode = true; + CurrentWardService().devMode = isUsingOfflineClients; TasksAPIServiceClients() ..apiUrl = usedAPIURL - ..offlineMode = true; + ..offlineMode = isUsingOfflineClients; UserAPIServiceClients() ..apiUrl = usedAPIURL - ..offlineMode = true; - UserSessionService().changeMode(devMode); + ..offlineMode = isUsingOfflineClients; + UserSessionService().changeMode(isUsingDevModeLogin); runApp(const MyApp()); } @@ -37,7 +37,7 @@ class MyApp extends StatelessWidget { create: (_) => LanguageModel(), ), ChangeNotifierProvider( - create: (_) => CurrentWardController(devMode: devMode), + create: (_) => CurrentWardController(devMode: isUsingDevModeLogin), ), ChangeNotifierProvider( create: (_) => UserSessionController(), diff --git a/apps/tasks/lib/screens/settings_screen.dart b/apps/tasks/lib/screens/settings_screen.dart index 87cc7951..653bfeb4 100644 --- a/apps/tasks/lib/screens/settings_screen.dart +++ b/apps/tasks/lib/screens/settings_screen.dart @@ -242,14 +242,13 @@ class SettingsBottomSheetPage extends StatelessWidget { titleBuilder(context.localization!.myOrganizations), LoadingFutureBuilder( data: OrganizationService().getOrganizationsForUser(), - thenWidgetBuilder: (context, data) { + thenBuilder: (context, data) { return RoundedListTiles( children: data .map((organization) => NavigationListTile( icon: Icons.apartment_rounded, title: organization.longName, onTap: () { - print("tap"); NavigationStackController.of(context) .push(OrganizationBottomSheetPage(organizationId: organization.id)); }, diff --git a/apps/tasks/lib/screens/ward_select_screen.dart b/apps/tasks/lib/screens/ward_select_screen.dart index 41c06071..cd02f9c0 100644 --- a/apps/tasks/lib/screens/ward_select_screen.dart +++ b/apps/tasks/lib/screens/ward_select_screen.dart @@ -22,6 +22,7 @@ class _WardSelectScreen extends State { @override Widget build(BuildContext context) { + return Scaffold( appBar: AppBar( title: Text(context.localization!.selectWard), @@ -75,14 +76,14 @@ class _WardSelectScreen extends State { trailing: const Icon(Icons.arrow_forward), onTap: organization == null ? null - : () => Navigator.push( + : () => Navigator.push( context, MaterialPageRoute( - builder: (context) => ListSearch( + builder: (context) => ListSearch( title: context.localization!.ward, asyncItems: (_) async => - await WardService().getWardOverviews(organizationId: organization!.id), - elementToString: (WardOverview ward) => ward.name, + await WardService().getWards(organizationId: organization!.id), + elementToString: (WardMinimal ward) => ward.name, ), ), ).then((value) { diff --git a/apps/tasks/pubspec.lock b/apps/tasks/pubspec.lock index bbc15e5d..a7f649ca 100644 --- a/apps/tasks/pubspec.lock +++ b/apps/tasks/pubspec.lock @@ -272,10 +272,10 @@ packages: dependency: transitive description: name: helpwave_proto_dart - sha256: "4d4b2b6d0129c7c1b678164688e2bd0a99029cc178794fd655e7c4e7aa505d58" + sha256: "5d68c5f9552857ca2d6b7cc3dc2b15275f796ef3cd5fbc5744caacade3bdfb1e" url: "https://pub.dev" source: hosted - version: "0.46.0-336429a" + version: "0.50.0-de87640" helpwave_service: dependency: "direct main" description: diff --git a/packages/helpwave_localization/lib/l10n/app_de.arb b/packages/helpwave_localization/lib/l10n/app_de.arb index 27199d1e..8b70d57a 100644 --- a/packages/helpwave_localization/lib/l10n/app_de.arb +++ b/packages/helpwave_localization/lib/l10n/app_de.arb @@ -187,5 +187,9 @@ "organizationDangerZoneDescription": "Vorsicht, das löschen einer Organization ist permanent und kann nicht rückgängig gemacht werden!", "invitations": "Einladungen", "revoke": "Zurückziehen", - "nothingYet": "Bis jetzt noch nichts" + "nothingYet": "Bis jetzt noch nichts", + "add": "Hinzufügen", + "new_": "Neu", + "newRoom": "Neuer Raum", + "newBed": "Neues Bett" } diff --git a/packages/helpwave_localization/lib/l10n/app_en.arb b/packages/helpwave_localization/lib/l10n/app_en.arb index 51b7e6a4..840bb5ad 100644 --- a/packages/helpwave_localization/lib/l10n/app_en.arb +++ b/packages/helpwave_localization/lib/l10n/app_en.arb @@ -187,5 +187,9 @@ "organizationDangerZoneDescription": "Deleting the organization is a permanent action and cannot be undone be careful!", "invitations": "Invitations", "revoke": "Revoke", - "nothingYet": "Nothing yet" + "nothingYet": "Nothing yet", + "add": "Add", + "new_": "New", + "newRoom": "New Room", + "newBed": "New Bed" } diff --git a/packages/helpwave_service/lib/src/api/offline/offline_client_store.dart b/packages/helpwave_service/lib/src/api/offline/offline_client_store.dart index 9f61072c..cfddfc0e 100644 --- a/packages/helpwave_service/lib/src/api/offline/offline_client_store.dart +++ b/packages/helpwave_service/lib/src/api/offline/offline_client_store.dart @@ -70,9 +70,9 @@ final List initialRooms = initialWards .map((index) => RoomWithWardId(id: "${ward.id}${index + 1}", name: "Room ${index + 1}", wardId: ward.id))) .expand((element) => element) .toList(); -final List initialBeds = initialRooms +final List initialBeds = initialRooms .map((room) => range(0, 4) - .map((index) => BedWithRoomId(id: "${room.id}${index + 1}", name: "Bed ${index + 1}", roomId: room.id))) + .map((index) => Bed(id: "${room.id}${index + 1}", name: "Bed ${index + 1}", roomId: room.id))) .expand((element) => element) .toList(); final List initialPatients = initialBeds.indexed diff --git a/packages/helpwave_service/lib/src/api/tasks/controllers/beds_controller.dart b/packages/helpwave_service/lib/src/api/tasks/controllers/beds_controller.dart new file mode 100644 index 00000000..d3f1454c --- /dev/null +++ b/packages/helpwave_service/lib/src/api/tasks/controllers/beds_controller.dart @@ -0,0 +1,107 @@ +import 'dart:async'; +import 'package:helpwave_service/src/api/tasks/index.dart'; +import 'package:helpwave_util/loading.dart'; + +/// The Controller for managing [Bed]s in a [Room] +/// +/// Providing a [roomId] means loading and synchronising the [Bed]s with +/// the backend while no [roomId] or a empty [String] means that the [Bed]s are +/// only used locally +class BedsController extends LoadingChangeNotifier { + /// The [Bed]s + List _beds = []; + + List get beds => [..._beds]; + + set beds(List value) { + _beds = value; + notifyListeners(); + } + + bool get isCreating => roomId == null || roomId!.isEmpty; + + String? roomId; + + BedsController({this.roomId = "", List? beds}) { + if (!isCreating) { + load(); + } + } + + /// Loads the [Bed]s + Future load() async { + if (isCreating) { + return; + } + loadBeds() async { + beds = await BedService().getBedsByRoom(roomId: roomId!); + } + + loadHandler(future: loadBeds()); + } + + /// Delete the [Bed] by its index in the list + Future deleteByIndex(int index) async { + assert (index < 0 || index >= beds.length); + if (isCreating) { + _beds.removeAt(index); + notifyListeners(); + return; + } + await delete(beds[index].id); + } + + /// Delete the [Bed] by the id + Future delete(String id) async { + assert(!isCreating, "deleteById should not be used when creating a completely new Subtask list"); + deleteOp() async { + await BedService().delete(id: id).then((value) { + if (value) { + int index = _beds.indexWhere((element) => element.id == id); + if (index != -1) { + _beds.removeAt(index); + } + } + }); + } + + loadHandler(future: deleteOp()); + } + + /// Add the [Bed] + Future create(Bed bed) async { + if (isCreating) { + _beds.add(bed); + notifyListeners(); + return; + } + createSubtask() async { + await BedService().create(roomId: roomId!, name: bed.name).then((value) { + _beds.add(bed); + }); + } + + loadHandler(future: createSubtask()); + } + + Future update({required Bed bed, int? index}) async { + if (isCreating) { + assert( + index != null && index >= 0 && index < beds.length, + "When creating a bed list and updating a bed, a index for the bed must be provided", + ); + beds[index!] = bed; + return; + } + updateOp() async { + assert(!bed.isCreating, "To update a bed on the server the bed must have an id"); + await BedService().update(id: bed.id, name: bed.name); + int index = beds.indexWhere((element) => element.id == bed.id); + if (index != -1) { + beds[index] = bed; + } + } + + loadHandler(future: updateOp()); + } +} diff --git a/packages/helpwave_service/lib/src/api/tasks/controllers/index.dart b/packages/helpwave_service/lib/src/api/tasks/controllers/index.dart index 2940b0c6..be3fb9f9 100644 --- a/packages/helpwave_service/lib/src/api/tasks/controllers/index.dart +++ b/packages/helpwave_service/lib/src/api/tasks/controllers/index.dart @@ -3,3 +3,5 @@ export 'subtask_list_controller.dart'; export 'task_controller.dart'; export 'my_tasks_controller.dart'; export 'ward_patients_controller.dart'; +export 'rooms_controller.dart'; +export 'beds_controller.dart'; diff --git a/packages/helpwave_service/lib/src/api/tasks/controllers/rooms_controller.dart b/packages/helpwave_service/lib/src/api/tasks/controllers/rooms_controller.dart new file mode 100644 index 00000000..17c63ff0 --- /dev/null +++ b/packages/helpwave_service/lib/src/api/tasks/controllers/rooms_controller.dart @@ -0,0 +1,107 @@ +import 'dart:async'; +import 'package:helpwave_service/src/api/tasks/index.dart'; +import 'package:helpwave_util/loading.dart'; + +/// The Controller for managing [Room]s in a [Ward] +/// +/// Providing a [wardId] means loading and synchronising the [Room]s with +/// the backend while no [wardId] or a empty [String] means that the [Room]s are +/// only used locally +class RoomsController extends LoadingChangeNotifier { + /// The [Room]s + List _rooms = []; + + List get rooms => [..._rooms]; + + set rooms(List value) { + _rooms = value; + notifyListeners(); + } + + bool get isCreating => wardId == null || wardId!.isEmpty; + + String? wardId; + + RoomsController({this.wardId = "", List? rooms}) { + if (!isCreating) { + load(); + } + } + + /// Loads the [Room]s + Future load() async { + if (isCreating) { + return; + } + loadOp() async { + rooms = await RoomService().getRooms(wardId: wardId!); + } + + loadHandler(future: loadOp()); + } + + /// Delete the [Room] by its index in the list + Future deleteByIndex(int index) async { + assert(index < 0 || index >= rooms.length); + if (isCreating) { + _rooms.removeAt(index); + notifyListeners(); + return; + } + await delete(rooms[index].id); + } + + /// Delete the [Room] by the id + Future delete(String id) async { + assert(!isCreating, "deleteById should not be used when creating a completely new Subtask list"); + deleteOp() async { + await RoomService().delete(id: id).then((value) { + if (value) { + int index = _rooms.indexWhere((element) => element.id == id); + if (index != -1) { + _rooms.removeAt(index); + } + } + }); + } + + loadHandler(future: deleteOp()); + } + + /// Add the [Room] + Future create(RoomWithBedWithMinimalPatient room) async { + if (isCreating) { + _rooms.add(room); + notifyListeners(); + return; + } + createOp() async { + await RoomService().createRoom(wardId: wardId!, name: room.name).then((value) { + _rooms.add(room); + }); + } + + loadHandler(future: createOp()); + } + + Future update({required RoomWithBedWithMinimalPatient room, int? index}) async { + if (isCreating) { + assert( + index != null && index >= 0 && index < rooms.length, + "When creating a room list and updating a room, a index for the room must be provided", + ); + rooms[index!] = room; + return; + } + updateOp() async { + assert(!room.isCreating, "To update a room on the server the room must have an id"); + await RoomService().update(id: room.id, name: room.name); + int index = rooms.indexWhere((element) => element.id == room.id); + if (index != -1) { + rooms[index] = room; + } + } + + loadHandler(future: updateOp()); + } +} diff --git a/packages/helpwave_service/lib/src/api/tasks/data_types/bed.dart b/packages/helpwave_service/lib/src/api/tasks/data_types/bed.dart index 2bf7f84e..d89de03d 100644 --- a/packages/helpwave_service/lib/src/api/tasks/data_types/bed.dart +++ b/packages/helpwave_service/lib/src/api/tasks/data_types/bed.dart @@ -1,6 +1,6 @@ import 'package:helpwave_service/src/api/tasks/index.dart'; -/// data class for [Bed] +/// Data class for a [Bed] class BedMinimal { String id; String name; @@ -9,20 +9,18 @@ class BedMinimal { required this.id, required this.name, }); -} - -class BedWithRoomId extends BedMinimal { - String roomId; - BedWithRoomId({required super.id, required super.name, required this.roomId}); + bool get isCreating => id == ""; } -class BedWithMinimalPatient extends BedMinimal{ +class Bed extends BedMinimal { PatientMinimal? patient; + String roomId; - BedWithMinimalPatient({ + Bed({ required super.id, required super.name, - required this.patient, + required this.roomId, + this.patient, }); } diff --git a/packages/helpwave_service/lib/src/api/tasks/data_types/room.dart b/packages/helpwave_service/lib/src/api/tasks/data_types/room.dart index 8e943ea0..0385aac0 100644 --- a/packages/helpwave_service/lib/src/api/tasks/data_types/room.dart +++ b/packages/helpwave_service/lib/src/api/tasks/data_types/room.dart @@ -9,6 +9,8 @@ class RoomMinimal { required this.id, required this.name, }); + + bool get isCreating => id == ""; } class RoomWithWardId extends RoomMinimal { @@ -60,7 +62,7 @@ class RoomWithBedFlat { } class RoomWithBedWithMinimalPatient extends RoomMinimal { - List beds; + List beds; RoomWithBedWithMinimalPatient({ required super.id, diff --git a/packages/helpwave_service/lib/src/api/tasks/offline_clients/bed_offline_client.dart b/packages/helpwave_service/lib/src/api/tasks/offline_clients/bed_offline_client.dart index cadcb891..3dc48e55 100644 --- a/packages/helpwave_service/lib/src/api/tasks/offline_clients/bed_offline_client.dart +++ b/packages/helpwave_service/lib/src/api/tasks/offline_clients/bed_offline_client.dart @@ -13,9 +13,9 @@ class BedUpdate { } class BedOfflineService { - List beds = []; + List beds = []; - BedWithRoomId? findBed(String id) { + Bed? findBed(String id) { int index = OfflineClientStore().bedStore.beds.indexWhere((value) => value.id == id); if (index == -1) { return null; @@ -23,7 +23,7 @@ class BedOfflineService { return beds[index]; } - List findBeds([String? roomId]) { + List findBeds([String? roomId]) { final valueStore = OfflineClientStore().bedStore; if (roomId == null) { return valueStore.beds; @@ -31,7 +31,7 @@ class BedOfflineService { return valueStore.beds.where((value) => value.roomId == roomId).toList(); } - void create(BedWithRoomId bed) { + void create(Bed bed) { OfflineClientStore().bedStore.beds.add(bed); } @@ -42,7 +42,7 @@ class BedOfflineService { valueStore.beds = valueStore.beds.map((value) { if (value.id == bed.id) { found = true; - return BedWithRoomId(id: bed.id, name: bed.name ?? value.name, roomId: value.roomId); + return Bed(id: bed.id, name: bed.name ?? value.name, roomId: value.roomId); } return value; }).toList(); @@ -62,8 +62,8 @@ class BedOfflineService { } } -class BedServicePromiseClient extends BedServiceClient { - BedServicePromiseClient(super.channel); +class BedOfflineClient extends BedServiceClient { + BedOfflineClient(super.channel); @override ResponseFuture getBed(GetBedRequest request, {CallOptions? options}) { @@ -125,7 +125,7 @@ class BedServicePromiseClient extends BedServiceClient { @override ResponseFuture createBed(CreateBedRequest request, {CallOptions? options}) { - final newBed = BedWithRoomId( + final newBed = Bed( id: DateTime.now().millisecondsSinceEpoch.toString(), name: request.name, roomId: request.roomId, @@ -141,7 +141,7 @@ class BedServicePromiseClient extends BedServiceClient { @override ResponseFuture bulkCreateBeds(BulkCreateBedsRequest request, {CallOptions? options}) { final beds = range(0, request.amountOfBeds) - .map((index) => BedWithRoomId( + .map((index) => Bed( id: DateTime.now().millisecondsSinceEpoch.toString(), name: "New Bed ${index + 1}", roomId: request.roomId, diff --git a/packages/helpwave_service/lib/src/api/tasks/offline_clients/patient_offline_client.dart b/packages/helpwave_service/lib/src/api/tasks/offline_clients/patient_offline_client.dart index af78180b..1783ab40 100644 --- a/packages/helpwave_service/lib/src/api/tasks/offline_clients/patient_offline_client.dart +++ b/packages/helpwave_service/lib/src/api/tasks/offline_clients/patient_offline_client.dart @@ -129,7 +129,6 @@ class PatientOfflineClient extends PatientServiceClient { if (room == null) { return MockResponseFuture.value(response); } - response.bedId = patient.bedId!; response.bed = GetPatientResponse_Bed(id: bed.id, name: bed.name); response.room = GetPatientResponse_Room(id: room.id, name: room.name); return MockResponseFuture.value(response); diff --git a/packages/helpwave_service/lib/src/api/tasks/services/bed_svc.dart b/packages/helpwave_service/lib/src/api/tasks/services/bed_svc.dart index e69de29b..3a4913d7 100644 --- a/packages/helpwave_service/lib/src/api/tasks/services/bed_svc.dart +++ b/packages/helpwave_service/lib/src/api/tasks/services/bed_svc.dart @@ -0,0 +1,110 @@ +import 'package:grpc/grpc.dart'; +import 'package:helpwave_proto_dart/services/tasks_svc/v1/bed_svc.pbgrpc.dart'; +import 'package:helpwave_service/src/api/tasks/data_types/index.dart'; +import 'package:helpwave_service/src/api/tasks/tasks_api_service_clients.dart'; + +/// The GRPC Service for [Bed]s +/// +/// Provides queries and requests that load or alter [Bed] objects on the server +/// The server is defined in the underlying [TasksAPIServiceClients] +class BedService { + /// The GRPC ServiceClient which handles GRPC + BedServiceClient bedService = TasksAPIServiceClients().bedServiceClient; + + Future getBed({required String id}) async { + GetBedRequest request = GetBedRequest(id: id); + GetBedResponse response = await bedService.getBed( + request, + options: CallOptions(metadata: TasksAPIServiceClients().getMetaData()), + ); + + return Bed( + id: response.id, + name: response.name, + roomId: response.roomId, + ); + } + + Future> getBeds() async { + GetBedsRequest request = GetBedsRequest(); + GetBedsResponse response = await bedService.getBeds( + request, + options: CallOptions(metadata: TasksAPIServiceClients().getMetaData()), + ); + + List beds = response.beds + .map((bed) => Bed( + id: bed.id, + name: bed.name, + roomId: bed.roomId, + )) + .toList(); + + return beds; + } + + Future> getBedsByRoom({required String roomId}) async { + GetBedsByRoomRequest request = GetBedsByRoomRequest(roomId: roomId); + GetBedsByRoomResponse response = await bedService.getBedsByRoom( + request, + options: CallOptions(metadata: TasksAPIServiceClients().getMetaData()), + ); + + List beds = response.beds + .map((bed) => Bed( + id: bed.id, + name: bed.name, + roomId: roomId, + )) + .toList(); + + return beds; + } + + Future getBedAndRoomByPatient({required String patientId}) async { + GetBedByPatientRequest request = GetBedByPatientRequest(patientId: patientId); + GetBedByPatientResponse response = await bedService.getBedByPatient( + request, + options: CallOptions(metadata: TasksAPIServiceClients().getMetaData()), + ); + + return RoomWithBedFlat( + bed: Bed( + id: response.bed.id, + name: response.bed.name, + roomId: response.room.id, + ), + room: RoomWithWardId(id: response.room.id, name: response.room.name, wardId: response.room.wardId), + ); + } + + Future create({required String roomId, required String name}) async { + CreateBedRequest request = CreateBedRequest(roomId: roomId, name: name); + CreateBedResponse response = await bedService.createBed( + request, + options: CallOptions(metadata: TasksAPIServiceClients().getMetaData()), + ); + + return Bed(id: response.id, name: name, roomId: roomId); + } + + Future update({ + required String id, + String? name, + }) async { + UpdateBedRequest request = UpdateBedRequest(id: id, name: name); + await bedService.updateBed( + request, + options: CallOptions(metadata: TasksAPIServiceClients().getMetaData()), + ); + } + + Future delete({required String id}) async { + DeleteBedRequest request = DeleteBedRequest(id: id); + await bedService.deleteBed( + request, + options: CallOptions(metadata: TasksAPIServiceClients().getMetaData()), + ); + return true; + } +} diff --git a/packages/helpwave_service/lib/src/api/tasks/services/patient_svc.dart b/packages/helpwave_service/lib/src/api/tasks/services/patient_svc.dart index 06c3ba6d..fe58f0e9 100644 --- a/packages/helpwave_service/lib/src/api/tasks/services/patient_svc.dart +++ b/packages/helpwave_service/lib/src/api/tasks/services/patient_svc.dart @@ -136,9 +136,10 @@ class PatientService { name: room.id, beds: beds.map((bed) { var patient = bed.patient; - return BedWithMinimalPatient( + return Bed( id: bed.id, name: bed.name, + roomId: room.id, patient: patient.isInitialized() ? PatientMinimal(id: patient.id, name: patient.name) : null, ); }).toList()); diff --git a/packages/helpwave_service/lib/src/api/tasks/services/room_svc.dart b/packages/helpwave_service/lib/src/api/tasks/services/room_svc.dart index 59ae4381..785d159b 100644 --- a/packages/helpwave_service/lib/src/api/tasks/services/room_svc.dart +++ b/packages/helpwave_service/lib/src/api/tasks/services/room_svc.dart @@ -11,6 +11,40 @@ class RoomService { /// The GRPC ServiceClient which handles GRPC RoomServiceClient roomService = TasksAPIServiceClients().roomServiceClient; + Future get({required String roomId}) async { + GetRoomRequest request = GetRoomRequest(id: roomId); + GetRoomResponse response = await roomService.getRoom( + request, + options: CallOptions(metadata: TasksAPIServiceClients().getMetaData()), + ); + + RoomWithBedWithMinimalPatient rooms = RoomWithBedWithMinimalPatient( + id: response.id, + name: response.name, + beds: response.beds.map((bed) => Bed(id: bed.id, name: bed.name, roomId: roomId)).toList(), + ); + + return rooms; + } + + Future> getRooms({required String wardId}) async { + GetRoomsRequest request = GetRoomsRequest(wardId: wardId); + GetRoomsResponse response = await roomService.getRooms( + request, + options: CallOptions(metadata: TasksAPIServiceClients().getMetaData()), + ); + + List rooms = response.rooms + .map((room) => RoomWithBedWithMinimalPatient( + id: room.id, + name: room.name, + beds: room.beds.map((bed) => Bed(id: bed.id, name: bed.name, roomId: room.id)).toList(), + )) + .toList(); + + return rooms; + } + Future> getRoomOverviews({required String wardId}) async { GetRoomOverviewsByWardRequest request = GetRoomOverviewsByWardRequest(id: wardId); GetRoomOverviewsByWardResponse response = await roomService.getRoomOverviewsByWard( @@ -23,9 +57,10 @@ class RoomService { id: room.id, name: room.name, beds: room.beds - .map((bed) => BedWithMinimalPatient( + .map((bed) => Bed( id: bed.id, name: bed.name, + roomId: room.id, patient: bed.hasPatient() // TODO bed.patient possibly has more information ? PatientMinimal( @@ -40,4 +75,36 @@ class RoomService { return rooms; } + + Future createRoom({required String wardId, required String name}) async { + CreateRoomRequest request = CreateRoomRequest(wardId: wardId, name: name); + CreateRoomResponse response = await roomService.createRoom( + request, + options: CallOptions(metadata: TasksAPIServiceClients().getMetaData()), + ); + + return RoomMinimal(id: response.id, name: name); + } + + Future update({ + required String id, + String? name, + }) async { + UpdateRoomRequest request = UpdateRoomRequest(id: id, name: name); + await roomService.updateRoom( + request, + options: CallOptions(metadata: TasksAPIServiceClients().getMetaData()), + ); + } + + Future delete({ + required String id, + }) async { + DeleteRoomRequest request = DeleteRoomRequest(id: id); + await roomService.deleteRoom( + request, + options: CallOptions(metadata: TasksAPIServiceClients().getMetaData()), + ); + return true; + } } diff --git a/packages/helpwave_service/lib/src/api/tasks/tasks_api_service_clients.dart b/packages/helpwave_service/lib/src/api/tasks/tasks_api_service_clients.dart index b2e06333..331e2df9 100644 --- a/packages/helpwave_service/lib/src/api/tasks/tasks_api_service_clients.dart +++ b/packages/helpwave_service/lib/src/api/tasks/tasks_api_service_clients.dart @@ -1,4 +1,5 @@ import 'package:grpc/grpc.dart'; +import 'package:helpwave_proto_dart/services/tasks_svc/v1/bed_svc.pbgrpc.dart'; import 'package:helpwave_proto_dart/services/tasks_svc/v1/ward_svc.pbgrpc.dart'; import 'package:helpwave_proto_dart/services/tasks_svc/v1/patient_svc.pbgrpc.dart'; import 'package:helpwave_proto_dart/services/tasks_svc/v1/room_svc.pbgrpc.dart'; @@ -7,6 +8,7 @@ import 'package:helpwave_service/src/api/tasks/offline_clients/patient_offline_c import 'package:helpwave_service/src/api/tasks/offline_clients/ward_offline_client.dart'; import 'package:helpwave_service/src/auth/index.dart'; +import 'offline_clients/bed_offline_client.dart'; import 'offline_clients/room_offline_client.dart'; import 'offline_clients/task_offline_client.dart'; @@ -31,7 +33,7 @@ class TasksAPIServiceClients { Map getMetaData({String? organizationId}) { var metaData = { ...AuthenticationUtility.authMetaData, - "dapr-app-id": "task-svc", + "dapr-app-id": "tasks-svc", }; if (organizationId != null) { @@ -52,6 +54,9 @@ class TasksAPIServiceClients { RoomServiceClient get roomServiceClient => offlineMode ? RoomOfflineClient(serviceChannel) : RoomServiceClient(serviceChannel); + BedServiceClient get bedServiceClient => + offlineMode ? BedOfflineClient(serviceChannel) : BedServiceClient(serviceChannel); + TaskServiceClient get taskServiceClient => offlineMode ? TaskOfflineClient(serviceChannel) : TaskServiceClient(serviceChannel); } diff --git a/packages/helpwave_service/pubspec.yaml b/packages/helpwave_service/pubspec.yaml index cce05101..71bcb407 100644 --- a/packages/helpwave_service/pubspec.yaml +++ b/packages/helpwave_service/pubspec.yaml @@ -17,7 +17,7 @@ dependencies: flutter_secure_storage: 9.0.0 jose: ^0.3.4 logger: ^2.0.2+1 - helpwave_proto_dart: ^0.46.0-336429a + helpwave_proto_dart: ^0.50.0-de87640 grpc: ^3.2.4 helpwave_util: path: "../helpwave_util" diff --git a/packages/helpwave_widget/lib/src/content_selection/list_select.dart b/packages/helpwave_widget/lib/src/content_selection/list_select.dart index 1f7e9383..c41ba885 100644 --- a/packages/helpwave_widget/lib/src/content_selection/list_select.dart +++ b/packages/helpwave_widget/lib/src/content_selection/list_select.dart @@ -20,7 +20,7 @@ class ListSelect extends StatelessWidget { Widget build(BuildContext context) { return LoadingFutureBuilder( data: items, - thenWidgetBuilder: (context, data) => Column( + thenBuilder: (context, data) => Column( children: data.map((item) => builder(context, item, () => onSelect(item))).toList(), ), ); diff --git a/packages/helpwave_widget/lib/src/loading/loading_future_builder.dart b/packages/helpwave_widget/lib/src/loading/loading_future_builder.dart index fb7439c0..4c4a4bec 100644 --- a/packages/helpwave_widget/lib/src/loading/loading_future_builder.dart +++ b/packages/helpwave_widget/lib/src/loading/loading_future_builder.dart @@ -10,7 +10,7 @@ class LoadingFutureBuilder extends StatelessWidget { final FutureOr data; /// The Builder for the [Widget] upon an successful [FutureOr] - final Widget Function(BuildContext context, T data) thenWidgetBuilder; + final Widget Function(BuildContext context, T data) thenBuilder; /// The Builder for the [Widget] when loading the [FutureOr] final Widget loadingWidget; @@ -21,7 +21,7 @@ class LoadingFutureBuilder extends StatelessWidget { const LoadingFutureBuilder({ super.key, required this.data, - required this.thenWidgetBuilder, + required this.thenBuilder, this.loadingWidget = const LoadingSpinner(), this.errorWidget = const LoadErrorWidget(), }); @@ -44,11 +44,11 @@ class LoadingFutureBuilder extends StatelessWidget { errorWidget: Center(child: errorWidget), loadingWidget: Center(child: loadingWidget), // Safety check because typecast may fail otherwise - child: snapshot.data != null ? thenWidgetBuilder(context, snapshot.data as T) : const SizedBox(), + child: snapshot.data != null ? thenBuilder(context, snapshot.data as T) : const SizedBox(), ); }, ); } - return thenWidgetBuilder(context, data as T); + return thenBuilder(context, data as T); } } From bee92a2a8ff12c5333e415a3c12e290fc74973c4 Mon Sep 17 00:00:00 2001 From: Felix Thape Date: Thu, 3 Oct 2024 00:51:53 +0200 Subject: [PATCH 14/36] feat: update room overview bottomsheet --- .../organization_bottom_sheet.dart | 134 +++++++++--------- .../patient_bottom_sheet.dart | 2 +- .../room_overview_bottom_sheet.dart | 56 ++++++-- .../bottom_sheet_pages/task_bottom_sheet.dart | 2 +- .../bottom_sheet_pages/user_bottom_sheet.dart | 2 +- .../ward_overview_bottom_sheet.dart | 2 +- .../ward_select_bottom_sheet.dart | 2 +- .../wards_bottom_sheet_page.dart | 40 +++--- apps/tasks/lib/components/user_header.dart | 2 +- apps/tasks/lib/screens/settings_screen.dart | 3 +- .../lib/src/api/tasks/controllers/index.dart | 1 + .../tasks/controllers/room_controller.dart | 89 ++++++++++++ .../lib/src/theme/light_theme.dart | 2 +- .../helpwave_theme/lib/src/theme/theme.dart | 90 ++++++------ .../src/content_selection/list_select.dart | 2 +- .../lib/src/lists/rounded_list_tiles.dart | 16 ++- .../src/loading/loading_future_builder.dart | 60 ++++---- .../lib/src/loading/pulsing_container.dart | 45 +++--- 18 files changed, 338 insertions(+), 212 deletions(-) create mode 100644 packages/helpwave_service/lib/src/api/tasks/controllers/room_controller.dart diff --git a/apps/tasks/lib/components/bottom_sheet_pages/organization_bottom_sheet.dart b/apps/tasks/lib/components/bottom_sheet_pages/organization_bottom_sheet.dart index 43b29345..ba7e07e6 100644 --- a/apps/tasks/lib/components/bottom_sheet_pages/organization_bottom_sheet.dart +++ b/apps/tasks/lib/components/bottom_sheet_pages/organization_bottom_sheet.dart @@ -39,76 +39,72 @@ class OrganizationBottomSheetPage extends StatelessWidget { child: LoadingAndErrorWidget( state: controller.state, child: ListView( + shrinkWrap: true, children: [ - Flexible( - child: ListView( - shrinkWrap: true, - children: [ - const SizedBox(height: distanceMedium), - Text(context.localization!.shortName, style: context.theme.textTheme.titleMedium), - const SizedBox(height: distanceTiny), - TextFormFieldWithTimer( - initialValue: controller.organization.shortName, - onUpdate: (value) => controller.update(shortName: value), - ), - const SizedBox(height: distanceMedium), - Text(context.localization!.longName, style: context.theme.textTheme.titleMedium), - const SizedBox(height: distanceTiny), - TextFormFieldWithTimer( - initialValue: controller.organization.longName, - onUpdate: (value) => controller.update(longName: value), - ), - const SizedBox(height: distanceMedium), - Text(context.localization!.contactEmail, style: context.theme.textTheme.titleMedium), - const SizedBox(height: distanceTiny), - TextFormFieldWithTimer( - initialValue: controller.organization.email, - // TODO validation - onUpdate: (value) => controller.update(email: value), - ), - const SizedBox(height: distanceMedium), - Text(context.localization!.settings, style: context.theme.textTheme.titleMedium), - const SizedBox(height: distanceTiny), - RoundedListTiles( - children: [ - NavigationListTile( - icon: Icons.house_rounded, - title: context.localization!.wards, - onTap: () { - NavigationStackController.of(context).push(WardsBottomSheetPage(organizationId: organizationId)); - }, - ), - NavigationListTile( - icon: Icons.person, - title: context.localization!.members, - onTap: () { - NavigationStackController.of(context).push(OrganizationMembersBottomSheetPage(organizationId: organizationId)); - }, - ), - NavigationListTile( - icon: Icons.label, - title: context.localization!.properties, - onTap: () { - // TODO navigate to properties page - }, - ) - ], - ), - const SizedBox(height: distanceMedium), - Text(context.localization!.dangerZone, style: context.theme.textTheme.titleMedium), - Text( - context.localization!.organizationDangerZoneDescription, - style: TextStyle(color: context.theme.hintColor), - ), - PressableText( - text: "${context.localization!.delete} ${context.localization!.organization}", - style: const TextStyle(color: Colors.red, fontWeight: FontWeight.w700), // TODO get from theme - onPressed: () { - // TODO show modal and delete organization - }, - ), - ], - ), + const SizedBox(height: distanceMedium), + Text(context.localization!.shortName, style: context.theme.textTheme.titleMedium), + const SizedBox(height: distanceTiny), + TextFormFieldWithTimer( + initialValue: controller.organization.shortName, + onUpdate: (value) => controller.update(shortName: value), + ), + const SizedBox(height: distanceMedium), + Text(context.localization!.longName, style: context.theme.textTheme.titleMedium), + const SizedBox(height: distanceTiny), + TextFormFieldWithTimer( + initialValue: controller.organization.longName, + onUpdate: (value) => controller.update(longName: value), + ), + const SizedBox(height: distanceMedium), + Text(context.localization!.contactEmail, style: context.theme.textTheme.titleMedium), + const SizedBox(height: distanceTiny), + TextFormFieldWithTimer( + initialValue: controller.organization.email, + // TODO validation + onUpdate: (value) => controller.update(email: value), + ), + const SizedBox(height: distanceMedium), + Text(context.localization!.settings, style: context.theme.textTheme.titleMedium), + const SizedBox(height: distanceTiny), + RoundedListTiles( + children: [ + NavigationListTile( + icon: Icons.house_rounded, + title: context.localization!.wards, + onTap: () { + NavigationStackController.of(context) + .push(WardsBottomSheetPage(organizationId: organizationId)); + }, + ), + NavigationListTile( + icon: Icons.person, + title: context.localization!.members, + onTap: () { + NavigationStackController.of(context) + .push(OrganizationMembersBottomSheetPage(organizationId: organizationId)); + }, + ), + NavigationListTile( + icon: Icons.label, + title: context.localization!.properties, + onTap: () { + // TODO navigate to properties page + }, + ) + ], + ), + const SizedBox(height: distanceMedium), + Text(context.localization!.dangerZone, style: context.theme.textTheme.titleMedium), + Text( + context.localization!.organizationDangerZoneDescription, + style: TextStyle(color: context.theme.hintColor), + ), + PressableText( + text: "${context.localization!.delete} ${context.localization!.organization}", + style: const TextStyle(color: Colors.red, fontWeight: FontWeight.w700), // TODO get from theme + onPressed: () { + // TODO show modal and delete organization + }, ), ], ), diff --git a/apps/tasks/lib/components/bottom_sheet_pages/patient_bottom_sheet.dart b/apps/tasks/lib/components/bottom_sheet_pages/patient_bottom_sheet.dart index bf28f3ed..0420fb81 100644 --- a/apps/tasks/lib/components/bottom_sheet_pages/patient_bottom_sheet.dart +++ b/apps/tasks/lib/components/bottom_sheet_pages/patient_bottom_sheet.dart @@ -154,7 +154,7 @@ class _PatientBottomSheetState extends State { Center( child: Consumer(builder: (context, patientController, _) { return LoadingFutureBuilder( - data: loadRoomsWithBeds(patientController.patient.id), + future: loadRoomsWithBeds(patientController.patient.id), // TODO use a better loading widget loadingWidget: const SizedBox(), thenBuilder: (context, beds) { diff --git a/apps/tasks/lib/components/bottom_sheet_pages/room_overview_bottom_sheet.dart b/apps/tasks/lib/components/bottom_sheet_pages/room_overview_bottom_sheet.dart index 389d46bf..cee25064 100644 --- a/apps/tasks/lib/components/bottom_sheet_pages/room_overview_bottom_sheet.dart +++ b/apps/tasks/lib/components/bottom_sheet_pages/room_overview_bottom_sheet.dart @@ -1,13 +1,14 @@ import 'package:flutter/material.dart'; import 'package:helpwave_localization/localization.dart'; import 'package:helpwave_service/tasks.dart'; +import 'package:helpwave_theme/constants.dart'; import 'package:helpwave_theme/util.dart'; import 'package:helpwave_util/loading.dart'; import 'package:helpwave_widget/bottom_sheets.dart'; import 'package:helpwave_widget/lists.dart'; import 'package:helpwave_widget/loading.dart'; +import 'package:helpwave_widget/text_input.dart'; import 'package:provider/provider.dart'; -import 'package:tasks/screens/settings_screen.dart'; class RoomOverviewBottomSheetPage extends StatelessWidget { final String roomId; @@ -16,8 +17,11 @@ class RoomOverviewBottomSheetPage extends StatelessWidget { @override Widget build(BuildContext context) { - return ChangeNotifierProvider( - create: (_) => BedsController(roomId: roomId), + return MultiProvider( + providers: [ + ChangeNotifierProvider(create: (context) => BedsController(roomId: roomId)), + ChangeNotifierProvider(create: (context) => RoomController(roomId: roomId)), + ], child: BottomSheetPage( header: BottomSheetHeader.navigation( context, @@ -25,19 +29,42 @@ class RoomOverviewBottomSheetPage extends StatelessWidget { mainAxisSize: MainAxisSize.min, children: [ Text(context.localization!.rooms, style: context.theme.textTheme.titleMedium), - LoadingFutureBuilder( - data: RoomService().get(roomId: roomId), - thenBuilder: (context, room) { - return Text(room.name, style: TextStyle(color: context.theme.hintColor)); - }, - loadingWidget: LoadingAndErrorWidget.pulsing(state: LoadingState.loading, child: SizedBox()), - ), + Consumer(builder: (context, controller, child) { + return LoadingAndErrorWidget( + state: controller.state, + loadingWidget: const PulsingContainer(width: 80), + child: Text(controller.room.name, style: TextStyle(color: context.theme.hintColor)), + ); + }), ], ), ), child: Flexible( child: ListView( children: [ + Consumer( + builder: (context, controller, child) { + return LoadingAndErrorWidget( + state: controller.state, + loadingWidget: const PulsingContainer(height: 80), + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text(context.localization!.name, style: context.theme.textTheme.titleSmall), + const SizedBox(height: distanceTiny), + TextFormFieldWithTimer( + initialValue: controller.state == LoadingState.loaded ? controller.room.name : "", + onUpdate: (value) => controller.update(name: value), + ), + ], + ), + ); + }, + ), + const SizedBox(height: distanceMedium), + Text(context.localization!.beds, style: context.theme.textTheme.titleSmall), + const SizedBox(height: distanceTiny), Consumer( builder: (context, controller, _) { return LoadingAndErrorWidget( @@ -45,11 +72,10 @@ class RoomOverviewBottomSheetPage extends StatelessWidget { child: RoundedListTiles( children: controller.beds .map( - (bed) => NavigationListTile( - icon: Icons.bed_rounded, - title: bed.name, - trailingText: bed.patient?.name ?? context.localization!.unassigned, - onTap: () {}, + (bed) => ListTile( + leading: const Icon(Icons.bed_rounded), + title: Text(bed.name), + trailing: Text(bed.patient?.name ?? context.localization!.unassigned), ), ) .toList(), diff --git a/apps/tasks/lib/components/bottom_sheet_pages/task_bottom_sheet.dart b/apps/tasks/lib/components/bottom_sheet_pages/task_bottom_sheet.dart index a4056951..de40d8bc 100644 --- a/apps/tasks/lib/components/bottom_sheet_pages/task_bottom_sheet.dart +++ b/apps/tasks/lib/components/bottom_sheet_pages/task_bottom_sheet.dart @@ -172,7 +172,7 @@ class _TaskBottomSheetState extends State { child: !taskController.isCreating ? Text(taskController.patient.name) : LoadingFutureBuilder( - data: PatientService().getPatientList(), + future: PatientService().getPatientList(), loadingWidget: const PulsingContainer(), thenBuilder: (context, patientList) { List patients = patientList.active + patientList.unassigned; diff --git a/apps/tasks/lib/components/bottom_sheet_pages/user_bottom_sheet.dart b/apps/tasks/lib/components/bottom_sheet_pages/user_bottom_sheet.dart index 4e92a999..f53d02a3 100644 --- a/apps/tasks/lib/components/bottom_sheet_pages/user_bottom_sheet.dart +++ b/apps/tasks/lib/components/bottom_sheet_pages/user_bottom_sheet.dart @@ -35,7 +35,7 @@ class UserBottomSheetPage extends StatelessWidget { Padding( padding: const EdgeInsets.all(paddingSmall).copyWith(top: paddingMedium), child: LoadingFutureBuilder( - data: UserService().getSelf(), + future: UserService().getSelf(), thenBuilder: (context, data) { return CircleAvatar( backgroundColor: Colors.transparent, diff --git a/apps/tasks/lib/components/bottom_sheet_pages/ward_overview_bottom_sheet.dart b/apps/tasks/lib/components/bottom_sheet_pages/ward_overview_bottom_sheet.dart index 44eeefc2..e20b15f9 100644 --- a/apps/tasks/lib/components/bottom_sheet_pages/ward_overview_bottom_sheet.dart +++ b/apps/tasks/lib/components/bottom_sheet_pages/ward_overview_bottom_sheet.dart @@ -29,7 +29,7 @@ class WardOverviewBottomSheetPage extends StatelessWidget { children: [ Text(context.localization!.rooms, style: context.theme.textTheme.titleMedium), LoadingFutureBuilder( - data: WardService().getWard(id: wardId), + future: WardService().getWard(id: wardId), thenBuilder: (context, ward) { return Text(ward.name, style: TextStyle(color: context.theme.hintColor)); }), diff --git a/apps/tasks/lib/components/bottom_sheet_pages/ward_select_bottom_sheet.dart b/apps/tasks/lib/components/bottom_sheet_pages/ward_select_bottom_sheet.dart index f83d34ed..1ecf1653 100644 --- a/apps/tasks/lib/components/bottom_sheet_pages/ward_select_bottom_sheet.dart +++ b/apps/tasks/lib/components/bottom_sheet_pages/ward_select_bottom_sheet.dart @@ -28,7 +28,7 @@ class WardSelectBottomSheet extends StatelessWidget { mainAxisSize: MainAxisSize.min, child: LoadingFutureBuilder( loadingWidget: const SizedBox(), - data: WardService().getWards(organizationId: organizationId), + future: WardService().getWards(organizationId: organizationId), thenBuilder: (context, wards) { return Flexible( child: ListView( diff --git a/apps/tasks/lib/components/bottom_sheet_pages/wards_bottom_sheet_page.dart b/apps/tasks/lib/components/bottom_sheet_pages/wards_bottom_sheet_page.dart index 35149794..efaaccd1 100644 --- a/apps/tasks/lib/components/bottom_sheet_pages/wards_bottom_sheet_page.dart +++ b/apps/tasks/lib/components/bottom_sheet_pages/wards_bottom_sheet_page.dart @@ -22,7 +22,7 @@ class WardsBottomSheetPage extends StatelessWidget { header: BottomSheetHeader.navigation( context, title: LoadingFutureBuilder( - data: OrganizationService().getOrganization(id: organizationId), + future: OrganizationService().getOrganization(id: organizationId), thenBuilder: (context, data) => Column( mainAxisSize: MainAxisSize.min, children: [ @@ -36,34 +36,28 @@ class WardsBottomSheetPage extends StatelessWidget { ), ], ), + loadingWidget: const PulsingContainer(width: 60, height: 40), ), ), child: Flexible( child: LoadingFutureBuilder( - data: WardService().getWards(organizationId: organizationId), + future: WardService().getWards(organizationId: organizationId), thenBuilder: (context, data) => ListView( + shrinkWrap: true, children: [ - Flexible( - child: ListView( - shrinkWrap: true, - children: [ - const SizedBox(height: distanceMedium), - RoundedListTiles( - children: data - .map( - (ward) => NavigationListTile( - icon: Icons.house_rounded, - title: ward.name, - onTap: () { - NavigationStackController.of(context) - .push(WardOverviewBottomSheetPage(wardId: ward.id)); - }, - ), - ) - .toList()), - ], - ), - ), + const SizedBox(height: distanceMedium), + RoundedListTiles( + children: data + .map( + (ward) => NavigationListTile( + icon: Icons.house_rounded, + title: ward.name, + onTap: () { + NavigationStackController.of(context).push(WardOverviewBottomSheetPage(wardId: ward.id)); + }, + ), + ) + .toList()), ], ), ), diff --git a/apps/tasks/lib/components/user_header.dart b/apps/tasks/lib/components/user_header.dart index f28001a0..2ffe6f6f 100644 --- a/apps/tasks/lib/components/user_header.dart +++ b/apps/tasks/lib/components/user_header.dart @@ -31,7 +31,7 @@ class UserHeader extends StatelessWidget implements PreferredSizeWidget { width: iconSizeSmall, height: iconSizeSmall, child: LoadingFutureBuilder( - data: UserService().getSelf(), + future: UserService().getSelf(), thenBuilder: (context, data) { return CircleAvatar( backgroundColor: Colors.transparent, diff --git a/apps/tasks/lib/screens/settings_screen.dart b/apps/tasks/lib/screens/settings_screen.dart index 653bfeb4..7fc6a824 100644 --- a/apps/tasks/lib/screens/settings_screen.dart +++ b/apps/tasks/lib/screens/settings_screen.dart @@ -241,7 +241,7 @@ class SettingsBottomSheetPage extends StatelessWidget { const SizedBox(height: distanceMedium), titleBuilder(context.localization!.myOrganizations), LoadingFutureBuilder( - data: OrganizationService().getOrganizationsForUser(), + future: OrganizationService().getOrganizationsForUser(), thenBuilder: (context, data) { return RoundedListTiles( children: data @@ -256,6 +256,7 @@ class SettingsBottomSheetPage extends StatelessWidget { .toList(), ); }, + loadingWidget: const PulsingContainer(height: 50), ), const SizedBox(height: distanceMedium), titleBuilder(context.localization!.appearance), diff --git a/packages/helpwave_service/lib/src/api/tasks/controllers/index.dart b/packages/helpwave_service/lib/src/api/tasks/controllers/index.dart index be3fb9f9..2998d200 100644 --- a/packages/helpwave_service/lib/src/api/tasks/controllers/index.dart +++ b/packages/helpwave_service/lib/src/api/tasks/controllers/index.dart @@ -3,5 +3,6 @@ export 'subtask_list_controller.dart'; export 'task_controller.dart'; export 'my_tasks_controller.dart'; export 'ward_patients_controller.dart'; +export 'room_controller.dart'; export 'rooms_controller.dart'; export 'beds_controller.dart'; diff --git a/packages/helpwave_service/lib/src/api/tasks/controllers/room_controller.dart b/packages/helpwave_service/lib/src/api/tasks/controllers/room_controller.dart new file mode 100644 index 00000000..3c7184a9 --- /dev/null +++ b/packages/helpwave_service/lib/src/api/tasks/controllers/room_controller.dart @@ -0,0 +1,89 @@ +import 'dart:async'; +import 'package:helpwave_service/src/api/tasks/index.dart'; +import 'package:helpwave_util/loading.dart'; + +/// The Controller for managing a [Room] +/// +/// Providing a [roomId] means loading and synchronising the [Room]s with +/// the backend while no [roomId] or a empty [String] means that the [Room] is +/// only used locally +class RoomController extends LoadingChangeNotifier { + /// The [Room] + RoomMinimal? _room; + + RoomMinimal get room { + // TODO find a better solution here + return _room ?? RoomMinimal(id: "", name: ""); + } + + set room(RoomMinimal value) { + _room = value; + notifyListeners(); + } + + bool get isCreating => roomId == null || roomId!.isEmpty; + + String? roomId; + + RoomController({this.roomId = "", RoomMinimal? room}) { + assert(room == null || room.id == roomId); + if (room != null) { + _room = room; + roomId = room.id; + } + if (!isCreating) { + load(); + } + } + + /// Loads the [Room]s + Future load() async { + if (isCreating) { + return; + } + loadOp() async { + room = await RoomService().get(roomId: roomId!); + } + + loadHandler(future: loadOp()); + } + + /// Delete the [Room] by the id + Future delete() async { + assert(!isCreating, "deleteById should not be used when creating a completely new Subtask list"); + deleteOp() async { + await RoomService().delete(id: room.id); + } + + loadHandler(future: deleteOp()); + } + + /// Add the [Room] + Future create(RoomMinimal room) async { + assert(isCreating); + createOp() async { + await RoomService().createRoom(wardId: roomId!, name: room.name).then((value) { + roomId = value.id; + room = value; + }); + } + + loadHandler(future: createOp()); + } + + Future update({String? name}) async { + if (isCreating) { + room.name = name ?? room.name; + notifyListeners(); + return; + } + updateOp() async { + assert(!room.isCreating, "To update a room on the server the room must have an id"); + await RoomService().update(id: room.id, name: name); + room.name = name ?? room.name; + notifyListeners(); + } + + loadHandler(future: updateOp()); + } +} diff --git a/packages/helpwave_theme/lib/src/theme/light_theme.dart b/packages/helpwave_theme/lib/src/theme/light_theme.dart index 50eba584..65d1c27d 100644 --- a/packages/helpwave_theme/lib/src/theme/light_theme.dart +++ b/packages/helpwave_theme/lib/src/theme/light_theme.dart @@ -78,7 +78,7 @@ ThemeData lightTheme = makeTheme( defaultColor: defaultColor, // additional - brightness: Brightness.dark, + brightness: Brightness.light, // flutter themes appBarTheme: sharedAppBarTheme.copyWith( diff --git a/packages/helpwave_theme/lib/src/theme/theme.dart b/packages/helpwave_theme/lib/src/theme/theme.dart index bd38d4e0..d71ca522 100644 --- a/packages/helpwave_theme/lib/src/theme/theme.dart +++ b/packages/helpwave_theme/lib/src/theme/theme.dart @@ -3,57 +3,57 @@ import 'package:helpwave_util/material_state.dart'; import '../../constants.dart'; // A function to map incoming colors to a theme -ThemeData makeTheme( - { - // main colors - required Color primaryColor, - required Color onPrimaryColor, - required Color inversePrimaryColor, - required Color secondaryColor, - required Color onSecondaryColor, - required Color tertiary, - required Color onTertiary, - required Color errorColor, - required Color onErrorColor, +ThemeData makeTheme({ + // main colors + required Color primaryColor, + required Color onPrimaryColor, + required Color inversePrimaryColor, + required Color secondaryColor, + required Color onSecondaryColor, + required Color tertiary, + required Color onTertiary, + required Color errorColor, + required Color onErrorColor, - // background - required Color backgroundColor, - required Color onBackgroundColor, + // background + required Color backgroundColor, + required Color onBackgroundColor, - // surfaces - required Color surface, - required Color onSurface, - required Color surfaceVariant, - required Color onSurfaceVariant, - required Color inverseSurface, - required Color onInverseSurface, + // surfaces + required Color surface, + required Color onSurface, + required Color surfaceVariant, + required Color onSurfaceVariant, + required Color inverseSurface, + required Color onInverseSurface, - // container - required Color primaryContainer, - required Color onPrimaryContainer, - required Color secondaryContainer, - required Color onSecondaryContainer, - required Color tertiaryContainer, - required Color onTertiaryContainer, - required Color errorContainer, - required Color onErrorContainer, + // container + required Color primaryContainer, + required Color onPrimaryContainer, + required Color secondaryContainer, + required Color onSecondaryContainer, + required Color tertiaryContainer, + required Color onTertiaryContainer, + required Color errorContainer, + required Color onErrorContainer, - // other - required Color shadow, - required Color outline, - required Color disabledColor, - required Color onDisabledColor, - required Color focusedColor, - required Color defaultColor, + // other + required Color shadow, + required Color outline, + required Color disabledColor, + required Color onDisabledColor, + required Color focusedColor, + required Color defaultColor, - // additional parameters - required Brightness brightness, + // additional parameters + required Brightness brightness, - // flutter themes - AppBarTheme appBarTheme = sharedAppBarTheme, + // flutter themes + AppBarTheme appBarTheme = sharedAppBarTheme, - // text - required Color primaryTextColor}) { + // text + required Color primaryTextColor, +}) { return ThemeData( useMaterial3: true, disabledColor: disabledColor, @@ -109,7 +109,7 @@ ThemeData makeTheme( backgroundColor: surface, ), listTileTheme: ListTileThemeData( - iconColor: primaryTextColor, + iconColor: primaryColor, ), appBarTheme: appBarTheme, elevatedButtonTheme: ElevatedButtonThemeData( diff --git a/packages/helpwave_widget/lib/src/content_selection/list_select.dart b/packages/helpwave_widget/lib/src/content_selection/list_select.dart index c41ba885..1732e1bf 100644 --- a/packages/helpwave_widget/lib/src/content_selection/list_select.dart +++ b/packages/helpwave_widget/lib/src/content_selection/list_select.dart @@ -19,7 +19,7 @@ class ListSelect extends StatelessWidget { @override Widget build(BuildContext context) { return LoadingFutureBuilder( - data: items, + future: Future.value(items), thenBuilder: (context, data) => Column( children: data.map((item) => builder(context, item, () => onSelect(item))).toList(), ), diff --git a/packages/helpwave_widget/lib/src/lists/rounded_list_tiles.dart b/packages/helpwave_widget/lib/src/lists/rounded_list_tiles.dart index f6a16060..0e2881ed 100644 --- a/packages/helpwave_widget/lib/src/lists/rounded_list_tiles.dart +++ b/packages/helpwave_widget/lib/src/lists/rounded_list_tiles.dart @@ -17,7 +17,10 @@ class RoundedListTiles extends StatelessWidget { SizedBox( height: 60, child: Center( - child: Text(context.localization!.nothingYet, style: const TextStyle(fontWeight: FontWeight.w700),), + child: Text( + context.localization!.nothingYet, + style: const TextStyle(fontWeight: FontWeight.w700), + ), ), ) ] @@ -41,23 +44,24 @@ class RoundedListTiles extends StatelessWidget { if (index.isOdd) { return const Divider(); } - Widget item = usedChildren[index ~/ 2]; + final itemIndex = index ~/ 2; + Widget item = usedChildren[itemIndex]; if (index == 0 && usedChildren.length == 1) { return ClipRRect( borderRadius: BorderRadius.circular(borderRadius), - child: item, + child: Material(color: Colors.transparent, child: item), ); } if (index == 0) { return ClipRRect( borderRadius: const BorderRadius.vertical(top: Radius.circular(borderRadius)), - child: item, + child: Material(color: Colors.transparent, child: item), ); } - if (index == usedChildren.length - 1) { + if (itemIndex == usedChildren.length - 1) { return ClipRRect( borderRadius: const BorderRadius.vertical(bottom: Radius.circular(borderRadius)), - child: item, + child: Material(color: Colors.transparent, child: item), ); } return item; diff --git a/packages/helpwave_widget/lib/src/loading/loading_future_builder.dart b/packages/helpwave_widget/lib/src/loading/loading_future_builder.dart index 4c4a4bec..760af440 100644 --- a/packages/helpwave_widget/lib/src/loading/loading_future_builder.dart +++ b/packages/helpwave_widget/lib/src/loading/loading_future_builder.dart @@ -6,49 +6,53 @@ import 'package:helpwave_widget/loading.dart'; /// A Wrapper for the standard [FutureBuilder] to easily distinguish the three /// cases error, loading, then class LoadingFutureBuilder extends StatelessWidget { - /// The [FutureOr] to load - final FutureOr data; + /// The [Future] to load + final Future future; - /// The Builder for the [Widget] upon an successful [FutureOr] + /// The Builder for the [Widget] upon an successful [Future] final Widget Function(BuildContext context, T data) thenBuilder; - /// The Builder for the [Widget] when loading the [FutureOr] + /// The Builder for the [Widget] when loading the [Future] final Widget loadingWidget; - /// The [Widget] for an error containing [FutureOr] + /// The [Widget] for an error containing [Future] final Widget errorWidget; + /// The minimum [Duration] to show the [loadingWidget] + final Duration? minimumLoadingTime; + const LoadingFutureBuilder({ super.key, - required this.data, + required this.future, required this.thenBuilder, this.loadingWidget = const LoadingSpinner(), this.errorWidget = const LoadErrorWidget(), + this.minimumLoadingTime, }); @override Widget build(BuildContext context) { - if(data is Future){ - return FutureBuilder( - future: data as Future, - builder: (context, snapshot) { - LoadingState state = LoadingState.loaded; - if (snapshot.hasError) { - state = LoadingState.error; - } - if (!snapshot.hasData || snapshot.data == null) { - state = LoadingState.loading; - } - return LoadingAndErrorWidget( - state: state, - errorWidget: Center(child: errorWidget), - loadingWidget: Center(child: loadingWidget), - // Safety check because typecast may fail otherwise - child: snapshot.data != null ? thenBuilder(context, snapshot.data as T) : const SizedBox(), - ); - }, - ); - } - return thenBuilder(context, data as T); + return FutureBuilder( + future: Future.wait([ + future, + Future.delayed(minimumLoadingTime ?? Duration.zero), + ]), + builder: (context, snapshot) { + LoadingState state = LoadingState.loaded; + if (snapshot.hasError) { + state = LoadingState.error; + } + if (!snapshot.hasData || snapshot.data == null) { + state = LoadingState.loading; + } + return LoadingAndErrorWidget( + state: state, + errorWidget: Center(child: errorWidget), + loadingWidget: Center(child: loadingWidget), + // Safety check because typecast may fail otherwise + child: snapshot.data != null ? thenBuilder(context, snapshot.data![0] as T) : const SizedBox(), + ); + }, + ); } } diff --git a/packages/helpwave_widget/lib/src/loading/pulsing_container.dart b/packages/helpwave_widget/lib/src/loading/pulsing_container.dart index 64750abe..c7ad29fd 100644 --- a/packages/helpwave_widget/lib/src/loading/pulsing_container.dart +++ b/packages/helpwave_widget/lib/src/loading/pulsing_container.dart @@ -3,8 +3,8 @@ import 'package:helpwave_theme/constants.dart'; /// A [Widget] for content while its loading class PulsingContainer extends StatefulWidget { - /// The [Color] of the container - final Color color; + /// The [Color] of the container defaults to onBackground + final Color? color; /// Minimum opacity of the containers color final double minOpacity; @@ -15,23 +15,27 @@ class PulsingContainer extends StatefulWidget { /// The [Duration] from [minOpacity] to [maxOpacity] or vice versa final Duration duration; + /// The constraints of the [Container] overwritten by [height] an [width] + final BoxConstraints boxConstraints; + /// The height of the [Container] - final double height; + final double? height; /// The width of the [Container] - final double width; + final double? width; /// The border radius of the [Container] final BorderRadiusGeometry? borderRadius; const PulsingContainer({ super.key, - this.color = Colors.grey, - this.minOpacity = 0.3, - this.maxOpacity = 0.8, + this.color, + this.minOpacity = 0.2, + this.maxOpacity = 0.4, this.duration = const Duration(seconds: 1), - this.height = 16, - this.width = 48, + this.boxConstraints = const BoxConstraints.expand(height: 16), + this.width, + this.height, this.borderRadius = const BorderRadius.all(Radius.circular(borderRadiusMedium)), }); @@ -41,7 +45,7 @@ class PulsingContainer extends StatefulWidget { class _PulsingContainerState extends State with TickerProviderStateMixin { late AnimationController _controller; - late Animation _colorAnimation; + late Animation _opacityAnimation; @override void initState() { @@ -52,9 +56,9 @@ class _PulsingContainerState extends State with TickerProvider duration: widget.duration, ); - _colorAnimation = ColorTween( - begin: widget.color.withOpacity(widget.maxOpacity), - end: widget.color.withOpacity(widget.minOpacity), + _opacityAnimation = Tween( + begin: widget.maxOpacity, + end: widget.minOpacity, ).animate(_controller); _controller.repeat(reverse: true); @@ -68,16 +72,23 @@ class _PulsingContainerState extends State with TickerProvider @override Widget build(BuildContext context) { + Color baseColor = widget.color ?? Theme.of(context).colorScheme.onBackground; + return AnimatedBuilder( - animation: _colorAnimation, + animation: _opacityAnimation, builder: (context, child) { + // Interpolate the color's opacity using the _opacityAnimation value return Container( decoration: BoxDecoration( borderRadius: widget.borderRadius, - color: _colorAnimation.value, + color: baseColor.withOpacity(_opacityAnimation.value), + ), + constraints: widget.boxConstraints.copyWith( + minWidth: widget.width, + maxWidth: widget.width, + minHeight: widget.height, + maxHeight: widget.height, ), - width: widget.width, - height: widget.height, ); }, ); From 1d70bfbbabfd3ab1b25aa62fc915a773028b2071 Mon Sep 17 00:00:00 2001 From: Felix Thape Date: Thu, 3 Oct 2024 00:56:00 +0200 Subject: [PATCH 15/36] feat: add add bed button --- .../room_overview_bottom_sheet.dart | 21 ++++++++++++++++++- 1 file changed, 20 insertions(+), 1 deletion(-) diff --git a/apps/tasks/lib/components/bottom_sheet_pages/room_overview_bottom_sheet.dart b/apps/tasks/lib/components/bottom_sheet_pages/room_overview_bottom_sheet.dart index cee25064..44f7d28c 100644 --- a/apps/tasks/lib/components/bottom_sheet_pages/room_overview_bottom_sheet.dart +++ b/apps/tasks/lib/components/bottom_sheet_pages/room_overview_bottom_sheet.dart @@ -63,7 +63,26 @@ class RoomOverviewBottomSheetPage extends StatelessWidget { }, ), const SizedBox(height: distanceMedium), - Text(context.localization!.beds, style: context.theme.textTheme.titleSmall), + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text(context.localization!.beds, style: context.theme.textTheme.titleSmall), + Consumer( + builder: (context, controller, _) { + return LoadingAndErrorWidget( + state: controller.state, + loadingWidget: const PulsingContainer(width: 40), + child: TextButton( + onPressed: () => { + controller.create(Bed(id: "", name: context.localization!.newBed, roomId: roomId)) + }, + child: Text("+ ${context.localization!.add} ${context.localization!.bed}"), + ), + ); + }, + ), + ], + ), const SizedBox(height: distanceTiny), Consumer( builder: (context, controller, _) { From 24dc8aa98f5ebf1a034c36cb2400ba172e4149e4 Mon Sep 17 00:00:00 2001 From: Felix Thape Date: Fri, 4 Oct 2024 15:45:31 +0200 Subject: [PATCH 16/36] feat: add TaskTemplate bottom sheets --- ...tom_sheet.dart => rooms_bottom_sheet.dart} | 6 +- .../task_template_bottom_sheet.dart | 152 +++++++++++++++ .../task_templates_bottom_sheet.dart | 86 +++++++++ .../bottom_sheet_pages/ward_bottom_sheet.dart | 113 +++++++++++ .../wards_bottom_sheet_page.dart | 4 +- apps/tasks/lib/screens/settings_screen.dart | 5 +- .../helpwave_localization/lib/l10n/app_de.arb | 4 +- .../helpwave_localization/lib/l10n/app_en.arb | 4 +- .../src/api/offline/offline_client_store.dart | 4 +- .../lib/src/api/tasks/controllers/index.dart | 2 + .../controllers/task_template_controller.dart | 179 ++++++++++++++++++ .../tasks/controllers/ward_controller.dart | 88 +++++++++ .../api/tasks/data_types/task_template.dart | 42 ++-- .../data_types/task_template_subtask.dart | 35 +++- .../lib/src/api/tasks/data_types/ward.dart | 12 ++ .../patient_offline_client.dart | 2 +- .../offline_clients/task_offline_client.dart | 2 +- .../template_offline_client.dart | 18 +- .../offline_clients/ward_offline_client.dart | 2 +- .../lib/src/api/tasks/services/index.dart | 1 + .../api/tasks/services/task_template_svc.dart | 160 ++++++++++++++++ .../src/api/tasks/services/ward_service.dart | 32 +++- .../api/tasks/tasks_api_service_clients.dart | 5 + .../lib/src/api/util/crud_interface.dart | 25 +++ .../lib/src/auth/current_ward_svc.dart | 4 +- packages/helpwave_util/lib/lists.dart | 2 +- .../helpwave_util/lib/src/lists/index.dart | 3 + .../lib/src/lists/is_index_valid.dart | 3 + .../lib/src/lists/map_with_index.dart | 7 + .../src/loading/loading_change_notifier.dart | 4 + .../src/loading/loading_and_error_widget.dart | 4 +- 31 files changed, 956 insertions(+), 54 deletions(-) rename apps/tasks/lib/components/bottom_sheet_pages/{ward_overview_bottom_sheet.dart => rooms_bottom_sheet.dart} (93%) create mode 100644 apps/tasks/lib/components/bottom_sheet_pages/task_template_bottom_sheet.dart create mode 100644 apps/tasks/lib/components/bottom_sheet_pages/task_templates_bottom_sheet.dart create mode 100644 apps/tasks/lib/components/bottom_sheet_pages/ward_bottom_sheet.dart create mode 100644 packages/helpwave_service/lib/src/api/tasks/controllers/task_template_controller.dart create mode 100644 packages/helpwave_service/lib/src/api/tasks/controllers/ward_controller.dart create mode 100644 packages/helpwave_service/lib/src/api/tasks/services/task_template_svc.dart create mode 100644 packages/helpwave_service/lib/src/api/util/crud_interface.dart create mode 100644 packages/helpwave_util/lib/src/lists/index.dart create mode 100644 packages/helpwave_util/lib/src/lists/is_index_valid.dart create mode 100644 packages/helpwave_util/lib/src/lists/map_with_index.dart diff --git a/apps/tasks/lib/components/bottom_sheet_pages/ward_overview_bottom_sheet.dart b/apps/tasks/lib/components/bottom_sheet_pages/rooms_bottom_sheet.dart similarity index 93% rename from apps/tasks/lib/components/bottom_sheet_pages/ward_overview_bottom_sheet.dart rename to apps/tasks/lib/components/bottom_sheet_pages/rooms_bottom_sheet.dart index e20b15f9..efe14086 100644 --- a/apps/tasks/lib/components/bottom_sheet_pages/ward_overview_bottom_sheet.dart +++ b/apps/tasks/lib/components/bottom_sheet_pages/rooms_bottom_sheet.dart @@ -10,10 +10,10 @@ import 'package:provider/provider.dart'; import 'package:tasks/components/bottom_sheet_pages/room_overview_bottom_sheet.dart'; import 'package:tasks/screens/settings_screen.dart'; -class WardOverviewBottomSheetPage extends StatelessWidget { +class RoomsBottomSheetPage extends StatelessWidget { final String wardId; - const WardOverviewBottomSheetPage({super.key, required this.wardId}); + const RoomsBottomSheetPage({super.key, required this.wardId}); @override Widget build(BuildContext context) { @@ -29,7 +29,7 @@ class WardOverviewBottomSheetPage extends StatelessWidget { children: [ Text(context.localization!.rooms, style: context.theme.textTheme.titleMedium), LoadingFutureBuilder( - future: WardService().getWard(id: wardId), + future: WardService().get(id: wardId), thenBuilder: (context, ward) { return Text(ward.name, style: TextStyle(color: context.theme.hintColor)); }), diff --git a/apps/tasks/lib/components/bottom_sheet_pages/task_template_bottom_sheet.dart b/apps/tasks/lib/components/bottom_sheet_pages/task_template_bottom_sheet.dart new file mode 100644 index 00000000..33563628 --- /dev/null +++ b/apps/tasks/lib/components/bottom_sheet_pages/task_template_bottom_sheet.dart @@ -0,0 +1,152 @@ +import 'package:flutter/material.dart'; +import 'package:helpwave_localization/localization.dart'; +import 'package:helpwave_service/tasks.dart'; +import 'package:helpwave_theme/constants.dart'; +import 'package:helpwave_theme/util.dart'; +import 'package:helpwave_util/lists.dart'; +import 'package:helpwave_widget/bottom_sheets.dart'; +import 'package:helpwave_widget/lists.dart'; +import 'package:helpwave_widget/loading.dart'; +import 'package:helpwave_widget/navigation.dart'; +import 'package:helpwave_widget/shapes.dart'; +import 'package:helpwave_widget/text_input.dart'; +import 'package:provider/provider.dart'; + +import '../subtask_list.dart'; + +class TaskTemplateBottomSheetPage extends StatelessWidget { + final TaskTemplate? template; + final String? templateId; + + const TaskTemplateBottomSheetPage({ + super.key, + this.templateId, + this.template, + }) : assert(templateId != null || template != null, "either template or templateId must be provided"); + + @override + Widget build(BuildContext context) { + return ChangeNotifierProvider( + create: (_) => TaskTemplateController( + templateId: templateId, + taskTemplate: templateId == null ? template : null, + ), + builder: (context, _) => BottomSheetPage( + header: Consumer(builder: (context, controller, _) { + return BottomSheetHeader.navigation( + context, + title: Column( + mainAxisSize: MainAxisSize.min, + children: [ + LoadingAndErrorWidget( + state: controller.state, + loadingWidget: const PulsingContainer(width: 60), + child: Text( + controller.taskTemplate.name, + style: const TextStyle(fontWeight: FontWeight.w800, fontSize: 16), + ), + ), + controller.taskTemplate.wardId != null + ? LoadingFutureBuilder( + future: WardService().get(id: controller.taskTemplate.wardId!), + thenBuilder: (context, data) => Text( + data.name, + style: TextStyle(color: context.theme.hintColor), + ), + loadingWidget: const PulsingContainer(width: 60, height: 40), + errorWidget: LoadErrorWidget( + errorText: controller.error.toString(), + ), + ) + : const SizedBox(), + ], + ), + trailing: controller.isCreating + ? BottomSheetAction( + icon: Icons.check, + onPressed: () { + controller.create().then((_) { + NavigationStackController.of(context).pop(); + }); + }, + ) + : null, // TODO consider screen for using task template + ); + }), + child: Flexible( + child: Consumer( + builder: (context, controller, child) { + return LoadingAndErrorWidget( + state: controller.state, + child: ListView( + children: [ + Text(context.localization!.name, style: context.theme.textTheme.titleSmall), + const SizedBox(height: distanceTiny), + TextFormFieldWithTimer( + initialValue: controller.taskTemplate.name, + onUpdate: (value) => controller.update(name: value), + ), + const SizedBox(height: distanceMedium), + Text(context.localization!.notes, style: context.theme.textTheme.titleSmall), + const SizedBox(height: distanceTiny), + TextFormFieldWithTimer( + initialValue: controller.taskTemplate.description, + onUpdate: (value) => controller.update(description: value), + maxLines: 6, + decoration: + InputDecoration(hintText: "${context.localization!.add} ${context.localization!.notes}"), + ), + const SizedBox(height: distanceMedium), + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text(context.localization!.subtasks, style: context.theme.textTheme.titleSmall), + TextButton( + onPressed: () { + controller.createSubtask(TaskTemplateSubtask( + templateId: controller.templateId, + name: context.localization!.subtask, + )); + }, + child: Text("+ ${context.localization!.add} ${context.localization!.subtask}"), + ), + ], + ), + const SizedBox(height: distanceTiny), + RoundedListTiles( + children: controller.taskTemplate.subtasks.mapWithIndex( + (subtask, index) => ListTile( + leading: Circle(diameter: 16, color: context.theme.colorScheme.primary), + title: Text(subtask.name), + onTap: () { + showDialog( + context: context, + builder: (context) => SubTaskChangeDialog(initialName: subtask.name)).then((value) { + if (value != null) { + controller.updateSubtaskByIndex(index: index, name: value); + } + }); + }, + trailing: IconButton( + onPressed: () { + controller.deleteSubtaskByIndex(index: index); + }, + // TODO get color from theme + icon: const Icon( + Icons.delete, + color: Colors.red, + ), + ), + ), + ), + ) + ], + ), + ); + }, + ), + ), + ), + ); + } +} diff --git a/apps/tasks/lib/components/bottom_sheet_pages/task_templates_bottom_sheet.dart b/apps/tasks/lib/components/bottom_sheet_pages/task_templates_bottom_sheet.dart new file mode 100644 index 00000000..b42ec0ae --- /dev/null +++ b/apps/tasks/lib/components/bottom_sheet_pages/task_templates_bottom_sheet.dart @@ -0,0 +1,86 @@ +import 'package:flutter/material.dart'; +import 'package:helpwave_localization/localization.dart'; +import 'package:helpwave_service/tasks.dart'; +import 'package:helpwave_theme/constants.dart'; +import 'package:helpwave_theme/util.dart'; +import 'package:helpwave_widget/bottom_sheets.dart'; +import 'package:helpwave_widget/lists.dart'; +import 'package:helpwave_widget/loading.dart'; +import 'package:helpwave_widget/navigation.dart'; +import 'package:tasks/components/bottom_sheet_pages/task_template_bottom_sheet.dart'; +import 'package:tasks/screens/settings_screen.dart'; + +class TaskTemplatesBottomSheetPage extends StatelessWidget { + final bool isPersonal; + final String? wardId; + + const TaskTemplatesBottomSheetPage({ + super.key, + this.isPersonal = false, + this.wardId, + }); + + @override + Widget build(BuildContext context) { + return BottomSheetPage( + header: BottomSheetHeader.navigation( + context, + title: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Text( + context.localization!.taskTemplates, + style: const TextStyle(fontWeight: FontWeight.w800, fontSize: 16), + ), + wardId == null + ? const SizedBox() + : LoadingFutureBuilder( + future: WardService().get(id: wardId ?? ""), + thenBuilder: (context, data) => Text( + data.name, + style: TextStyle(color: context.theme.hintColor), + ), + loadingWidget: const PulsingContainer(width: 60), + ), + ], + ), + trailing: BottomSheetAction( + icon: Icons.add, + onPressed: () { + NavigationStackController.of(context).push(TaskTemplateBottomSheetPage( + template: TaskTemplate( + name: context.localization!.task, + wardId: wardId, + isPublicVisible: !isPersonal, + ), + )); + }), + ), + child: Flexible( + child: LoadingFutureBuilder( + future: TaskTemplateService().getMany(wardId: wardId, privateOnly: isPersonal), + thenBuilder: (context, data) => ListView( + shrinkWrap: true, + children: [ + const SizedBox(height: distanceMedium), + RoundedListTiles( + children: data + .map( + (template) => NavigationListTile( + icon: Icons.fact_check_rounded, + title: template.name, + onTap: () { + NavigationStackController.of(context).push(TaskTemplateBottomSheetPage( + templateId: template.id!, + )); + }, + ), + ) + .toList()), + ], + ), + ), + ), + ); + } +} diff --git a/apps/tasks/lib/components/bottom_sheet_pages/ward_bottom_sheet.dart b/apps/tasks/lib/components/bottom_sheet_pages/ward_bottom_sheet.dart new file mode 100644 index 00000000..b872a72a --- /dev/null +++ b/apps/tasks/lib/components/bottom_sheet_pages/ward_bottom_sheet.dart @@ -0,0 +1,113 @@ +import 'package:flutter/material.dart'; +import 'package:helpwave_localization/localization.dart'; +import 'package:helpwave_service/tasks.dart'; +import 'package:helpwave_theme/constants.dart'; +import 'package:helpwave_theme/util.dart'; +import 'package:helpwave_util/loading.dart'; +import 'package:helpwave_widget/bottom_sheets.dart'; +import 'package:helpwave_widget/lists.dart'; +import 'package:helpwave_widget/loading.dart'; +import 'package:helpwave_widget/navigation.dart'; +import 'package:helpwave_widget/text_input.dart'; +import 'package:helpwave_widget/widgets.dart'; +import 'package:provider/provider.dart'; +import 'package:tasks/components/bottom_sheet_pages/rooms_bottom_sheet.dart'; +import 'package:tasks/components/bottom_sheet_pages/task_templates_bottom_sheet.dart'; +import 'package:tasks/screens/settings_screen.dart'; + +class WardBottomSheetPage extends StatelessWidget { + final String wardId; + + const WardBottomSheetPage({super.key, required this.wardId}); + + @override + Widget build(BuildContext context) { + return ChangeNotifierProvider( + create: (context) => WardController(wardId: wardId), + child: BottomSheetPage( + header: BottomSheetHeader.navigation( + context, + title: Consumer( + builder: (context, controller, _) { + return LoadingAndErrorWidget.pulsing( + state: controller.state, + child: Text( + controller.ward.name, + style: const TextStyle(fontWeight: FontWeight.w800, fontSize: 16), + ), + ); + }, + ), + ), + child: Flexible( + child: ListView( + shrinkWrap: true, + children: [ + Consumer( + builder: (context, controller, child) { + return LoadingAndErrorWidget( + state: controller.state, + loadingWidget: const PulsingContainer(height: 80), + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text(context.localization!.name, style: context.theme.textTheme.titleSmall), + const SizedBox(height: distanceTiny), + TextFormFieldWithTimer( + initialValue: controller.state == LoadingState.loaded ? controller.ward.name : "", + onUpdate: (value) => controller.update(name: value), + ), + ], + ), + ); + }, + ), + const SizedBox(height: distanceMedium), + Text(context.localization!.settings, style: context.theme.textTheme.titleMedium), + const SizedBox(height: distanceTiny), + RoundedListTiles( + children: [ + NavigationListTile( + icon: Icons.house_rounded, + title: context.localization!.rooms, + onTap: () { + NavigationStackController.of(context).push(RoomsBottomSheetPage(wardId: wardId)); + }, + ), + NavigationListTile( + icon: Icons.checklist_rounded, + title: context.localization!.taskTemplates, + onTap: () { + NavigationStackController.of(context).push(TaskTemplatesBottomSheetPage(wardId: wardId)); + }, + ), + NavigationListTile( + icon: Icons.label, + title: context.localization!.properties, + onTap: () { + // TODO navigate to properties page + }, + ) + ], + ), + const SizedBox(height: distanceMedium), + Text(context.localization!.dangerZone, style: context.theme.textTheme.titleMedium), + Text( + context.localization!.organizationDangerZoneDescription, + style: TextStyle(color: context.theme.hintColor), + ), + PressableText( + text: "${context.localization!.delete} ${context.localization!.organization}", + style: const TextStyle(color: Colors.red, fontWeight: FontWeight.w700), // TODO get from theme + onPressed: () { + // TODO show modal and delete organization + }, + ), + ], + ), + ), + ), + ); + } +} diff --git a/apps/tasks/lib/components/bottom_sheet_pages/wards_bottom_sheet_page.dart b/apps/tasks/lib/components/bottom_sheet_pages/wards_bottom_sheet_page.dart index efaaccd1..2cdf0145 100644 --- a/apps/tasks/lib/components/bottom_sheet_pages/wards_bottom_sheet_page.dart +++ b/apps/tasks/lib/components/bottom_sheet_pages/wards_bottom_sheet_page.dart @@ -8,7 +8,7 @@ import 'package:helpwave_widget/bottom_sheets.dart'; import 'package:helpwave_widget/lists.dart'; import 'package:helpwave_widget/loading.dart'; import 'package:helpwave_widget/navigation.dart'; -import 'package:tasks/components/bottom_sheet_pages/ward_overview_bottom_sheet.dart'; +import 'package:tasks/components/bottom_sheet_pages/ward_bottom_sheet.dart'; import 'package:tasks/screens/settings_screen.dart'; class WardsBottomSheetPage extends StatelessWidget { @@ -53,7 +53,7 @@ class WardsBottomSheetPage extends StatelessWidget { icon: Icons.house_rounded, title: ward.name, onTap: () { - NavigationStackController.of(context).push(WardOverviewBottomSheetPage(wardId: ward.id)); + NavigationStackController.of(context).push(WardBottomSheetPage(wardId: ward.id)); }, ), ) diff --git a/apps/tasks/lib/screens/settings_screen.dart b/apps/tasks/lib/screens/settings_screen.dart index 7fc6a824..d3dce295 100644 --- a/apps/tasks/lib/screens/settings_screen.dart +++ b/apps/tasks/lib/screens/settings_screen.dart @@ -12,6 +12,7 @@ import 'package:helpwave_widget/loading.dart'; import 'package:helpwave_widget/navigation.dart'; import 'package:provider/provider.dart'; import 'package:tasks/components/bottom_sheet_pages/organization_bottom_sheet.dart'; +import 'package:tasks/components/bottom_sheet_pages/task_templates_bottom_sheet.dart'; import 'package:tasks/screens/login_screen.dart'; /// Screen for settings and other app options @@ -234,7 +235,9 @@ class SettingsBottomSheetPage extends StatelessWidget { NavigationListTile( icon: Icons.checklist_rounded, title: context.localization!.myTaskTemplates, - onTap: () {}, + onTap: () { + NavigationStackController.of(context).push(const TaskTemplatesBottomSheetPage(isPersonal: true)); + }, ), ], ), diff --git a/packages/helpwave_localization/lib/l10n/app_de.arb b/packages/helpwave_localization/lib/l10n/app_de.arb index 8b70d57a..744e734c 100644 --- a/packages/helpwave_localization/lib/l10n/app_de.arb +++ b/packages/helpwave_localization/lib/l10n/app_de.arb @@ -135,6 +135,7 @@ "upcoming": "Todo", "inProgress": "In Arbeit", "done": "Fertig", + "subtask": "Subtask", "subtasks": "Subtasks", "notes": "Notizen", "yourNotes": "Deine Notizen,", @@ -191,5 +192,6 @@ "add": "Hinzufügen", "new_": "Neu", "newRoom": "Neuer Raum", - "newBed": "Neues Bett" + "newBed": "Neues Bett", + "taskTemplates": "Task Vorlagen" } diff --git a/packages/helpwave_localization/lib/l10n/app_en.arb b/packages/helpwave_localization/lib/l10n/app_en.arb index 840bb5ad..3dbf8b63 100644 --- a/packages/helpwave_localization/lib/l10n/app_en.arb +++ b/packages/helpwave_localization/lib/l10n/app_en.arb @@ -135,6 +135,7 @@ "upcoming": "Upcoming", "inProgress": "In Progress", "done": "Done", + "subtask": "Subtask", "subtasks": "Subtasks", "notes": "Notes", "yourNotes": "Your Notes", @@ -191,5 +192,6 @@ "add": "Add", "new_": "New", "newRoom": "New Room", - "newBed": "New Bed" + "newBed": "New Bed", + "taskTemplates": "Task Templates" } diff --git a/packages/helpwave_service/lib/src/api/offline/offline_client_store.dart b/packages/helpwave_service/lib/src/api/offline/offline_client_store.dart index cfddfc0e..204cd60b 100644 --- a/packages/helpwave_service/lib/src/api/offline/offline_client_store.dart +++ b/packages/helpwave_service/lib/src/api/offline/offline_client_store.dart @@ -106,14 +106,14 @@ final List initialTaskTemplates = range(0, 5) .map((index) => TaskTemplate( id: "template$index", name: "template${index + 1}", - notes: "", + description: "", )) .toList() + initialWards .map((ward) => TaskTemplate( id: "wardTemplate${ward.id}", name: "Ward ${ward.name} Template", - notes: "", + description: "", wardId: ward.id, )) .toList(); diff --git a/packages/helpwave_service/lib/src/api/tasks/controllers/index.dart b/packages/helpwave_service/lib/src/api/tasks/controllers/index.dart index 2998d200..7d828f11 100644 --- a/packages/helpwave_service/lib/src/api/tasks/controllers/index.dart +++ b/packages/helpwave_service/lib/src/api/tasks/controllers/index.dart @@ -2,7 +2,9 @@ export 'patient_controller.dart'; export 'subtask_list_controller.dart'; export 'task_controller.dart'; export 'my_tasks_controller.dart'; +export 'task_template_controller.dart'; export 'ward_patients_controller.dart'; +export 'ward_controller.dart'; export 'room_controller.dart'; export 'rooms_controller.dart'; export 'beds_controller.dart'; diff --git a/packages/helpwave_service/lib/src/api/tasks/controllers/task_template_controller.dart b/packages/helpwave_service/lib/src/api/tasks/controllers/task_template_controller.dart new file mode 100644 index 00000000..69696a3a --- /dev/null +++ b/packages/helpwave_service/lib/src/api/tasks/controllers/task_template_controller.dart @@ -0,0 +1,179 @@ +import 'dart:async'; +import 'package:helpwave_service/src/api/tasks/index.dart'; +import 'package:helpwave_util/lists.dart'; +import 'package:helpwave_util/loading.dart'; + +/// The Controller for managing a [TaskTemplate] +/// +/// Providing a [templateId] means loading and synchronising the [TaskTemplate]s with +/// the backend while no [templateId] or a empty [String] means that the [TaskTemplate] is +/// only used locally +class TaskTemplateController extends LoadingChangeNotifier { + /// The [TaskTemplate] + TaskTemplate? _taskTemplate; + + List get subtasks => _taskTemplate?.subtasks ?? []; + + TaskTemplate get taskTemplate { + return _taskTemplate ?? TaskTemplate(name: ""); + } + + set taskTemplate(TaskTemplate value) { + _taskTemplate = value; + notifyListeners(); + } + + bool get isCreating => templateId == null; + + String? templateId; + + TaskTemplateController({this.templateId, TaskTemplate? taskTemplate}) { + assert(taskTemplate == null || taskTemplate.id == templateId, "Either provide only the templateId or provide an " + "taskTemplate that has the same identifier"); + if (taskTemplate != null) { + _taskTemplate = taskTemplate; + templateId = taskTemplate.id; + } + load(); + } + + /// Loads the [TaskTemplate]s + Future load() async { + loadOp() async { + if (isCreating) { + return; + } + taskTemplate = await TaskTemplateService().get(id: templateId!); + } + + loadHandler(future: loadOp()); + } + + /// Add the [TaskTemplate] + Future create() async { + assert(isCreating); + createOp() async { + await TaskTemplateService().create(template: taskTemplate).then((value) { + templateId = value.id; + taskTemplate = value; + }); + await load(); // because the template returned by create does not have the subtask ids + } + + loadHandler(future: createOp()); + } + + Future update({String? name, String? description}) async { + updateOp() async { + if (isCreating) { + _taskTemplate = taskTemplate.copyWith(name: name, description: description); + return; + } + await TaskTemplateService().update(id: taskTemplate.id!, name: name, description: description); + _taskTemplate = taskTemplate.copyWith(name: name, description: description); + notifyListeners(); + } + + loadHandler(future: updateOp()); + } + + /// Delete the [TaskTemplate] by the id + Future delete() async { + assert(!isCreating, "deleteById should not be used when creating a completely new Subtask list"); + deleteOp() async { + await TaskTemplateService().delete(id: taskTemplate.id!); + } + + loadHandler(future: deleteOp()); + } + + /// Add the [TaskTemplateSubtask] + /// + /// **Only use** this when the [TaskTemplate] was **loaded or an initial [TaskTemplate]** is provided + Future createSubtask(TaskTemplateSubtask subtask) async { + assert(_taskTemplate != null); + createOp() async { + if (isCreating) { + _taskTemplate = _taskTemplate!.copyWith(subtasks: [..._taskTemplate!.subtasks, subtask]); + return; + } + await TaskTemplateService().createSubtask(value: subtask).then((value) { + _taskTemplate = _taskTemplate!.copyWith(subtasks: [..._taskTemplate!.subtasks, value]); + }); + } + + loadHandler(future: createOp()); + } + + /// Updates the [TaskTemplateSubtask] by its index in the [List] + /// + /// **Only use** this when the [TaskTemplate] was **loaded or an initial [TaskTemplate]** is provided + Future updateSubtaskByIndex({required int index, String? name}) async { + assert(_taskTemplate != null && _taskTemplate!.subtasks.isIndexValid(index)); + updateOp() async { + if (isCreating) { + _taskTemplate = _taskTemplate!.copyWith( + subtasks: _taskTemplate!.subtasks + .mapWithIndex((element, index1) => index == index1 ? element.copyWith(name: name) : element), + ); + return; + } + final subtaskId = _taskTemplate!.subtasks[index].id!; + await TaskTemplateService().updateSubtask(id: subtaskId, name: name); + _taskTemplate = _taskTemplate!.copyWith( + subtasks: _taskTemplate!.subtasks + .mapWithIndex((element, index1) => index == index1 ? element.copyWith(name: name) : element), + ); + } + + loadHandler(future: updateOp()); + } + + /// Updates the [TaskTemplateSubtask] + /// + /// **Only use** this when the [TaskTemplate] was **loaded and exists** on the server + Future updateSubtask({required String id, String? name}) async { + assert(_taskTemplate != null && !isCreating); + updateOp() async { + await TaskTemplateService().updateSubtask(id: id, name: name); + _taskTemplate = _taskTemplate!.copyWith( + subtasks: _taskTemplate!.subtasks.map((element) => id == element.id ? element.copyWith(name: name) : element) + .toList(), + ); + } + loadHandler(future: updateOp()); + } + + /// Delete the [TaskTemplate] by the index in the [List] + Future deleteSubtaskByIndex({required int index}) async { + assert(_taskTemplate != null && _taskTemplate!.subtasks.isIndexValid(index)); + deleteOp() async { + if (isCreating) { + _taskTemplate = _taskTemplate!.copyWith( + subtasks: [..._taskTemplate!.subtasks]..removeAt(index), + ); + return; + } + final subtaskId = _taskTemplate!.subtasks[index].id!; + await TaskTemplateService().deleteSubtask(id: subtaskId); + _taskTemplate = _taskTemplate!.copyWith( + subtasks: [..._taskTemplate!.subtasks]..removeAt(index), + ); + } + + loadHandler(future: deleteOp()); + } + + /// Delete the [TaskTemplate] by the id + Future deleteSubtask({required String id}) async { + assert(_taskTemplate != null && !isCreating); + deleteOp() async { + await TaskTemplateService().deleteSubtask(id: id); + _taskTemplate = _taskTemplate!.copyWith( + subtasks: _taskTemplate!.subtasks.where((element) => id != element.id).toList(), + ); + } + + loadHandler(future: deleteOp()); + } +} diff --git a/packages/helpwave_service/lib/src/api/tasks/controllers/ward_controller.dart b/packages/helpwave_service/lib/src/api/tasks/controllers/ward_controller.dart new file mode 100644 index 00000000..b5a68655 --- /dev/null +++ b/packages/helpwave_service/lib/src/api/tasks/controllers/ward_controller.dart @@ -0,0 +1,88 @@ +import 'dart:async'; +import 'package:helpwave_service/src/api/tasks/index.dart'; +import 'package:helpwave_util/loading.dart'; + +/// The Controller for managing a [WardMinimal] +/// +/// Providing a [wardId] means loading and synchronising the [WardMinimal]s with +/// the backend while no [wardId] or a empty [String] means that the [WardMinimal] is +/// only used locally +class WardController extends LoadingChangeNotifier { + /// The [WardMinimal] + WardMinimal? _ward; + + WardMinimal get ward { + return _ward ?? WardMinimal(id: "", name: ""); + } + + set ward(WardMinimal value) { + _ward = value; + notifyListeners(); + } + + bool get isCreating => wardId == null || wardId!.isEmpty; + + String? wardId; + + WardController({this.wardId = "", WardMinimal? ward}) { + assert(ward == null || ward.id == wardId); + if (ward != null) { + _ward = ward; + wardId = ward.id; + } + if (!isCreating) { + load(); + } + } + + /// Loads the [WardMinimal]s + Future load() async { + if (isCreating) { + return; + } + loadOp() async { + ward = await WardService().get(id: wardId!); + } + + loadHandler(future: loadOp()); + } + + /// Delete the [WardMinimal] by the id + Future delete() async { + assert(!isCreating, "deleteById should not be used when creating a completely new Subtask list"); + deleteOp() async { + await WardService().delete(id: ward.id); + } + + loadHandler(future: deleteOp()); + } + + /// Add the [WardMinimal] + Future create(WardMinimal ward) async { + assert(isCreating); + createOp() async { + await WardService().create(ward: ward).then((value) { + wardId = value.id; + ward = value; + }); + } + + loadHandler(future: createOp()); + } + + Future update({String? name}) async { + if (isCreating) { + ward.name = name ?? ward.name; + notifyListeners(); + return; + } + updateOp() async { + assert(!ward.isCreating, "To update a ward on the server the ward must have an id"); + await WardService().update(id: ward.id, name: name); + ward.name = name ?? ward.name; + notifyListeners(); + } + + loadHandler(future: updateOp()); + } +} diff --git a/packages/helpwave_service/lib/src/api/tasks/data_types/task_template.dart b/packages/helpwave_service/lib/src/api/tasks/data_types/task_template.dart index 735c1993..ffecdbd9 100644 --- a/packages/helpwave_service/lib/src/api/tasks/data_types/task_template.dart +++ b/packages/helpwave_service/lib/src/api/tasks/data_types/task_template.dart @@ -2,32 +2,35 @@ import '../index.dart'; /// data class for [TaskTemplate] class TaskTemplate { - String id; - String? wardId; + final String? id; + final String? wardId; String name; - String notes; - List subtasks; + String description; + List subtasks; bool isPublicVisible; String? createdBy; - get isWardTemplate => wardId != null; + bool get isWardTemplate => wardId != null; - TaskTemplate({ - required this.id, - this.wardId, - required this.name, - required this.notes, - this.subtasks = const [], - this.isPublicVisible = false, - this.createdBy - }); + bool get isCreating => id == null; + + TaskTemplate( + {this.id, + this.wardId, + required this.name, + this.description = "", + this.subtasks = const [], + this.isPublicVisible = false, + this.createdBy}) + : assert((id == null && subtasks.every((element) => element.isCreating)) || + (id != null && subtasks.every((element) => !element.isCreating))); TaskTemplate copyWith({ String? id, String? wardId, String? name, - String? notes, - List? subtasks, + String? description, + List? subtasks, bool? isPublicVisible, String? createdBy, }) { @@ -35,10 +38,15 @@ class TaskTemplate { id: id ?? this.id, wardId: wardId ?? this.wardId, name: name ?? this.name, - notes: notes ?? this.notes, + description: description ?? this.description, subtasks: subtasks ?? this.subtasks, isPublicVisible: isPublicVisible ?? this.isPublicVisible, createdBy: createdBy ?? this.createdBy, ); } + + @override + String toString() { + return "{id: $id, name: $name, description: $description}"; + } } diff --git a/packages/helpwave_service/lib/src/api/tasks/data_types/task_template_subtask.dart b/packages/helpwave_service/lib/src/api/tasks/data_types/task_template_subtask.dart index b65cde71..ce159f4f 100644 --- a/packages/helpwave_service/lib/src/api/tasks/data_types/task_template_subtask.dart +++ b/packages/helpwave_service/lib/src/api/tasks/data_types/task_template_subtask.dart @@ -1,12 +1,31 @@ -/// data class for [TaskTemplateSubTask] -class TaskTemplateSubTask { - String id; +/// data class for [TaskTemplateSubtask] +class TaskTemplateSubtask { + final String? id; + final String? templateId; String name; - bool isDone; - TaskTemplateSubTask({ - required this.id, + bool get isCreating => id == null || templateId == null; + + TaskTemplateSubtask({ + this.id, + this.templateId, required this.name, - this.isDone = false - }); + }) : assert((templateId == null && id == null) || (templateId != null)); + + TaskTemplateSubtask copyWith({ + String? id, + String? templateId, + String? name, + }) { + return TaskTemplateSubtask( + id: id ?? this.id, + templateId: templateId ?? this.templateId, + name: name ?? this.name, + ); + } + + @override + String toString() { + return "{id: $id, templateId: $templateId, name: $name}"; + } } diff --git a/packages/helpwave_service/lib/src/api/tasks/data_types/ward.dart b/packages/helpwave_service/lib/src/api/tasks/data_types/ward.dart index 1fdab4df..1921e6f8 100644 --- a/packages/helpwave_service/lib/src/api/tasks/data_types/ward.dart +++ b/packages/helpwave_service/lib/src/api/tasks/data_types/ward.dart @@ -8,6 +8,18 @@ class WardMinimal { required this.name, }); + bool get isCreating => id == ""; + + WardMinimal copyWith({ + String? id, + String? name, + }) { + return WardMinimal( + id: id ?? this.id, + name: name ?? this.name, + ); + } + @override String toString() { return "{id: $id, name: $name}"; diff --git a/packages/helpwave_service/lib/src/api/tasks/offline_clients/patient_offline_client.dart b/packages/helpwave_service/lib/src/api/tasks/offline_clients/patient_offline_client.dart index 1783ab40..aaf0e787 100644 --- a/packages/helpwave_service/lib/src/api/tasks/offline_clients/patient_offline_client.dart +++ b/packages/helpwave_service/lib/src/api/tasks/offline_clients/patient_offline_client.dart @@ -205,7 +205,7 @@ class PatientOfflineClient extends PatientServiceClient { mapping(patient) { final res = GetPatientListResponse_Patient( id: patient.id, - notes: patient.notes, + notes: patient.description, humanReadableIdentifier: patient.name, tasks: OfflineClientStore().taskStore.findTasks(patient.id).map((task) => GetPatientListResponse_Task( id: task.id, diff --git a/packages/helpwave_service/lib/src/api/tasks/offline_clients/task_offline_client.dart b/packages/helpwave_service/lib/src/api/tasks/offline_clients/task_offline_client.dart index 7aad563b..75c84626 100644 --- a/packages/helpwave_service/lib/src/api/tasks/offline_clients/task_offline_client.dart +++ b/packages/helpwave_service/lib/src/api/tasks/offline_clients/task_offline_client.dart @@ -295,7 +295,7 @@ class TaskOfflineClient extends TaskServiceClient { mapping(task) => GetTasksByPatientSortedByStatusResponse_Task( id: task.id, name: task.name, - description: task.notes, + description: task.description, dueAt: task.dueDate == null ? null : Timestamp.fromDateTime(task.dueDate!), createdBy: task.createdBy, createdAt: task.creationDate == null ? null : Timestamp.fromDateTime(task.creationDate!), diff --git a/packages/helpwave_service/lib/src/api/tasks/offline_clients/template_offline_client.dart b/packages/helpwave_service/lib/src/api/tasks/offline_clients/template_offline_client.dart index 3479128a..53d94cc9 100644 --- a/packages/helpwave_service/lib/src/api/tasks/offline_clients/template_offline_client.dart +++ b/packages/helpwave_service/lib/src/api/tasks/offline_clients/template_offline_client.dart @@ -7,9 +7,9 @@ import 'package:helpwave_service/src/api/tasks/data_types/index.dart'; class TaskTemplateUpdate { String id; String? name; - String? notes; + String? description; - TaskTemplateUpdate({required this.id, this.name, this.notes}); + TaskTemplateUpdate({required this.id, this.name, this.description}); } class TaskSubtaskTemplateUpdate { @@ -49,7 +49,7 @@ class TaskTemplateOfflineService { found = true; return value.copyWith( name: taskTemplateUpdate.name, - notes: taskTemplateUpdate.notes, + description: taskTemplateUpdate.description, ); } return value; @@ -111,8 +111,8 @@ class TaskTemplateSubtaskOfflineService { } } -class TaskTemplateServicePromiseClient extends TaskTemplateServiceClient { - TaskTemplateServicePromiseClient(super.channel); +class TaskTemplateOfflineClient extends TaskTemplateServiceClient { + TaskTemplateOfflineClient(super.channel); @override ResponseFuture getAllTaskTemplates(GetAllTaskTemplatesRequest request, @@ -137,7 +137,7 @@ class TaskTemplateServicePromiseClient extends TaskTemplateServiceClient { id: template.id, name: template.name, createdBy: template.createdBy, - description: template.notes, + description: template.description, isPublic: template.isPublicVisible, subtasks: OfflineClientStore() .taskTemplateSubtaskStore @@ -157,7 +157,7 @@ class TaskTemplateServicePromiseClient extends TaskTemplateServiceClient { final newTaskTemplate = TaskTemplate( id: DateTime.now().millisecondsSinceEpoch.toString(), name: request.name, - notes: request.description, + description: request.description, wardId: request.hasWardId() ? request.wardId : null, createdBy: OfflineClientStore().userStore.users[0].id, ); @@ -166,13 +166,13 @@ class TaskTemplateServicePromiseClient extends TaskTemplateServiceClient { for (var templateSubtask in request.subtasks) { OfflineClientStore().taskTemplateSubtaskStore.create(Subtask( id: DateTime.now().millisecondsSinceEpoch.toString(), - taskId: newTaskTemplate.id, + taskId: newTaskTemplate.id!, name: templateSubtask.name, isDone: false, )); } - final response = CreateTaskTemplateResponse()..id = newTaskTemplate.id; + final response = CreateTaskTemplateResponse()..id = newTaskTemplate.id!; return MockResponseFuture.value(response); } diff --git a/packages/helpwave_service/lib/src/api/tasks/offline_clients/ward_offline_client.dart b/packages/helpwave_service/lib/src/api/tasks/offline_clients/ward_offline_client.dart index aacb28a3..c920f0d9 100644 --- a/packages/helpwave_service/lib/src/api/tasks/offline_clients/ward_offline_client.dart +++ b/packages/helpwave_service/lib/src/api/tasks/offline_clients/ward_offline_client.dart @@ -59,7 +59,7 @@ class WardOfflineService { }); final taskTemplates = OfflineClientStore().taskTemplateStore.findTaskTemplates(wardId); for (var element in taskTemplates) { - OfflineClientStore().taskTemplateStore.delete(element.id); + OfflineClientStore().taskTemplateStore.delete(element.id!); } } } diff --git a/packages/helpwave_service/lib/src/api/tasks/services/index.dart b/packages/helpwave_service/lib/src/api/tasks/services/index.dart index 78922c72..687a5b53 100644 --- a/packages/helpwave_service/lib/src/api/tasks/services/index.dart +++ b/packages/helpwave_service/lib/src/api/tasks/services/index.dart @@ -3,3 +3,4 @@ export 'room_svc.dart'; export 'bed_svc.dart'; export 'patient_svc.dart'; export 'task_svc.dart'; +export 'task_template_svc.dart'; diff --git a/packages/helpwave_service/lib/src/api/tasks/services/task_template_svc.dart b/packages/helpwave_service/lib/src/api/tasks/services/task_template_svc.dart new file mode 100644 index 00000000..8803fad8 --- /dev/null +++ b/packages/helpwave_service/lib/src/api/tasks/services/task_template_svc.dart @@ -0,0 +1,160 @@ +import 'package:grpc/grpc.dart'; +import 'package:helpwave_proto_dart/services/tasks_svc/v1/task_template_svc.pbgrpc.dart'; +import 'package:helpwave_service/src/api/tasks/data_types/index.dart'; +import 'package:helpwave_service/src/api/tasks/tasks_api_service_clients.dart'; + +/// The Service for [TaskTemplate]s +/// +/// Provides queries and requests that load or alter [TaskTemplate] objects on the server +/// The server is defined in the underlying [TasksAPIServiceClients] +class TaskTemplateService { + /// The GRPC ServiceClient which handles GRPC + TaskTemplateServiceClient taskTemplateService = TasksAPIServiceClients().taskTemplatesServiceClient; + + /// Loads a [TaskTemplate] by its identifier + Future get({required String id}) async { + // TODO use a query here + final templates = await getMany(); + return templates.firstWhere((element) => element.id == id); + } + + /// Loads multiple [TaskTemplate]s which can be filtered by the methods parameters + Future> getMany({ + String? wardId, + String? createdBy, + bool privateOnly = false, + }) async { + GetAllTaskTemplatesRequest request = GetAllTaskTemplatesRequest( + wardId: wardId, + createdBy: createdBy, + privateOnly: privateOnly, + ); + GetAllTaskTemplatesResponse response = await taskTemplateService.getAllTaskTemplates( + request, + options: CallOptions(metadata: TasksAPIServiceClients().getMetaData()), + ); + + return response.templates + .map((template) => TaskTemplate( + id: template.id, + name: template.name, + description: template.description, + wardId: wardId, + createdBy: createdBy ?? template.createdBy, + isPublicVisible: template.isPublic, + subtasks: template.subtasks + .map((subtask) => TaskTemplateSubtask( + id: subtask.id, + templateId: subtask.taskTemplateId, + name: subtask.name, + )) + .toList())) + .toList(); + } + + /// Method to create a [TaskTemplate] + /// + /// Note that the returned [TaskTemplate] does not have any [TaskTemplateSubtask]s as there identifiers are unknown + /// after the request + Future create({required TaskTemplate template}) async { + CreateTaskTemplateRequest request = CreateTaskTemplateRequest( + name: template.name, + description: template.description, + wardId: template.wardId, + subtasks: template.subtasks.map((subtask) => CreateTaskTemplateRequest_SubTask(name: subtask.name))); + CreateTaskTemplateResponse response = await taskTemplateService.createTaskTemplate( + request, + options: CallOptions( + metadata: TasksAPIServiceClients().getMetaData(), + ), + ); + + return template.copyWith(id: response.id, subtasks: []); + } + + /// Method to update a [TaskTemplate] + Future update({ + required String id, + String? name, + String? description, + }) async { + UpdateTaskTemplateRequest request = UpdateTaskTemplateRequest( + id: id, + name: name, + description: description, + ); + + await taskTemplateService.updateTaskTemplate( + request, + options: CallOptions( + metadata: TasksAPIServiceClients().getMetaData(), + ), + ); + + return true; + } + + /// Method to update a [TaskTemplate] + Future delete({required String id}) async { + DeleteTaskTemplateRequest request = DeleteTaskTemplateRequest(id: id); + + await taskTemplateService.deleteTaskTemplate( + request, + options: CallOptions( + metadata: TasksAPIServiceClients().getMetaData(), + ), + ); + + return true; + } + + /// Method to create a [TaskTemplateSubtask] + Future createSubtask({required TaskTemplateSubtask value}) async { + CreateTaskTemplateSubTaskRequest request = CreateTaskTemplateSubTaskRequest( + taskTemplateId: value.templateId, + name: value.name, + ); + CreateTaskTemplateSubTaskResponse response = await taskTemplateService.createTaskTemplateSubTask( + request, + options: CallOptions( + metadata: TasksAPIServiceClients().getMetaData(), + ), + ); + + return value.copyWith(id: response.id); + } + + /// Method to update a [TaskTemplateSubtask] + Future updateSubtask({ + required String id, + String? name, + }) async { + UpdateTaskTemplateSubTaskRequest request = UpdateTaskTemplateSubTaskRequest( + subtaskId: id, + name: name, + ); + + await taskTemplateService.updateTaskTemplateSubTask( + request, + options: CallOptions( + metadata: TasksAPIServiceClients().getMetaData(), + ), + ); + + return true; + } + + /// Method to update a [TaskTemplateSubtask] + Future deleteSubtask({required String id}) async { + DeleteTaskTemplateSubTaskRequest request = DeleteTaskTemplateSubTaskRequest(id: id); + + await taskTemplateService.deleteTaskTemplateSubTask( + request, + options: CallOptions( + metadata: TasksAPIServiceClients().getMetaData(), + ), + ); + + return true; + } +} diff --git a/packages/helpwave_service/lib/src/api/tasks/services/ward_service.dart b/packages/helpwave_service/lib/src/api/tasks/services/ward_service.dart index 5dcf8e79..a7097f6b 100644 --- a/packages/helpwave_service/lib/src/api/tasks/services/ward_service.dart +++ b/packages/helpwave_service/lib/src/api/tasks/services/ward_service.dart @@ -12,7 +12,7 @@ class WardService { WardServiceClient wardService = TasksAPIServiceClients().wardServiceClient; /// Loads a [WardMinimal] by its identifier - Future getWard({required String id}) async { + Future get({required String id}) async { GetWardRequest request = GetWardRequest(id: id); GetWardResponse response = await wardService.getWard( request, @@ -60,5 +60,33 @@ class WardService { .toList(); } - // TODO ward requests + Future create({required WardMinimal ward}) async { + CreateWardRequest request = CreateWardRequest(name: ward.name); + CreateWardResponse response = await wardService.createWard( + request, + options: CallOptions(metadata: TasksAPIServiceClients().getMetaData()), + ); + + return ward.copyWith(id: response.id); + } + + Future update({required String id, String? name}) async { + UpdateWardRequest request = UpdateWardRequest(id: id, name: name); + await wardService.updateWard( + request, + options: CallOptions(metadata: TasksAPIServiceClients().getMetaData()), + ); + + return true; + } + + Future delete({required String id}) async { + DeleteWardRequest request = DeleteWardRequest(id: id); + await wardService.deleteWard( + request, + options: CallOptions(metadata: TasksAPIServiceClients().getMetaData()), + ); + + return true; + } } diff --git a/packages/helpwave_service/lib/src/api/tasks/tasks_api_service_clients.dart b/packages/helpwave_service/lib/src/api/tasks/tasks_api_service_clients.dart index 331e2df9..dd62b12a 100644 --- a/packages/helpwave_service/lib/src/api/tasks/tasks_api_service_clients.dart +++ b/packages/helpwave_service/lib/src/api/tasks/tasks_api_service_clients.dart @@ -1,10 +1,12 @@ import 'package:grpc/grpc.dart'; +import 'package:helpwave_proto_dart/services/tasks_svc/v1/task_template_svc.pbgrpc.dart'; import 'package:helpwave_proto_dart/services/tasks_svc/v1/bed_svc.pbgrpc.dart'; import 'package:helpwave_proto_dart/services/tasks_svc/v1/ward_svc.pbgrpc.dart'; import 'package:helpwave_proto_dart/services/tasks_svc/v1/patient_svc.pbgrpc.dart'; import 'package:helpwave_proto_dart/services/tasks_svc/v1/room_svc.pbgrpc.dart'; import 'package:helpwave_proto_dart/services/tasks_svc/v1/task_svc.pbgrpc.dart'; import 'package:helpwave_service/src/api/tasks/offline_clients/patient_offline_client.dart'; +import 'package:helpwave_service/src/api/tasks/offline_clients/template_offline_client.dart'; import 'package:helpwave_service/src/api/tasks/offline_clients/ward_offline_client.dart'; import 'package:helpwave_service/src/auth/index.dart'; @@ -59,4 +61,7 @@ class TasksAPIServiceClients { TaskServiceClient get taskServiceClient => offlineMode ? TaskOfflineClient(serviceChannel) : TaskServiceClient(serviceChannel); + + TaskTemplateServiceClient get taskTemplatesServiceClient => + offlineMode ? TaskTemplateOfflineClient(serviceChannel) : TaskTemplateServiceClient(serviceChannel); } diff --git a/packages/helpwave_service/lib/src/api/util/crud_interface.dart b/packages/helpwave_service/lib/src/api/util/crud_interface.dart new file mode 100644 index 00000000..c7eb1980 --- /dev/null +++ b/packages/helpwave_service/lib/src/api/util/crud_interface.dart @@ -0,0 +1,25 @@ +abstract class CRUDInterface { + /// A [Function] for loading the object + Future get(String id); + + /// A [Function] for creating the object + Future create(T value); + + /// A [Function] for updating the object + /// + /// Due to a lack in the dart language we cannot enforce a return type of a Function without providing a fixed + /// parameter list + /// + /// Example usage: + /// + /// ```dart + /// @override + /// Future Function({String? id}) get update => ({String? id}) async { + /// return true; + /// } + /// ``` + UpdateFunction get update; + + /// A [Function] for deleting the object + Future delete(String id); +} diff --git a/packages/helpwave_service/lib/src/auth/current_ward_svc.dart b/packages/helpwave_service/lib/src/auth/current_ward_svc.dart index aa7bcb6e..7bbacd43 100644 --- a/packages/helpwave_service/lib/src/auth/current_ward_svc.dart +++ b/packages/helpwave_service/lib/src/auth/current_ward_svc.dart @@ -132,7 +132,7 @@ class CurrentWardService extends Listenable { if (!isLoaded) { fetch(); } - if (kDebugMode) { + if (kDebugMode) { // TODO use logger print(currentWard); } notifyListeners(); @@ -155,7 +155,7 @@ class CurrentWardService extends Listenable { return; } Organization organization = await OrganizationService().getOrganization(id: currentWard!.organizationId); - WardMinimal ward = await WardService().getWard(id: currentWard!.wardId); + WardMinimal ward = await WardService().get(id: currentWard!.wardId); _currentWard = CurrentWardInformation(ward, organization); notifyListeners(); } diff --git a/packages/helpwave_util/lib/lists.dart b/packages/helpwave_util/lib/lists.dart index da052351..017b37ba 100644 --- a/packages/helpwave_util/lib/lists.dart +++ b/packages/helpwave_util/lib/lists.dart @@ -1 +1 @@ -export 'package:helpwave_util/src/lists/range.dart'; +export 'package:helpwave_util/src/lists/index.dart'; diff --git a/packages/helpwave_util/lib/src/lists/index.dart b/packages/helpwave_util/lib/src/lists/index.dart new file mode 100644 index 00000000..f83ec7d5 --- /dev/null +++ b/packages/helpwave_util/lib/src/lists/index.dart @@ -0,0 +1,3 @@ +export 'range.dart'; +export 'is_index_valid.dart'; +export 'map_with_index.dart'; diff --git a/packages/helpwave_util/lib/src/lists/is_index_valid.dart b/packages/helpwave_util/lib/src/lists/is_index_valid.dart new file mode 100644 index 00000000..a071407c --- /dev/null +++ b/packages/helpwave_util/lib/src/lists/is_index_valid.dart @@ -0,0 +1,3 @@ +extension ListIsInRangeExtension on List { + bool isIndexValid(int index) => index >= 0 && index < length; +} diff --git a/packages/helpwave_util/lib/src/lists/map_with_index.dart b/packages/helpwave_util/lib/src/lists/map_with_index.dart new file mode 100644 index 00000000..dd8fb2bf --- /dev/null +++ b/packages/helpwave_util/lib/src/lists/map_with_index.dart @@ -0,0 +1,7 @@ +extension ListMapWithIndexExtension on List { + /// Maps each element of the list to a new value based on its index. + /// The [mapper] function receives the element and its index. + List mapWithIndex(E Function(T element, int index) mapper) { + return indexed.map((value) => mapper(value.$2, value.$1)).toList(); + } +} diff --git a/packages/helpwave_util/lib/src/loading/loading_change_notifier.dart b/packages/helpwave_util/lib/src/loading/loading_change_notifier.dart index a3fa3782..0bcf2bea 100644 --- a/packages/helpwave_util/lib/src/loading/loading_change_notifier.dart +++ b/packages/helpwave_util/lib/src/loading/loading_change_notifier.dart @@ -1,4 +1,5 @@ import 'package:flutter/cupertino.dart'; +import 'package:flutter/foundation.dart'; import '../../loading.dart'; /// A [ChangeNotifier] that manages a [LoadingState] to indicate to components what state they should show @@ -27,6 +28,9 @@ class LoadingChangeNotifier extends ChangeNotifier { bool success = false; defaultErrorHandler(errorObj, _) async { error = errorObj.toString(); + if (kDebugMode) { // TODO use logger + print(error); + } return false; } diff --git a/packages/helpwave_widget/lib/src/loading/loading_and_error_widget.dart b/packages/helpwave_widget/lib/src/loading/loading_and_error_widget.dart index 1f64ddf1..863bda4b 100644 --- a/packages/helpwave_widget/lib/src/loading/loading_and_error_widget.dart +++ b/packages/helpwave_widget/lib/src/loading/loading_and_error_widget.dart @@ -27,8 +27,8 @@ class LoadingAndErrorWidget extends StatelessWidget { super.key, required this.state, required this.child, - this.loadingWidget = const LoadingSpinner(), - this.errorWidget = const LoadErrorWidget(), + this.loadingWidget = const Center(child: LoadingSpinner()), + this.errorWidget = const Center(child: LoadErrorWidget()), this.initialWidget = const SizedBox(), this.unspecifiedWidget, }); From f0dfca8935d2b0c09c65a7733630184ac969876b Mon Sep 17 00:00:00 2001 From: Felix Thape Date: Wed, 9 Oct 2024 15:12:27 +0200 Subject: [PATCH 17/36] feat: add draft for properties service --- apps/tasks/pubspec.lock | 4 +- packages/helpwave_service/lib/property.dart | 1 + .../data_types/attached_property.dart | 52 +++++++++ .../api/property/data_types/field_type.dart | 9 ++ .../src/api/property/data_types/index.dart | 5 + .../src/api/property/data_types/property.dart | 76 +++++++++++++ .../property_view_filter_update.dart | 15 +++ .../api/property/data_types/select_data.dart | 63 +++++++++++ .../api/property/data_types/subject_type.dart | 4 + .../lib/src/api/property/index.dart | 1 + .../property_api_service_clients.dart | 54 ++++++++++ .../property/services/property_service.dart | 100 ++++++++++++++++++ .../src/api/property/util/type_converter.dart | 64 +++++++++++ .../patient_offline_client.dart | 2 +- .../lib/src/api/util/copy_with_interface.dart | 3 + .../lib/src/api/util/crud_interface.dart | 20 +--- .../lib/src/api/util/identified_object.dart | 12 +++ packages/helpwave_service/pubspec.yaml | 2 +- .../helpwave_util/lib/src/lists/index.dart | 1 + .../helpwave_util/lib/src/lists/upsert.dart | 15 +++ 20 files changed, 483 insertions(+), 20 deletions(-) create mode 100644 packages/helpwave_service/lib/property.dart create mode 100644 packages/helpwave_service/lib/src/api/property/data_types/attached_property.dart create mode 100644 packages/helpwave_service/lib/src/api/property/data_types/field_type.dart create mode 100644 packages/helpwave_service/lib/src/api/property/data_types/index.dart create mode 100644 packages/helpwave_service/lib/src/api/property/data_types/property.dart create mode 100644 packages/helpwave_service/lib/src/api/property/data_types/property_view_filter_update.dart create mode 100644 packages/helpwave_service/lib/src/api/property/data_types/select_data.dart create mode 100644 packages/helpwave_service/lib/src/api/property/data_types/subject_type.dart create mode 100644 packages/helpwave_service/lib/src/api/property/index.dart create mode 100644 packages/helpwave_service/lib/src/api/property/property_api_service_clients.dart create mode 100644 packages/helpwave_service/lib/src/api/property/services/property_service.dart create mode 100644 packages/helpwave_service/lib/src/api/property/util/type_converter.dart create mode 100644 packages/helpwave_service/lib/src/api/util/copy_with_interface.dart create mode 100644 packages/helpwave_service/lib/src/api/util/identified_object.dart create mode 100644 packages/helpwave_util/lib/src/lists/upsert.dart diff --git a/apps/tasks/pubspec.lock b/apps/tasks/pubspec.lock index a7f649ca..e171c686 100644 --- a/apps/tasks/pubspec.lock +++ b/apps/tasks/pubspec.lock @@ -272,10 +272,10 @@ packages: dependency: transitive description: name: helpwave_proto_dart - sha256: "5d68c5f9552857ca2d6b7cc3dc2b15275f796ef3cd5fbc5744caacade3bdfb1e" + sha256: b3cb1c868319e384e341f438a1a7d9e1f07bd283c2f63ab35e76745e7ce37d7e url: "https://pub.dev" source: hosted - version: "0.50.0-de87640" + version: "0.56.0-29ad8c8" helpwave_service: dependency: "direct main" description: diff --git a/packages/helpwave_service/lib/property.dart b/packages/helpwave_service/lib/property.dart new file mode 100644 index 00000000..71837b3f --- /dev/null +++ b/packages/helpwave_service/lib/property.dart @@ -0,0 +1 @@ +export 'package:helpwave_service/src/api/property/index.dart'; diff --git a/packages/helpwave_service/lib/src/api/property/data_types/attached_property.dart b/packages/helpwave_service/lib/src/api/property/data_types/attached_property.dart new file mode 100644 index 00000000..404f947b --- /dev/null +++ b/packages/helpwave_service/lib/src/api/property/data_types/attached_property.dart @@ -0,0 +1,52 @@ +import 'package:helpwave_service/src/api/util/copy_with_interface.dart'; +import 'index.dart'; + +class PropertyValue { + final String? text; + final double? number; + final bool? boolValue; + final DateTime? date; + final DateTime? dateTime; + final PropertySelectOption? singleSelect; + final List multiSelect; + + const PropertyValue({ + this.text, + this.number, + this.boolValue, + this.date, + this.dateTime, + this.singleSelect, + this.multiSelect = const [], + }); +} + +class AttachedPropertyUpdate { + final String? propertyId; + final String? subjectId; + final PropertyValue? value; + + const AttachedPropertyUpdate({ + this.propertyId, + this.subjectId, + this.value, + }); +} + +/// The type for attaching a property +class AttachedProperty implements CopyWithInterface { + final String propertyId; + final String subjectId; + final PropertyValue value; + + AttachedProperty({required this.propertyId, required this.subjectId, this.value = const PropertyValue()}); + + @override + AttachedProperty copyWith(AttachedPropertyUpdate update) { + return AttachedProperty( + propertyId: update.propertyId ?? propertyId, + subjectId: update.subjectId ?? subjectId, + value: update.value ?? value, + ); + } +} diff --git a/packages/helpwave_service/lib/src/api/property/data_types/field_type.dart b/packages/helpwave_service/lib/src/api/property/data_types/field_type.dart new file mode 100644 index 00000000..7da662a5 --- /dev/null +++ b/packages/helpwave_service/lib/src/api/property/data_types/field_type.dart @@ -0,0 +1,9 @@ +enum PropertyFieldType { + text, + number, + bool, + date, + dateTime, + singleSelect, + multiSelect +} diff --git a/packages/helpwave_service/lib/src/api/property/data_types/index.dart b/packages/helpwave_service/lib/src/api/property/data_types/index.dart new file mode 100644 index 00000000..be0c741f --- /dev/null +++ b/packages/helpwave_service/lib/src/api/property/data_types/index.dart @@ -0,0 +1,5 @@ +export 'attached_property.dart'; +export 'field_type.dart'; +export 'property.dart'; +export 'select_data.dart'; +export 'subject_type.dart'; diff --git a/packages/helpwave_service/lib/src/api/property/data_types/property.dart b/packages/helpwave_service/lib/src/api/property/data_types/property.dart new file mode 100644 index 00000000..6166e4dc --- /dev/null +++ b/packages/helpwave_service/lib/src/api/property/data_types/property.dart @@ -0,0 +1,76 @@ +import 'package:helpwave_service/src/api/property/data_types/field_type.dart'; +import 'package:helpwave_service/src/api/property/data_types/select_data.dart'; +import 'package:helpwave_service/src/api/property/data_types/subject_type.dart'; +import 'package:helpwave_service/src/api/util/copy_with_interface.dart'; +import 'package:helpwave_service/src/api/util/identified_object.dart'; + +class PropertyUpdate { + String? id; + String? name; + String? description; + PropertySubjectType? subjectType; + PropertyFieldType? fieldType; + bool? isArchived; + String? setId; + PropertySelectDataUpdate? selectDataUpdate; + bool? alwaysIncludeForViewSource; + bool? removeSetId; + bool? removeAlwaysIncludeForViewSource; + + PropertyUpdate({ + this.id, + this.name, + this.description, + this.subjectType, + this.fieldType, + this.isArchived, + this.setId, + this.selectData, + this.alwaysIncludeForViewSource, + this.removeSetId, + this.removeAlwaysIncludeForViewSource, + }); +} + +class Property extends IdentifiedObject implements CopyWithInterface { + final String name; + final String description; + final PropertySubjectType subjectType; + final PropertyFieldType fieldType; + final bool isArchived; + final String? setId; + final PropertySelectData? selectData; + final bool? alwaysIncludeForViewSource; + + bool get isSelectType => fieldType == PropertyFieldType.singleSelect || fieldType == PropertyFieldType.multiSelect; + + Property({ + super.id, + required this.name, + this.description = "", + required this.subjectType, + required this.fieldType, + required this.isArchived, + this.setId, + this.selectData, + this.alwaysIncludeForViewSource, + }) : assert(!(fieldType == PropertyFieldType.singleSelect || fieldType == PropertyFieldType.multiSelect) || + selectData != null); + + @override + copyWith(PropertyUpdate update) { + return Property( + id: update.id ?? id, + name: update.name ?? name, + description: update.description ?? description, + subjectType: update.subjectType ?? subjectType, + fieldType: update.fieldType ?? fieldType, + isArchived: update.isArchived ?? isArchived, + setId: (update.removeSetId ?? false) ? null : update.setId ?? setId, + selectData: update.selectData ?? selectData, + alwaysIncludeForViewSource: (update.removeAlwaysIncludeForViewSource ?? false) + ? null + : update.alwaysIncludeForViewSource ?? alwaysIncludeForViewSource, + ); + } +} diff --git a/packages/helpwave_service/lib/src/api/property/data_types/property_view_filter_update.dart b/packages/helpwave_service/lib/src/api/property/data_types/property_view_filter_update.dart new file mode 100644 index 00000000..c6e7bec9 --- /dev/null +++ b/packages/helpwave_service/lib/src/api/property/data_types/property_view_filter_update.dart @@ -0,0 +1,15 @@ +class PropertyViewFilterUpdate { + final String subjectId; + final List appendToAlwaysInclude; + final List removeFromAlwaysInclude; + final List appendToDontAlwaysInclude; + final List removeFromDontAlwaysInclude; + + PropertyViewFilterUpdate({ + required this.subjectId, + this.appendToAlwaysInclude = const [], + this.removeFromAlwaysInclude = const [], + this.appendToDontAlwaysInclude = const [], + this.removeFromDontAlwaysInclude = const [], + }); +} diff --git a/packages/helpwave_service/lib/src/api/property/data_types/select_data.dart b/packages/helpwave_service/lib/src/api/property/data_types/select_data.dart new file mode 100644 index 00000000..a07bb828 --- /dev/null +++ b/packages/helpwave_service/lib/src/api/property/data_types/select_data.dart @@ -0,0 +1,63 @@ +import 'package:helpwave_util/lists.dart'; + +import '../../util/copy_with_interface.dart'; +import '../../util/identified_object.dart'; + +class PropertySelectOptionUpdate { + final String? id; + final String? name; + final String? description; + final bool? isCustom; + + PropertySelectOptionUpdate({this.id, this.name, this.description, this.isCustom}); +} + +class PropertySelectOption extends IdentifiedObject + implements CopyWithInterface { + final String name; + final String description; + final bool isCustom; + + PropertySelectOption({ + super.id, + required this.name, + this.description = "", + this.isCustom = false, + }); + + @override + copyWith(PropertySelectOptionUpdate update) { + return PropertySelectOption( + id: update.id ?? id, + name: update.name ?? name, + description: update.description ?? description, + isCustom: update.isCustom ?? isCustom, + ); + } +} + +typedef PropertySelectDataUpdate = ({ + bool? isAllowingFreeText, + List? removeOptions, + List? upsert, + List? options +}); + +class PropertySelectData implements CopyWithInterface { + final bool isAllowingFreeText; + final List options; + + PropertySelectData({this.isAllowingFreeText = false, this.options = const []}); + + @override + PropertySelectData copyWith(PropertySelectDataUpdate update) { + assert( + update.options == null || (update.upsert == null && update.removeOptions == null), + "Only provide either the upsert/remove or only the option overwrite", + ); + return PropertySelectData( + isAllowingFreeText: update.isAllowingFreeText ?? isAllowingFreeText, + options: update.options ?? options.upsert(update.upsert!, (a) => a.id!), + ); + } +} diff --git a/packages/helpwave_service/lib/src/api/property/data_types/subject_type.dart b/packages/helpwave_service/lib/src/api/property/data_types/subject_type.dart new file mode 100644 index 00000000..7bd72018 --- /dev/null +++ b/packages/helpwave_service/lib/src/api/property/data_types/subject_type.dart @@ -0,0 +1,4 @@ +enum PropertySubjectType { + patient, + task +} diff --git a/packages/helpwave_service/lib/src/api/property/index.dart b/packages/helpwave_service/lib/src/api/property/index.dart new file mode 100644 index 00000000..a41c25f8 --- /dev/null +++ b/packages/helpwave_service/lib/src/api/property/index.dart @@ -0,0 +1 @@ +export 'data_types/index.dart'; diff --git a/packages/helpwave_service/lib/src/api/property/property_api_service_clients.dart b/packages/helpwave_service/lib/src/api/property/property_api_service_clients.dart new file mode 100644 index 00000000..f83ee4d2 --- /dev/null +++ b/packages/helpwave_service/lib/src/api/property/property_api_service_clients.dart @@ -0,0 +1,54 @@ +import 'package:grpc/grpc.dart'; +import 'package:helpwave_proto_dart/services/property_svc/v1/property_svc.pbgrpc.dart'; +import 'package:helpwave_proto_dart/services/property_svc/v1/property_set_svc.pbgrpc.dart'; +import 'package:helpwave_proto_dart/services/property_svc/v1/property_value_svc.pbgrpc.dart'; +import 'package:helpwave_proto_dart/services/property_svc/v1/property_views_svc.pbgrpc.dart'; +import 'package:helpwave_service/src/auth/index.dart'; + +/// The Underlying GrpcService it provides other clients and the correct metadata for the requests +class PropertyAPIServiceClients { + PropertyAPIServiceClients._privateConstructor(); + + static final PropertyAPIServiceClients _instance = PropertyAPIServiceClients._privateConstructor(); + + factory PropertyAPIServiceClients() => _instance; + + /// The api URL used + String? apiUrl; + + bool offlineMode = false; + + ClientChannel get serviceChannel { + assert(apiUrl != null); + return ClientChannel(apiUrl!); + } + + Map getMetaData({String? organizationId}) { + var metaData = { + ...AuthenticationUtility.authMetaData, + "dapr-app-id": "property-svc", + }; + + if (organizationId != null) { + metaData["X-Organization"] = organizationId; + } else { + metaData["X-Organization"] = AuthenticationUtility.fallbackOrganizationId!; + } + + return metaData; + } + + // TODO add offline clients here + PropertyServiceClient get propertyServiceClient => + offlineMode ? PropertyServiceClient(serviceChannel) : PropertyServiceClient(serviceChannel); + + PropertyValueServiceClient get propertyValueServiceClient => + offlineMode ? PropertyValueServiceClient(serviceChannel) : PropertyValueServiceClient(serviceChannel); + + PropertyViewsServiceClient get propertyViewsServiceClient => + offlineMode ? PropertyViewsServiceClient(serviceChannel) : PropertyViewsServiceClient(serviceChannel); + + PropertySetServiceClient get propertySetServiceClient => + offlineMode ? PropertySetServiceClient(serviceChannel) : PropertySetServiceClient(serviceChannel); + +} diff --git a/packages/helpwave_service/lib/src/api/property/services/property_service.dart b/packages/helpwave_service/lib/src/api/property/services/property_service.dart new file mode 100644 index 00000000..1776b4fa --- /dev/null +++ b/packages/helpwave_service/lib/src/api/property/services/property_service.dart @@ -0,0 +1,100 @@ +import 'package:grpc/grpc.dart'; +import 'package:helpwave_proto_dart/services/property_svc/v1/property_svc.pbgrpc.dart'; +import 'package:helpwave_service/src/api/property/property_api_service_clients.dart'; +import 'package:helpwave_service/src/api/util/crud_interface.dart'; +import '../../../../property.dart'; +import '../util/type_converter.dart'; + +/// The GRPC Service for [Property]s +/// +/// Provides queries and requests that load or alter [Property] objects on the server +/// The server is defined in the underlying [PropertyAPIServiceClients] +class PropertyService implements CRUDInterface{ + /// The GRPC ServiceClient which handles GRPC + PropertyServiceClient service = PropertyAPIServiceClients().propertyServiceClient; + + @override + Future get(String id) async { + GetPropertyRequest request = GetPropertyRequest(id: id); + GetPropertyResponse response = await service.getProperty( + request, + options: CallOptions(metadata: PropertyAPIServiceClients().getMetaData()), + ); + + return Property( + id: response.id, + name: response.name, + subjectType: PropertyGRPCTypeConverter.subjectTypeFromGRPC(response.subjectType), + fieldType: PropertyGRPCTypeConverter.fieldTypeFromGRPC(response.fieldType), + description: response.description, + setId: response.hasSetId() ? response.setId : null, + alwaysIncludeForViewSource: + response.hasAlwaysIncludeForViewSource() ? response.alwaysIncludeForViewSource : null, + isArchived: response.isArchived, + selectData: response.hasSelectData() ? PropertySelectData() : null); + } + + Future> getMany({PropertySubjectType? subjectType}) async { + GetPropertiesRequest request = GetPropertiesRequest(); + if (subjectType != null) { + request.subjectType = PropertyGRPCTypeConverter.subjectTypeToGRPC(subjectType); + } + GetPropertiesResponse response = await service.getProperties( + request, + options: CallOptions(metadata: PropertyAPIServiceClients().getMetaData()), + ); + + List beds = response.properties + .map((value) => Property( + id: value.id, + name: value.name, + subjectType: PropertyGRPCTypeConverter.subjectTypeFromGRPC(value.subjectType), + fieldType: PropertyGRPCTypeConverter.fieldTypeFromGRPC(value.fieldType), + description: value.description, + setId: value.hasSetId() ? value.setId : null, + isArchived: value.isArchived, + selectData: value.hasSelectData() ? PropertySelectData() : null)) + .toList(); + + return beds; + } + + @override + Future create(Property property) async { + CreatePropertyRequest request = CreatePropertyRequest( + name: property.name, + description: property.description, + setId: property.setId, + subjectType: PropertyGRPCTypeConverter.subjectTypeToGRPC(property.subjectType), + fieldType: PropertyGRPCTypeConverter.fieldTypeToGRPC(property.fieldType), + selectData: property.isSelectType + ? CreatePropertyRequest_SelectData( + options: property.selectData!.options.map((option) => CreatePropertyRequest_SelectData_SelectOption( + name: option.name, + description: option.description, + )), + allowFreetext: property.selectData!.isAllowingFreeText, + ) + : null); + CreatePropertyResponse response = await service.createProperty( + request, + options: CallOptions(metadata: PropertyAPIServiceClients().getMetaData()), + ); + + return property.copyWith(PropertyUpdate(id: response.propertyId,)); + } + + @override + Future update(String id, PropertyUpdate update) async { + UpdatePropertyRequest request = UpdatePropertyRequest(id: id, name: name); + await service.updateBed( + request, + options: CallOptions(metadata: PropertyAPIServiceClients().getMetaData()), + ); + } + + @override + Future delete(String id) { + throw UnimplementedError("The Deletion of Properties is currently not allowed"); + } +} diff --git a/packages/helpwave_service/lib/src/api/property/util/type_converter.dart b/packages/helpwave_service/lib/src/api/property/util/type_converter.dart new file mode 100644 index 00000000..0353417b --- /dev/null +++ b/packages/helpwave_service/lib/src/api/property/util/type_converter.dart @@ -0,0 +1,64 @@ +import 'package:helpwave_proto_dart/services/property_svc/v1/types.pbenum.dart' as proto; +import 'package:helpwave_service/property.dart'; + +class PropertyGRPCTypeConverter { + static proto.SubjectType subjectTypeToGRPC(PropertySubjectType subjectType) { + switch (subjectType) { + case PropertySubjectType.patient: + return proto.SubjectType.SUBJECT_TYPE_PATIENT; + case PropertySubjectType.task: + return proto.SubjectType.SUBJECT_TYPE_TASK; + } + } + + static PropertySubjectType subjectTypeFromGRPC(proto.SubjectType subjectType) { + switch (subjectType) { + case proto.SubjectType.SUBJECT_TYPE_PATIENT: + return PropertySubjectType.patient; + case proto.SubjectType.SUBJECT_TYPE_TASK: + return PropertySubjectType.task; + default: + throw "SubjectType unspecified is not allowed"; + } + } + + static proto.FieldType fieldTypeToGRPC(PropertyFieldType fieldType) { + switch (fieldType) { + case PropertyFieldType.text: + return proto.FieldType.FIELD_TYPE_TEXT; + case PropertyFieldType.number: + return proto.FieldType.FIELD_TYPE_NUMBER; + case PropertyFieldType.bool: + return proto.FieldType.FIELD_TYPE_CHECKBOX; + case PropertyFieldType.date: + return proto.FieldType.FIELD_TYPE_DATE; + case PropertyFieldType.dateTime: + return proto.FieldType.FIELD_TYPE_DATE_TIME; + case PropertyFieldType.singleSelect: + return proto.FieldType.FIELD_TYPE_SELECT; + case PropertyFieldType.multiSelect: + return proto.FieldType.FIELD_TYPE_MULTI_SELECT; + } + } + + static PropertyFieldType fieldTypeFromGRPC(proto.FieldType fieldType) { + switch (fieldType) { + case proto.FieldType.FIELD_TYPE_TEXT: + return PropertyFieldType.text; + case proto.FieldType.FIELD_TYPE_NUMBER: + return PropertyFieldType.number; + case proto.FieldType.FIELD_TYPE_CHECKBOX: + return PropertyFieldType.bool; + case proto.FieldType.FIELD_TYPE_DATE: + return PropertyFieldType.date; + case proto.FieldType.FIELD_TYPE_DATE_TIME: + return PropertyFieldType.dateTime; + case proto.FieldType.FIELD_TYPE_SELECT: + return PropertyFieldType.singleSelect; + case proto.FieldType.FIELD_TYPE_MULTI_SELECT: + return PropertyFieldType.multiSelect; + default: + throw "FieldType unspecified is not allowed"; + } + } +} diff --git a/packages/helpwave_service/lib/src/api/tasks/offline_clients/patient_offline_client.dart b/packages/helpwave_service/lib/src/api/tasks/offline_clients/patient_offline_client.dart index aaf0e787..32627586 100644 --- a/packages/helpwave_service/lib/src/api/tasks/offline_clients/patient_offline_client.dart +++ b/packages/helpwave_service/lib/src/api/tasks/offline_clients/patient_offline_client.dart @@ -258,7 +258,7 @@ class PatientOfflineClient extends PatientServiceClient { ResponseFuture getRecentPatients(GetRecentPatientsRequest request, {CallOptions? options}) { final patients = OfflineClientStore().patientStore.patients.where((element) => element.hasBed).map((patient) { - final res = GetRecentPatientsResponse_PatientWithRoomAndBed( + final res = GetRecentPatientsResponse_Patient( id: patient.id, humanReadableIdentifier: patient.name, ); diff --git a/packages/helpwave_service/lib/src/api/util/copy_with_interface.dart b/packages/helpwave_service/lib/src/api/util/copy_with_interface.dart new file mode 100644 index 00000000..057810c5 --- /dev/null +++ b/packages/helpwave_service/lib/src/api/util/copy_with_interface.dart @@ -0,0 +1,3 @@ +abstract class CopyWithInterface { + T copyWith(U update); +} diff --git a/packages/helpwave_service/lib/src/api/util/crud_interface.dart b/packages/helpwave_service/lib/src/api/util/crud_interface.dart index c7eb1980..78ec707d 100644 --- a/packages/helpwave_service/lib/src/api/util/crud_interface.dart +++ b/packages/helpwave_service/lib/src/api/util/crud_interface.dart @@ -1,24 +1,12 @@ -abstract class CRUDInterface { +abstract class CRUDInterface { /// A [Function] for loading the object Future get(String id); /// A [Function] for creating the object - Future create(T value); + Future create(Create value); - /// A [Function] for updating the object - /// - /// Due to a lack in the dart language we cannot enforce a return type of a Function without providing a fixed - /// parameter list - /// - /// Example usage: - /// - /// ```dart - /// @override - /// Future Function({String? id}) get update => ({String? id}) async { - /// return true; - /// } - /// ``` - UpdateFunction get update; + /// A [Function] for updating the object with an update object + Future update(String id, Update update); /// A [Function] for deleting the object Future delete(String id); diff --git a/packages/helpwave_service/lib/src/api/util/identified_object.dart b/packages/helpwave_service/lib/src/api/util/identified_object.dart new file mode 100644 index 00000000..3cb522b9 --- /dev/null +++ b/packages/helpwave_service/lib/src/api/util/identified_object.dart @@ -0,0 +1,12 @@ +/// A class for the identification of objects +class IdentifiedObject { + final T? id; + + bool get isCreating => id == null; + + IdentifiedObject({this.id}); + + bool isReferencingSame(IdentifiedObject other) { + return runtimeType == other.runtimeType && this.id == other.id; + } +} diff --git a/packages/helpwave_service/pubspec.yaml b/packages/helpwave_service/pubspec.yaml index 71bcb407..e25a6e2b 100644 --- a/packages/helpwave_service/pubspec.yaml +++ b/packages/helpwave_service/pubspec.yaml @@ -17,7 +17,7 @@ dependencies: flutter_secure_storage: 9.0.0 jose: ^0.3.4 logger: ^2.0.2+1 - helpwave_proto_dart: ^0.50.0-de87640 + helpwave_proto_dart: ^0.56.0-29ad8c8 grpc: ^3.2.4 helpwave_util: path: "../helpwave_util" diff --git a/packages/helpwave_util/lib/src/lists/index.dart b/packages/helpwave_util/lib/src/lists/index.dart index f83ec7d5..31170fd6 100644 --- a/packages/helpwave_util/lib/src/lists/index.dart +++ b/packages/helpwave_util/lib/src/lists/index.dart @@ -1,3 +1,4 @@ export 'range.dart'; export 'is_index_valid.dart'; export 'map_with_index.dart'; +export 'upsert.dart'; diff --git a/packages/helpwave_util/lib/src/lists/upsert.dart b/packages/helpwave_util/lib/src/lists/upsert.dart new file mode 100644 index 00000000..83e0b9bd --- /dev/null +++ b/packages/helpwave_util/lib/src/lists/upsert.dart @@ -0,0 +1,15 @@ +extension UpsertExtension on List { + List upsert(List upsertList, String Function(T a) hash) { + Map uniqueMap = {}; + + for (T item in this) { + uniqueMap[hash(item)] = item; + } + + for (T item in upsertList) { + uniqueMap[hash(item)] = item; + } + + return uniqueMap.values.toList(); + } +} From ff7781f5a92e10bb62e6d093fe943b58d6fd8542 Mon Sep 17 00:00:00 2001 From: Felix Thape Date: Wed, 9 Oct 2024 23:55:05 +0200 Subject: [PATCH 18/36] feat: complete property api clients and fix various bugs --- .../patient_bottom_sheet.dart | 21 ++- .../rooms_bottom_sheet.dart | 11 +- .../bottom_sheet_pages/task_bottom_sheet.dart | 123 ++++++++---------- .../bottom_sheet_pages/user_bottom_sheet.dart | 74 ++++++----- .../bottom_sheet_pages/ward_bottom_sheet.dart | 4 +- apps/tasks/lib/components/patient_card.dart | 2 +- .../lib/components/patient_selector.dart | 40 ++++++ apps/tasks/lib/components/subtask_list.dart | 6 +- .../lib/components/visibility_select.dart | 3 +- .../patient_screen.dart | 62 +++++---- .../tasks/lib/screens/ward_select_screen.dart | 1 - .../data_types/attached_property.dart | 79 ++++++++++- .../src/api/property/data_types/property.dart | 24 ++-- .../property_view_filter_update.dart | 4 +- .../api/property/data_types/select_data.dart | 22 ++-- .../property/services/property_service.dart | 31 ++++- .../services/property_value_service.dart | 89 +++++++++++++ .../services/property_views_service.dart | 38 ++++++ .../tasks/controllers/patient_controller.dart | 37 +++--- .../controllers/subtask_list_controller.dart | 19 ++- .../tasks/controllers/task_controller.dart | 19 +-- .../lib/src/api/tasks/data_types/patient.dart | 16 +-- .../lib/src/api/tasks/data_types/subtask.dart | 10 +- .../lib/src/api/tasks/data_types/task.dart | 23 ++-- .../offline_clients/bed_offline_client.dart | 2 +- .../patient_offline_client.dart | 4 +- .../offline_clients/task_offline_client.dart | 10 +- .../template_offline_client.dart | 4 +- .../lib/src/api/util/copy_with_interface.dart | 2 +- .../lib/src/api/util/identified_object.dart | 5 + .../lib/src/auth/identity.dart | 4 +- .../src/bottom_sheets/bottom_sheet_base.dart | 3 +- .../src/text_input/clickable_text_edit.dart | 1 + .../text_form_field_with_timer.dart | 30 +++-- 34 files changed, 545 insertions(+), 278 deletions(-) create mode 100644 apps/tasks/lib/components/patient_selector.dart create mode 100644 packages/helpwave_service/lib/src/api/property/services/property_value_service.dart create mode 100644 packages/helpwave_service/lib/src/api/property/services/property_views_service.dart diff --git a/apps/tasks/lib/components/bottom_sheet_pages/patient_bottom_sheet.dart b/apps/tasks/lib/components/bottom_sheet_pages/patient_bottom_sheet.dart index 0420fb81..3ae537f6 100644 --- a/apps/tasks/lib/components/bottom_sheet_pages/patient_bottom_sheet.dart +++ b/apps/tasks/lib/components/bottom_sheet_pages/patient_bottom_sheet.dart @@ -26,7 +26,7 @@ class PatientBottomSheet extends StatefulWidget { } class _PatientBottomSheetState extends State { - Future> loadRoomsWithBeds(String patientId) async { + Future> loadRoomsWithBeds({String? patientId}) async { List rooms = await RoomService().getRoomOverviews(wardId: CurrentWardService().currentWard!.wardId); @@ -51,7 +51,7 @@ class _PatientBottomSheetState extends State { create: (_) => PatientController(Patient.empty(id: widget.patentId)), ), ], - child: BottomSheetBase( + child: BottomSheetPage( header: BottomSheetHeader( title: Consumer(builder: (context, patientController, _) { if (patientController.state == LoadingState.loaded || patientController.isCreating) { @@ -72,11 +72,8 @@ class _PatientBottomSheetState extends State { } }), ), - onClosing: () { - // TODO handle this - }, - bottomWidget: Padding( - padding: const EdgeInsets.only(top: paddingSmall), + bottom: Padding( + padding: const EdgeInsets.symmetric(vertical: paddingSmall), child: Consumer(builder: (context, patientController, _) { return LoadingAndErrorWidget( state: patientController.state, @@ -147,16 +144,14 @@ class _PatientBottomSheetState extends State { )); }), ), - child: SingleChildScrollView( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, + child: Flexible( + child: ListView( children: [ Center( child: Consumer(builder: (context, patientController, _) { return LoadingFutureBuilder( - future: loadRoomsWithBeds(patientController.patient.id), - // TODO use a better loading widget - loadingWidget: const SizedBox(), + future: loadRoomsWithBeds(patientId: patientController.patient.id), + loadingWidget: const PulsingContainer(width: 80, height: 20), thenBuilder: (context, beds) { if (beds.isEmpty) { return Text( diff --git a/apps/tasks/lib/components/bottom_sheet_pages/rooms_bottom_sheet.dart b/apps/tasks/lib/components/bottom_sheet_pages/rooms_bottom_sheet.dart index efe14086..0c59a73e 100644 --- a/apps/tasks/lib/components/bottom_sheet_pages/rooms_bottom_sheet.dart +++ b/apps/tasks/lib/components/bottom_sheet_pages/rooms_bottom_sheet.dart @@ -29,10 +29,12 @@ class RoomsBottomSheetPage extends StatelessWidget { children: [ Text(context.localization!.rooms, style: context.theme.textTheme.titleMedium), LoadingFutureBuilder( - future: WardService().get(id: wardId), - thenBuilder: (context, ward) { - return Text(ward.name, style: TextStyle(color: context.theme.hintColor)); - }), + future: WardService().get(id: wardId), + loadingWidget: const PulsingContainer(width: 50), + thenBuilder: (context, ward) { + return Text(ward.name, style: TextStyle(color: context.theme.hintColor)); + }, + ), ], ), trailing: BottomSheetAction( @@ -48,6 +50,7 @@ class RoomsBottomSheetPage extends StatelessWidget { children: [ LoadingAndErrorWidget( state: controller.state, + loadingWidget: const PulsingContainer(height: 300), child: RoundedListTiles( children: controller.rooms .map( diff --git a/apps/tasks/lib/components/bottom_sheet_pages/task_bottom_sheet.dart b/apps/tasks/lib/components/bottom_sheet_pages/task_bottom_sheet.dart index de40d8bc..7cc3fb10 100644 --- a/apps/tasks/lib/components/bottom_sheet_pages/task_bottom_sheet.dart +++ b/apps/tasks/lib/components/bottom_sheet_pages/task_bottom_sheet.dart @@ -14,6 +14,8 @@ import 'package:tasks/components/subtask_list.dart'; import 'package:tasks/components/visibility_select.dart'; import 'package:helpwave_service/tasks.dart'; +import '../patient_selector.dart'; + /// A private [Widget] similar to a [ListTile] that has an icon and then to text /// /// The [label] will be displayed over the [valueText] @@ -117,48 +119,50 @@ class _TaskBottomSheetState extends State { return ChangeNotifierProvider( create: (context) => TaskController(TaskWithPatient.fromTaskAndPatient(task: widget.task, patient: widget.patient)), - child: BottomSheetBase( - onClosing: () async { - // TODO do saving or something when the dialog is closed - }, + child: BottomSheetPage( header: BottomSheetHeader( title: Consumer( - builder: (context, taskController, child) => ClickableTextEdit( - initialValue: taskController.task.name, - onUpdated: taskController.changeName, - textAlign: TextAlign.center, - textStyle: TextStyle( - color: context.theme.colorScheme.primary, - fontWeight: FontWeight.bold, - fontSize: iconSizeTiny, - fontFamily: "SpaceGrotesk", - overflow: TextOverflow.ellipsis, + builder: (context, taskController, child) => LoadingAndErrorWidget( + state: taskController.state, + loadingWidget: const PulsingContainer(width: 60), + child: ClickableTextEdit( + initialValue: taskController.task.name, + onUpdated: taskController.changeName, + textAlign: TextAlign.center, + textStyle: TextStyle( + color: context.theme.colorScheme.primary, + fontWeight: FontWeight.bold, + fontSize: iconSizeTiny, + fontFamily: "SpaceGrotesk", + overflow: TextOverflow.ellipsis, + ), ), ), ), ), - bottomWidget: Consumer( - builder: (context, taskController, child) => taskController.isCreating - ? Padding( - padding: const EdgeInsets.only(top: paddingSmall), - child: Align( - alignment: Alignment.topRight, - child: FilledButton( - style: buttonStyleBig, - onPressed: taskController.isReadyForCreate - ? () { - taskController.create().then((value) { - if (value) { - Navigator.pop(context); - } - }); + bottom: Consumer( + builder: (context, taskController, child) => Visibility( + visible: taskController.isCreating, + child: Padding( + padding: const EdgeInsets.symmetric(vertical: paddingSmall), + child: Align( + alignment: Alignment.topRight, + child: FilledButton( + style: buttonStyleBig, + onPressed: taskController.isReadyForCreate + ? () { + taskController.create().then((value) { + if (value) { + Navigator.pop(context); } - : null, - child: Text(context.localization!.create), - ), - ), - ) - : const SizedBox(), + }); + } + : null, + child: Text(context.localization!.create), + ), + ), + ), + ), ), child: Flexible( child: ListView( @@ -167,32 +171,19 @@ class _TaskBottomSheetState extends State { child: Consumer(builder: // TODO move this to its own component (context, taskController, __) { - return LoadingAndErrorWidget.pulsing( + return LoadingAndErrorWidget( state: taskController.state, + loadingWidget: const PulsingContainer(width: 60), child: !taskController.isCreating ? Text(taskController.patient.name) - : LoadingFutureBuilder( - future: PatientService().getPatientList(), - loadingWidget: const PulsingContainer(), - thenBuilder: (context, patientList) { - List patients = patientList.active + patientList.unassigned; - return DropdownButton( - underline: const SizedBox(), - iconEnabledColor: context.theme.colorScheme.primary.withOpacity(0.6), - // removes the default underline - padding: EdgeInsets.zero, - hint: Text( - context.localization!.selectPatient, - style: TextStyle(color: context.theme.colorScheme.primary.withOpacity(0.6)), - ), - isDense: true, - items: patients - .map((patient) => DropdownMenuItem(value: patient, child: Text(patient.name))) - .toList(), - value: taskController.patient.isCreating ? null : taskController.patient, - onChanged: (patient) => taskController.changePatient(patient ?? PatientMinimal.empty()), - ); - }), + : PatientSelector( + initialPatientId: taskController.patient.id, + onChange: (value) { + if (value != null) { + taskController.changePatient(value); + } + }, + ), ); }), ), @@ -217,8 +208,9 @@ class _TaskBottomSheetState extends State { ), ), valueWidget: taskController.task.hasAssignee - ? LoadingAndErrorWidget.pulsing( + ? LoadingAndErrorWidget( state: taskController.assignee != null ? LoadingState.loaded : LoadingState.loading, + loadingWidget: const PulsingContainer(width: 60, height: 24), child: Text( // Never the case that we display the empty String, but the text is computed // before being displayed @@ -233,8 +225,9 @@ class _TaskBottomSheetState extends State { ); }), Consumer( - builder: (context, taskController, __) => LoadingAndErrorWidget.pulsing( + builder: (context, taskController, __) => LoadingAndErrorWidget( state: taskController.state, + loadingWidget: const PulsingContainer(width: 60, height: 24), child: _SheetListTile( icon: Icons.access_time, label: context.localization!.due, @@ -334,11 +327,6 @@ class _TaskBottomSheetState extends State { maxLines: 6, decoration: InputDecoration( contentPadding: const EdgeInsets.all(paddingMedium), - border: const OutlineInputBorder( - borderSide: BorderSide( - width: 1.0, - ), - ), hintText: context.localization!.yourNotes, ), ), @@ -347,14 +335,17 @@ class _TaskBottomSheetState extends State { const SizedBox(height: distanceBig), // TODO add callback here for task creation to update the Task accordingly Consumer( - builder: (_, taskController, __) => LoadingAndErrorWidget.pulsing( + builder: (_, taskController, __) => LoadingAndErrorWidget( state: taskController.state, + loadingWidget: const PulsingContainer(height: 200), child: SubtaskList( taskId: taskController.task.id, subtasks: taskController.task.subtasks, onChange: (subtasks) { if (taskController.task.isCreating) { taskController.task.subtasks = subtasks; + } else { + taskController.load(); } }, ), diff --git a/apps/tasks/lib/components/bottom_sheet_pages/user_bottom_sheet.dart b/apps/tasks/lib/components/bottom_sheet_pages/user_bottom_sheet.dart index f53d02a3..8527af33 100644 --- a/apps/tasks/lib/components/bottom_sheet_pages/user_bottom_sheet.dart +++ b/apps/tasks/lib/components/bottom_sheet_pages/user_bottom_sheet.dart @@ -63,43 +63,45 @@ class UserBottomSheetPage extends StatelessWidget { ), ), const SizedBox(height: distanceBig), - RoundedListTiles(children: [ - ListTile( - leading: Icon( - Icons.house_rounded, - color: context.theme.colorScheme.primary, - ), - title: Text(context.localization!.currentWard, style: const TextStyle(fontWeight: FontWeight.bold)), - trailing: Row( - mainAxisSize: MainAxisSize.min, - mainAxisAlignment: MainAxisAlignment.end, - children: [ - Consumer(builder: (context, currentWardController, __) { - return Text( - currentWardController.currentWard!.wardName, - style: context.theme.textTheme.labelLarge, - ); - }), - const Icon(Icons.chevron_right_rounded), - ], - ), - onTap: () => { - context.pushModal( - context: context, - builder: (context) => WardSelectBottomSheet( - selectedWardId: CurrentWardService().currentWard!.wardId, - onChange: (WardMinimal ward) { - CurrentWardService().currentWard = CurrentWardInformation( - ward, - CurrentWardService().currentWard!.organization, + RoundedListTiles( + children: [ + ListTile( + leading: Icon( + Icons.house_rounded, + color: context.theme.colorScheme.primary, + ), + title: Text(context.localization!.currentWard, style: const TextStyle(fontWeight: FontWeight.bold)), + trailing: Row( + mainAxisSize: MainAxisSize.min, + mainAxisAlignment: MainAxisAlignment.end, + children: [ + Consumer(builder: (context, currentWardController, __) { + return Text( + currentWardController.currentWard!.wardName, + style: context.theme.textTheme.labelLarge, ); - Navigator.pop(context); - }, - ), - ) - }, - ), - ]), + }), + const Icon(Icons.chevron_right_rounded), + ], + ), + onTap: () { + context.pushModal( + context: context, + builder: (context) => WardSelectBottomSheet( + selectedWardId: CurrentWardService().currentWard!.wardId, + onChange: (WardMinimal ward) { + CurrentWardService().currentWard = CurrentWardInformation( + ward, + CurrentWardService().currentWard!.organization, + ); + Navigator.pop(context); + }, + ), + ); + }, + ), + ], + ), const Spacer(), Padding( padding: const EdgeInsets.only(bottom: distanceMedium), diff --git a/apps/tasks/lib/components/bottom_sheet_pages/ward_bottom_sheet.dart b/apps/tasks/lib/components/bottom_sheet_pages/ward_bottom_sheet.dart index b872a72a..9050fece 100644 --- a/apps/tasks/lib/components/bottom_sheet_pages/ward_bottom_sheet.dart +++ b/apps/tasks/lib/components/bottom_sheet_pages/ward_bottom_sheet.dart @@ -29,7 +29,8 @@ class WardBottomSheetPage extends StatelessWidget { context, title: Consumer( builder: (context, controller, _) { - return LoadingAndErrorWidget.pulsing( + return LoadingAndErrorWidget( + loadingWidget: const PulsingContainer(height: 20), state: controller.state, child: Text( controller.ward.name, @@ -57,6 +58,7 @@ class WardBottomSheetPage extends StatelessWidget { TextFormFieldWithTimer( initialValue: controller.state == LoadingState.loaded ? controller.ward.name : "", onUpdate: (value) => controller.update(name: value), + maxLines: 1, ), ], ), diff --git a/apps/tasks/lib/components/patient_card.dart b/apps/tasks/lib/components/patient_card.dart index d26c63a3..28658333 100644 --- a/apps/tasks/lib/components/patient_card.dart +++ b/apps/tasks/lib/components/patient_card.dart @@ -39,7 +39,7 @@ class PatientCard extends StatelessWidget { Text( patient.name, style: const TextStyle( - // TODO set font to SpaceGrotesk + fontFamily: "SpaceGrotesk", fontWeight: FontWeight.bold, ), ), diff --git a/apps/tasks/lib/components/patient_selector.dart b/apps/tasks/lib/components/patient_selector.dart new file mode 100644 index 00000000..a6545d3c --- /dev/null +++ b/apps/tasks/lib/components/patient_selector.dart @@ -0,0 +1,40 @@ +import 'package:flutter/material.dart'; +import 'package:helpwave_localization/localization.dart'; +import 'package:helpwave_service/tasks.dart'; +import 'package:helpwave_theme/util.dart'; +import 'package:helpwave_widget/loading.dart'; + +class PatientSelector extends StatelessWidget { + final String? initialPatientId; + final void Function(PatientMinimal? value) onChange; + + const PatientSelector({super.key, this.initialPatientId, required this.onChange}); + + @override + Widget build(BuildContext context) { + return LoadingFutureBuilder( + future: PatientService().getPatientList(), + loadingWidget: const PulsingContainer(width: 60, height: 20), + thenBuilder: (context, patientList) { + List patients = patientList.active + patientList.unassigned; + return DropdownButton( + underline: const SizedBox(), + iconEnabledColor: context.theme.colorScheme.primary.withOpacity(0.6), + // removes the default underline + padding: EdgeInsets.zero, + hint: Text( + context.localization!.selectPatient, + style: TextStyle(color: context.theme.colorScheme.primary.withOpacity(0.6)), + ), + isDense: true, + items: patients.map((patient) => DropdownMenuItem(value: patient.id, child: Text(patient.name))).toList(), + value: initialPatientId, + onChanged: (value) { + if (value != null) { + onChange(patients.firstWhere((element) => element.id == value)); + } + }, + ); + }); + } +} diff --git a/apps/tasks/lib/components/subtask_list.dart b/apps/tasks/lib/components/subtask_list.dart index 77037ca6..1c652479 100644 --- a/apps/tasks/lib/components/subtask_list.dart +++ b/apps/tasks/lib/components/subtask_list.dart @@ -9,7 +9,7 @@ import 'package:provider/provider.dart'; /// A [Widget] for displaying an updating a [List] of [Subtask]s class SubtaskList extends StatelessWidget { /// The identifier of the [Task] to which all of these [Subtask]s belong - final String taskId; + final String? taskId; /// The [List] of initial subtasks final List subtasks; @@ -21,7 +21,7 @@ class SubtaskList extends StatelessWidget { const SubtaskList({ super.key, - this.taskId = "", + this.taskId, this.subtasks = const [], required this.onChange, }); @@ -41,7 +41,7 @@ class SubtaskList extends StatelessWidget { style: const TextStyle(fontWeight: FontWeight.bold, fontSize: 16), ), onAdd: () => subtasksController - .create(Subtask(id: "", name: "Subtask ${subtasksController.subtasks.length + 1}", taskId: taskId)) + .create(Subtask(name: "Subtask ${subtasksController.subtasks.length + 1}", taskId: taskId)) .then((_) => onChange(subtasksController.subtasks)), itemBuilder: (context, _, subtask) => ListTile( contentPadding: EdgeInsets.zero, diff --git a/apps/tasks/lib/components/visibility_select.dart b/apps/tasks/lib/components/visibility_select.dart index fd4494fd..4362f6a4 100644 --- a/apps/tasks/lib/components/visibility_select.dart +++ b/apps/tasks/lib/components/visibility_select.dart @@ -25,10 +25,11 @@ class _VisibilityBottomSheet extends StatelessWidget { top: paddingMedium, bottom: paddingBig, ), + mainAxisSize: MainAxisSize.min, header: BottomSheetHeader( titleText: context.localization!.visibility, ), - child: ListView( + child: Column( children: [ const SizedBox(height: distanceSmall), GestureDetector( diff --git a/apps/tasks/lib/screens/main_screen_subscreens/patient_screen.dart b/apps/tasks/lib/screens/main_screen_subscreens/patient_screen.dart index 663d89f8..3f4b1a33 100644 --- a/apps/tasks/lib/screens/main_screen_subscreens/patient_screen.dart +++ b/apps/tasks/lib/screens/main_screen_subscreens/patient_screen.dart @@ -29,7 +29,8 @@ class _PatientScreenState extends State { child: Column( children: [ Padding( - padding: const EdgeInsets.only(left: paddingSmall, right: paddingSmall, bottom: paddingMedium, top: paddingSmall), + padding: const EdgeInsets.only( + left: paddingSmall, right: paddingSmall, bottom: paddingMedium, top: paddingSmall), child: Consumer(builder: (_, patientController, __) { return SearchBar( hintText: context.localization!.searchPatient, @@ -80,20 +81,36 @@ class _PatientScreenState extends State { (patient) => Padding( padding: const EdgeInsets.symmetric(horizontal: paddingSmall), child: Dismissible( - key: Key(patient.id), + key: Key(patient.id!), + confirmDismiss: (direction) async { + if (direction == DismissDirection.endToStart) { + patientController.discharge(patient.id!); + } else if (!patient.isDischarged) { + context + .pushModal( + context: context, + builder: (context) => + TaskBottomSheet(task: Task.empty(patient.id), patient: patient)) + .then((value) => patientController.load()); + } + return false; + }, + direction: patient.isDischarged ? DismissDirection.none : DismissDirection.horizontal, background: Padding( padding: const EdgeInsets.all(paddingTiny), child: Container( - decoration: BoxDecoration( - color: context.theme.colorScheme.primary, - borderRadius: BorderRadius.circular(borderRadiusMedium), + decoration: BoxDecoration( + color: context.theme.colorScheme.primary, + borderRadius: BorderRadius.circular(borderRadiusMedium), + ), + padding: const EdgeInsets.symmetric(horizontal: paddingMedium), + child: Align( + alignment: Alignment.centerLeft, + child: Text( + context.localization!.addTask, ), - padding: const EdgeInsets.symmetric(horizontal: paddingMedium), - child: Align( - alignment: Alignment.centerLeft, - child: Text( - context.localization!.addTask, - ))), + ), + ), ), secondaryBackground: Padding( padding: const EdgeInsets.all(paddingTiny), @@ -112,24 +129,13 @@ class _PatientScreenState extends State { ), ), ), - onDismissed: (DismissDirection direction) async { - if (direction == DismissDirection.endToStart) { - patientController.discharge(patient.id); - } else { - context.pushModal( - context: context, - builder: (context) => TaskBottomSheet( - task: Task.empty(patient.id), - patient: patient, - ), - ).then((value) => patientController.load()); - } - }, child: PatientCard( - onClick: () => context.pushModal( - context: context, - builder: (context) => PatientBottomSheet(patentId: patient.id), - ).then((_) => patientController.load()), + onClick: () => context + .pushModal( + context: context, + builder: (context) => PatientBottomSheet(patentId: patient.id!), + ) + .then((_) => patientController.load()), patient: patient, margin: const EdgeInsets.symmetric(vertical: paddingTiny), ), diff --git a/apps/tasks/lib/screens/ward_select_screen.dart b/apps/tasks/lib/screens/ward_select_screen.dart index cd02f9c0..af0be9c2 100644 --- a/apps/tasks/lib/screens/ward_select_screen.dart +++ b/apps/tasks/lib/screens/ward_select_screen.dart @@ -22,7 +22,6 @@ class _WardSelectScreen extends State { @override Widget build(BuildContext context) { - return Scaffold( appBar: AppBar( title: Text(context.localization!.selectWard), diff --git a/packages/helpwave_service/lib/src/api/property/data_types/attached_property.dart b/packages/helpwave_service/lib/src/api/property/data_types/attached_property.dart index 404f947b..a2f0a123 100644 --- a/packages/helpwave_service/lib/src/api/property/data_types/attached_property.dart +++ b/packages/helpwave_service/lib/src/api/property/data_types/attached_property.dart @@ -1,6 +1,32 @@ import 'package:helpwave_service/src/api/util/copy_with_interface.dart'; +import 'package:helpwave_service/src/api/util/identified_object.dart'; +import 'package:helpwave_util/lists.dart'; import 'index.dart'; +typedef MultiSelectUpdate = ({List upsert, List remove}); + +class PropertyValueUpdate { + final String? text; + final double? number; + final bool? boolValue; + final DateTime? date; + final DateTime? dateTime; + final PropertySelectOption? singleSelect; + final MultiSelectUpdate? multiSelect; + final bool removeAll; + + const PropertyValueUpdate({ + this.text, + this.number, + this.boolValue, + this.date, + this.dateTime, + this.singleSelect, + this.multiSelect, + this.removeAll = false + }); +} + class PropertyValue { final String? text; final double? number; @@ -19,14 +45,33 @@ class PropertyValue { this.singleSelect, this.multiSelect = const [], }); + + PropertyValue copyWith(PropertyValueUpdate? update) { + if (update?.removeAll ?? false) { + return const PropertyValue(); + } + + return PropertyValue( + text: update?.text ?? text, + number: update?.number ?? number, + boolValue: update?.boolValue ?? boolValue, + date: update?.date ?? date, + dateTime: update?.dateTime ?? dateTime, + singleSelect: update?.singleSelect ?? singleSelect, + multiSelect: update?.multiSelect != null ? multiSelect.upsert(update!.multiSelect!.upsert, (a) => a.id!).where(( + element) => !update.multiSelect!.remove.any((id) => element.id == id)).toList() : multiSelect + ); + } } class AttachedPropertyUpdate { + final String? id; final String? propertyId; final String? subjectId; - final PropertyValue? value; + final PropertyValueUpdate? value; const AttachedPropertyUpdate({ + this.id, this.propertyId, this.subjectId, this.value, @@ -34,19 +79,39 @@ class AttachedPropertyUpdate { } /// The type for attaching a property -class AttachedProperty implements CopyWithInterface { +class AttachedProperty extends IdentifiedObject implements CopyWithInterface { final String propertyId; final String subjectId; final PropertyValue value; - AttachedProperty({required this.propertyId, required this.subjectId, this.value = const PropertyValue()}); + AttachedProperty({super.id, required this.propertyId, required this.subjectId, this.value = const PropertyValue()}); @override - AttachedProperty copyWith(AttachedPropertyUpdate update) { + AttachedProperty copyWith(AttachedPropertyUpdate? update) { return AttachedProperty( - propertyId: update.propertyId ?? propertyId, - subjectId: update.subjectId ?? subjectId, - value: update.value ?? value, + id: update?.id ?? id, + propertyId: update?.propertyId ?? propertyId, + subjectId: update?.subjectId ?? subjectId, + value: value.copyWith(update?.value), ); } } + +class DisplayableAttachedProperty extends AttachedProperty { + final String name; + final String description; + final PropertyFieldType fieldType; + final PropertySubjectType subjectType; + final bool isArchived; + + DisplayableAttachedProperty({ + required super.propertyId, + required super.subjectId, + super.value, + required this.name, + this.description = "", + required this.fieldType, + required this.subjectType, + this.isArchived = false, + }); +} diff --git a/packages/helpwave_service/lib/src/api/property/data_types/property.dart b/packages/helpwave_service/lib/src/api/property/data_types/property.dart index 6166e4dc..815d69d2 100644 --- a/packages/helpwave_service/lib/src/api/property/data_types/property.dart +++ b/packages/helpwave_service/lib/src/api/property/data_types/property.dart @@ -25,7 +25,7 @@ class PropertyUpdate { this.fieldType, this.isArchived, this.setId, - this.selectData, + this.selectDataUpdate, this.alwaysIncludeForViewSource, this.removeSetId, this.removeAlwaysIncludeForViewSource, @@ -58,19 +58,19 @@ class Property extends IdentifiedObject implements CopyWithInterface appendToAlwaysInclude; final List removeFromAlwaysInclude; final List appendToDontAlwaysInclude; final List removeFromDontAlwaysInclude; - PropertyViewFilterUpdate({ - required this.subjectId, + const PropertyViewFilterUpdate({ this.appendToAlwaysInclude = const [], this.removeFromAlwaysInclude = const [], this.appendToDontAlwaysInclude = const [], diff --git a/packages/helpwave_service/lib/src/api/property/data_types/select_data.dart b/packages/helpwave_service/lib/src/api/property/data_types/select_data.dart index a07bb828..d186f1db 100644 --- a/packages/helpwave_service/lib/src/api/property/data_types/select_data.dart +++ b/packages/helpwave_service/lib/src/api/property/data_types/select_data.dart @@ -26,12 +26,12 @@ class PropertySelectOption extends IdentifiedObject }); @override - copyWith(PropertySelectOptionUpdate update) { + copyWith(PropertySelectOptionUpdate? update) { return PropertySelectOption( - id: update.id ?? id, - name: update.name ?? name, - description: update.description ?? description, - isCustom: update.isCustom ?? isCustom, + id: update?.id ?? id, + name: update?.name ?? name, + description: update?.description ?? description, + isCustom: update?.isCustom ?? isCustom, ); } } @@ -50,14 +50,18 @@ class PropertySelectData implements CopyWithInterface a.id!), + isAllowingFreeText: update?.isAllowingFreeText ?? isAllowingFreeText, + options: update?.options ?? + options + .upsert(update!.upsert!, (a) => a.id!) + .where((element) => !update.removeOptions!.any((id) => id == element.id)) + .toList(), ); } } diff --git a/packages/helpwave_service/lib/src/api/property/services/property_service.dart b/packages/helpwave_service/lib/src/api/property/services/property_service.dart index 1776b4fa..c61839a2 100644 --- a/packages/helpwave_service/lib/src/api/property/services/property_service.dart +++ b/packages/helpwave_service/lib/src/api/property/services/property_service.dart @@ -9,7 +9,7 @@ import '../util/type_converter.dart'; /// /// Provides queries and requests that load or alter [Property] objects on the server /// The server is defined in the underlying [PropertyAPIServiceClients] -class PropertyService implements CRUDInterface{ +class PropertyService implements CRUDInterface { /// The GRPC ServiceClient which handles GRPC PropertyServiceClient service = PropertyAPIServiceClients().propertyServiceClient; @@ -81,16 +81,39 @@ class PropertyService implements CRUDInterface update(String id, PropertyUpdate update) async { - UpdatePropertyRequest request = UpdatePropertyRequest(id: id, name: name); - await service.updateBed( + UpdatePropertyRequest request = UpdatePropertyRequest( + id: id, + name: update.name, + description: update.description, + isArchived: update.isArchived, + subjectType: + update.subjectType != null ? PropertyGRPCTypeConverter.subjectTypeToGRPC(update.subjectType!) : null, + setId: update.setId, + selectData: update.selectDataUpdate != null + ? UpdatePropertyRequest_SelectData( + allowFreetext: update.selectDataUpdate!.isAllowingFreeText, + upsertOptions: + update.selectDataUpdate?.upsert?.map((option) => UpdatePropertyRequest_SelectData_SelectOption( + id: option.id, + name: option.name, + description: option.description, + isCustom: option.isCustom, + )), + removeOptions: update.selectDataUpdate!.removeOptions) + : null); + await service.updateProperty( request, options: CallOptions(metadata: PropertyAPIServiceClients().getMetaData()), ); + + return true; } @override diff --git a/packages/helpwave_service/lib/src/api/property/services/property_value_service.dart b/packages/helpwave_service/lib/src/api/property/services/property_value_service.dart new file mode 100644 index 00000000..d52c2764 --- /dev/null +++ b/packages/helpwave_service/lib/src/api/property/services/property_value_service.dart @@ -0,0 +1,89 @@ +import 'package:grpc/grpc.dart'; +import 'package:helpwave_proto_dart/google/protobuf/timestamp.pb.dart'; +import 'package:helpwave_proto_dart/services/property_svc/v1/property_value_svc.pbgrpc.dart'; +import 'package:helpwave_proto_dart/services/property_svc/v1/types.pb.dart'; +import 'package:helpwave_service/src/api/property/property_api_service_clients.dart'; +import 'package:helpwave_service/src/api/property/util/type_converter.dart'; +import '../../../../property.dart'; + +/// The GRPC Service for [AttachedProperty]s +/// +/// Provides queries and requests that load or alter [AttachedProperty] objects on the server +/// The server is defined in the underlying [PropertyAPIServiceClients] +class PropertyValueService { + /// The GRPC ServiceClient which handles GRPC + PropertyValueServiceClient service = PropertyAPIServiceClients().propertyValueServiceClient; + + Future> get(String id, PropertySubjectType subjectType) async { + GetAttachedPropertyValuesRequest request = GetAttachedPropertyValuesRequest(); + switch (subjectType) { + case PropertySubjectType.patient: + request.patientMatcher = PatientPropertyMatcher(patientId: id); + break; + case PropertySubjectType.task: + request.taskMatcher = TaskPropertyMatcher(wardId: id); + break; + } + + GetAttachedPropertyValuesResponse response = await service.getAttachedPropertyValues( + request, + options: CallOptions(metadata: PropertyAPIServiceClients().getMetaData()), + ); + + return response.values + .map((value) => DisplayableAttachedProperty( + propertyId: value.propertyId, + subjectId: id, + subjectType: subjectType, + name: value.name, + description: value.description, + isArchived: value.isArchived, + fieldType: PropertyGRPCTypeConverter.fieldTypeFromGRPC(value.fieldType), + value: PropertyValue( + text: value.hasTextValue() ? value.textValue : null, + number: value.hasNumberValue() ? value.numberValue : null, + boolValue: value.hasBoolValue() ? value.boolValue : null, + date: value.hasDateValue() ? value.dateValue.date.toDateTime() : null, + dateTime: value.hasDateTimeValue() ? value.dateTimeValue.toDateTime() : null, + singleSelect: value.hasSelectValue() + ? PropertySelectOption( + id: value.selectValue.id, + name: value.selectValue.name, + description: value.selectValue.description) + : null, + multiSelect: value.multiSelectValue.selectValues + .map((select) => + PropertySelectOption(id: select.id, name: select.name, description: select.description)) + .toList()), + )) + .toList(); + } + + Future attachProperty({ + required AttachedProperty property, + PropertyValueUpdate update = const PropertyValueUpdate(), + }) async { + AttachPropertyValueRequest request = AttachPropertyValueRequest( + propertyId: property.propertyId, + subjectId: property.subjectId, + textValue: update.text, + numberValue: update.number, + boolValue: update.boolValue, + dateValue: update.date != null ? Date(date: Timestamp.fromDateTime(update.date!)) : null, + dateTimeValue: update.dateTime != null ? Timestamp.fromDateTime(update.date!) : null, + selectValue: update.singleSelect?.id, + multiSelectValue: update.multiSelect != null + ? AttachPropertyValueRequest_MultiSelectValue( + selectValues: update.multiSelect!.upsert.map((e) => e.id!), + removeSelectValues: update.multiSelect!.remove) + : null, + ); + + AttachPropertyValueResponse response = await service.attachPropertyValue( + request, + options: CallOptions(metadata: PropertyAPIServiceClients().getMetaData()), + ); + + return property.copyWith(AttachedPropertyUpdate(id: response.propertyValueId, value: update)); + } +} diff --git a/packages/helpwave_service/lib/src/api/property/services/property_views_service.dart b/packages/helpwave_service/lib/src/api/property/services/property_views_service.dart new file mode 100644 index 00000000..4d4f537e --- /dev/null +++ b/packages/helpwave_service/lib/src/api/property/services/property_views_service.dart @@ -0,0 +1,38 @@ +import 'package:grpc/grpc.dart'; +import 'package:helpwave_proto_dart/services/property_svc/v1/property_value_svc.pb.dart'; +import 'package:helpwave_proto_dart/services/property_svc/v1/property_views_svc.pbgrpc.dart'; +import 'package:helpwave_service/src/api/property/data_types/property_view_filter_update.dart'; +import 'package:helpwave_service/src/api/property/property_api_service_clients.dart'; +import '../../../../property.dart'; + +/// The GRPC Service for [PropertyViewRules]s +/// +/// Provides queries and requests that load or alter [PropertyViewRules] objects on the server +/// The server is defined in the underlying [PropertyAPIServiceClients] +class PropertyValueService { + /// The GRPC ServiceClient which handles GRPC + PropertyViewsServiceClient service = PropertyAPIServiceClients().propertyViewsServiceClient; + + Future updateViewRule({ + required PropertySubjectType subjectType, + required String id, + PropertyViewFilterUpdate? update = const PropertyViewFilterUpdate(), + }) async { + UpdatePropertyViewRuleRequest request = UpdatePropertyViewRuleRequest(); + switch (subjectType) { + case PropertySubjectType.patient: + request.patientMatcher = PatientPropertyMatcher(patientId: id); + break; + case PropertySubjectType.task: + request.taskMatcher = TaskPropertyMatcher(wardId: id); + break; + } + + await service.updatePropertyViewRule( + request, + options: CallOptions(metadata: PropertyAPIServiceClients().getMetaData()), + ); + + return true; + } +} diff --git a/packages/helpwave_service/lib/src/api/tasks/controllers/patient_controller.dart b/packages/helpwave_service/lib/src/api/tasks/controllers/patient_controller.dart index 9bfa732e..f3355cb9 100644 --- a/packages/helpwave_service/lib/src/api/tasks/controllers/patient_controller.dart +++ b/packages/helpwave_service/lib/src/api/tasks/controllers/patient_controller.dart @@ -1,6 +1,5 @@ import 'package:helpwave_util/loading.dart'; import 'package:helpwave_service/src/api/tasks/index.dart'; -import 'package:logger/logger.dart'; /// The Controller for managing [Patient]s in a Ward class PatientController extends LoadingChangeNotifier { @@ -19,19 +18,16 @@ class PatientController extends LoadingChangeNotifier { get isCreating => _patient.isCreating; PatientController(this._patient) { - if (!_patient.isCreating) { - load(); - } + load(); } /// A function to load the [Patient] Future load() async { - if (isCreating) { - Logger().w("PatientController.load should not be called when the patient has not been created"); - return; - } loadPatient() async { - patient = await PatientService().getPatientDetails(patientId: patient.id); + if (isCreating) { + return; + } + patient = await PatientService().getPatientDetails(patientId: patient.id!); } loadHandler( @@ -42,11 +38,17 @@ class PatientController extends LoadingChangeNotifier { /// Unassigns the [Patient] from their [Bed] Future unassign() async { unassignPatient() async { - await PatientService().unassignPatient(patientId: patient.id).then((value) { + if(patient.isCreating) { final patientCopy = patient.copyWith(); patientCopy.bed = null; patientCopy.room = null; - patient = patientCopy; + _patient = patientCopy; + } + await PatientService().unassignPatient(patientId: patient.id!).then((value) { + final patientCopy = patient.copyWith(); + patientCopy.bed = null; + patientCopy.room = null; + _patient = patientCopy; }); } @@ -55,8 +57,9 @@ class PatientController extends LoadingChangeNotifier { /// Discharges the [Patient] Future discharge() async { + assert(!patient.isCreating, "You can only discharge created patients"); dischargePatient() async { - await PatientService().dischargePatient(patientId: patient.id).then((value) { + await PatientService().dischargePatient(patientId: patient.id!).then((value) { final patientCopy = patient.copyWith(isDischarged: true); patientCopy.bed = null; patientCopy.room = null; @@ -75,7 +78,7 @@ class PatientController extends LoadingChangeNotifier { return; } assignPatientToBed() async { - await PatientService().assignBed(patientId: patient.id, bedId: bed.id).then((value) { + await PatientService().assignBed(patientId: patient.id!, bedId: bed.id).then((value) { patient = patient.copyWith(bed: bed, room: room); }); } @@ -85,12 +88,12 @@ class PatientController extends LoadingChangeNotifier { /// Change the name of the [Patient] Future changeName(String name) async { - if(isCreating){ + if (isCreating) { patient.name = name; return; } updateName() async { - await PatientService().updatePatient(id: patient.id, name: name).then((_) { + await PatientService().updatePatient(id: patient.id!, name: name).then((_) { patient.name = name; }); } @@ -100,12 +103,12 @@ class PatientController extends LoadingChangeNotifier { /// Change the notes of the [Patient] Future changeNotes(String notes) async { - if(isCreating){ + if (isCreating) { patient.notes = notes; return; } updateNotes() async { - await PatientService().updatePatient(id: patient.id, notes: notes).then((_) { + await PatientService().updatePatient(id: patient.id!, notes: notes).then((_) { patient.notes = notes; }); } diff --git a/packages/helpwave_service/lib/src/api/tasks/controllers/subtask_list_controller.dart b/packages/helpwave_service/lib/src/api/tasks/controllers/subtask_list_controller.dart index a0b8e98c..6038a971 100644 --- a/packages/helpwave_service/lib/src/api/tasks/controllers/subtask_list_controller.dart +++ b/packages/helpwave_service/lib/src/api/tasks/controllers/subtask_list_controller.dart @@ -1,5 +1,6 @@ import 'dart:async'; import 'package:helpwave_service/src/api/tasks/index.dart'; +import 'package:helpwave_util/lists.dart'; import 'package:helpwave_util/loading.dart'; /// The Controller for managing [Subtask]s in a [Task] @@ -18,22 +19,20 @@ class SubtasksController extends LoadingChangeNotifier { notifyListeners(); } - bool get isCreating => taskId == null || taskId!.isEmpty; + bool get isCreating => taskId == null; String? taskId; - SubtasksController({this.taskId = "", List? subtasks}) { - if (!isCreating) { - load(); - } + SubtasksController({this.taskId, List? subtasks}) { + load(); } /// Loads a [Task] Future load() async { - if (isCreating) { - return; - } loadTask() async { + if (isCreating) { + return; + } final task = await TaskService().getTask(id: taskId); subtasks = task.subtasks; } @@ -43,7 +42,7 @@ class SubtasksController extends LoadingChangeNotifier { /// Delete the [Subtask] by its index int the list Future deleteByIndex(int index) async { - if (index < 0 || index >= subtasks.length) { + if (!subtasks.isIndexValid(index)) { return; } if (isCreating) { @@ -51,7 +50,7 @@ class SubtasksController extends LoadingChangeNotifier { notifyListeners(); return; } - await deleteById(subtasks[index].id); + await deleteById(subtasks[index].id!); } /// Delete the [Subtask] by the id diff --git a/packages/helpwave_service/lib/src/api/tasks/controllers/task_controller.dart b/packages/helpwave_service/lib/src/api/tasks/controllers/task_controller.dart index 2eae7924..06d4f7b9 100644 --- a/packages/helpwave_service/lib/src/api/tasks/controllers/task_controller.dart +++ b/packages/helpwave_service/lib/src/api/tasks/controllers/task_controller.dart @@ -9,9 +9,7 @@ class TaskController extends LoadingChangeNotifier { TaskWithPatient _task; TaskController(this._task) { - if (!_task.isCreating) { load(); - } } TaskWithPatient get task => _task; @@ -37,6 +35,9 @@ class TaskController extends LoadingChangeNotifier { /// A function to load the [Task] load() async { loadTask() async { + if (_task.isCreating) { + return; + } await TaskService().getTask(id: task.id).then((value) async { task = value; if (task.hasAssignee) { @@ -57,7 +58,7 @@ class TaskController extends LoadingChangeNotifier { return; } changeAssigneeFuture() async { - await TaskService().changeAssignee(taskId: task.id, userId: user?.id).then((value) { + await TaskService().changeAssignee(taskId: task.id!, userId: user?.id).then((value) { task.assigneeId = user?.id; _assignee = user; }); @@ -73,7 +74,7 @@ class TaskController extends LoadingChangeNotifier { return; } updateName() async { - await TaskService().updateTask(taskId: task.id, name: name).then( + await TaskService().updateTask(taskId: task.id!, name: name).then( (_) => task = TaskWithPatient.fromTaskAndPatient(task: task.copyWith(name: name), patient: task.patient)); } @@ -87,7 +88,7 @@ class TaskController extends LoadingChangeNotifier { return; } updateIsPublic() async { - await TaskService().updateTask(taskId: task.id, isPublic: isPublic).then((_) => task = + await TaskService().updateTask(taskId: task.id!, isPublic: isPublic).then((_) => task = TaskWithPatient.fromTaskAndPatient(task: task.copyWith(isPublicVisible: isPublic), patient: task.patient)); } @@ -101,7 +102,7 @@ class TaskController extends LoadingChangeNotifier { return; } updateNotes() async { - await TaskService().updateTask(taskId: task.id, notes: notes).then( + await TaskService().updateTask(taskId: task.id!, notes: notes).then( (_) => task = TaskWithPatient.fromTaskAndPatient(task: task.copyWith(notes: notes), patient: task.patient)); } @@ -115,12 +116,12 @@ class TaskController extends LoadingChangeNotifier { return; } updateDueDate() async { - await TaskService().updateTask(taskId: task.id, dueDate: dueDate).then((_) => + await TaskService().updateTask(taskId: task.id!, dueDate: dueDate).then((_) => task = TaskWithPatient.fromTaskAndPatient(task: task.copyWith(dueDate: dueDate), patient: task.patient)); } removeDueDate() async { - await TaskService().removeDueDate(taskId: task.id).then((_) => + await TaskService().removeDueDate(taskId: task.id!).then((_) => task = TaskWithPatient.fromTaskAndPatient(task: task.copyWith(dueDate: dueDate), patient: task.patient)); } @@ -137,7 +138,7 @@ class TaskController extends LoadingChangeNotifier { /// Creates the Task and returns Future create() async { - assert(!isReadyForCreate, "A the patient must be set to create a task"); + assert(isReadyForCreate, "A the patient must be set to create a task"); createTask() async { await TaskService().createTask(task).then((value) { task.copyWith(id: value); diff --git a/packages/helpwave_service/lib/src/api/tasks/data_types/patient.dart b/packages/helpwave_service/lib/src/api/tasks/data_types/patient.dart index 9a4adc94..caaeca1b 100644 --- a/packages/helpwave_service/lib/src/api/tasks/data_types/patient.dart +++ b/packages/helpwave_service/lib/src/api/tasks/data_types/patient.dart @@ -1,17 +1,15 @@ import 'package:helpwave_service/src/api/tasks/index.dart'; +import 'package:helpwave_service/src/api/util/identified_object.dart'; enum PatientAssignmentStatus { active, unassigned, discharged, all } -class PatientMinimal { - String id; +class PatientMinimal extends IdentifiedObject { String name; - factory PatientMinimal.empty() => PatientMinimal(id: "", name: ""); - - bool get isCreating => id == ""; + factory PatientMinimal.empty() => PatientMinimal(name: ""); PatientMinimal({ - required this.id, + super.id, required this.name, }); @@ -33,10 +31,7 @@ class PatientMinimal { @override String toString() { - if (isCreating) { - return "PatientMinimal"; - } - return "PatientMinimal<$id, $name>"; + return "$runtimeType{id: $id, name: $name}"; } } @@ -136,6 +131,7 @@ class PatientsByAssignmentStatus { List active; List unassigned; List discharged; + List get all => active + unassigned + discharged; PatientsByAssignmentStatus({ diff --git a/packages/helpwave_service/lib/src/api/tasks/data_types/subtask.dart b/packages/helpwave_service/lib/src/api/tasks/data_types/subtask.dart index 956d235c..0c706783 100644 --- a/packages/helpwave_service/lib/src/api/tasks/data_types/subtask.dart +++ b/packages/helpwave_service/lib/src/api/tasks/data_types/subtask.dart @@ -1,13 +1,13 @@ +import 'package:helpwave_service/src/api/util/identified_object.dart'; + /// Data class for a [Subtask] -class Subtask { - final String id; - final String taskId; +class Subtask extends IdentifiedObject { + final String? taskId; String name; bool isDone; - bool get isCreating => id == ""; - Subtask({required this.id, required this.taskId, required this.name, this.isDone = false}); + Subtask({super.id, required this.taskId, required this.name, this.isDone = false}) : assert(id == null || taskId != null); /// Create a copy of the [Subtask] Subtask copyWith({ diff --git a/packages/helpwave_service/lib/src/api/tasks/data_types/task.dart b/packages/helpwave_service/lib/src/api/tasks/data_types/task.dart index a8b09b82..04f6a63e 100644 --- a/packages/helpwave_service/lib/src/api/tasks/data_types/task.dart +++ b/packages/helpwave_service/lib/src/api/tasks/data_types/task.dart @@ -1,5 +1,6 @@ import 'package:helpwave_service/src/api/tasks/data_types/patient.dart'; import 'package:helpwave_service/src/api/tasks/data_types/subtask.dart'; +import 'package:helpwave_service/src/api/util/identified_object.dart'; enum TaskStatus { unspecified, @@ -9,8 +10,7 @@ enum TaskStatus { } /// data class for [Task] -class Task { - final String id; +class Task extends IdentifiedObject { String name; String? assigneeId; String notes; @@ -20,9 +20,9 @@ class Task { final DateTime? creationDate; final String? createdBy; bool isPublicVisible; - final String patientId; + final String? patientId; - factory Task.empty(String patientId) => Task(id: "", name: "name", notes: "", patientId: patientId); + factory Task.empty(String? patientId) => Task(name: "name", notes: "", patientId: patientId); final _nullID = "00000000-0000-0000-0000-000000000000"; @@ -39,12 +39,10 @@ class Task { bool get inNextHour => remainingTime.inHours < 1; - bool get isCreating => id == ""; - bool get hasAssignee => assigneeId != null && assigneeId != "" && assigneeId != _nullID; Task({ - required this.id, + super.id, required this.name, required this.notes, this.assigneeId, @@ -84,13 +82,18 @@ class Task { patientId: patientId ?? this.patientId, ); } + + @override + String toString() { + return "{id: $id, name: $name, description: $notes, subtasks: $subtasks, patientId: $patientId}"; + } } class TaskWithPatient extends Task { final PatientMinimal patient; factory TaskWithPatient.empty({ - String taskId = "", + String? taskId, PatientMinimal? patient, }) { return TaskWithPatient( @@ -98,7 +101,7 @@ class TaskWithPatient extends Task { name: "task name", notes: "", patient: patient ?? PatientMinimal.empty(), - patientId: patient?.id ?? "", + patientId: patient?.id, ); } @@ -118,7 +121,7 @@ class TaskWithPatient extends Task { creationDate: task.creationDate, assigneeId: task.assigneeId, patient: patient ?? PatientMinimal.empty(), - patientId: patient?.id ?? "", + patientId: patient?.id, ); } diff --git a/packages/helpwave_service/lib/src/api/tasks/offline_clients/bed_offline_client.dart b/packages/helpwave_service/lib/src/api/tasks/offline_clients/bed_offline_client.dart index 3dc48e55..20e61709 100644 --- a/packages/helpwave_service/lib/src/api/tasks/offline_clients/bed_offline_client.dart +++ b/packages/helpwave_service/lib/src/api/tasks/offline_clients/bed_offline_client.dart @@ -57,7 +57,7 @@ class BedOfflineService { valueStore.beds = valueStore.beds.where((value) => value.id != bedId).toList(); final patient = OfflineClientStore().patientStore.findPatientByBed(bedId); if(patient != null){ - OfflineClientStore().patientStore.unassignBed(patient.id); + OfflineClientStore().patientStore.unassignBed(patient.id!); } } } diff --git a/packages/helpwave_service/lib/src/api/tasks/offline_clients/patient_offline_client.dart b/packages/helpwave_service/lib/src/api/tasks/offline_clients/patient_offline_client.dart index 32627586..577d3c25 100644 --- a/packages/helpwave_service/lib/src/api/tasks/offline_clients/patient_offline_client.dart +++ b/packages/helpwave_service/lib/src/api/tasks/offline_clients/patient_offline_client.dart @@ -100,7 +100,7 @@ class PatientOfflineService { valueStore.patients = valueStore.patients.where((value) => value.id != patientId).toList(); final tasks = OfflineClientStore().taskStore.findTasks(patientId); for (var task in tasks) { - OfflineClientStore().taskStore.delete(task.id); + OfflineClientStore().taskStore.delete(task.id!); } } } @@ -337,7 +337,7 @@ class PatientOfflineClient extends PatientServiceClient { OfflineClientStore().patientStore.create(newPatient); - final response = CreatePatientResponse()..id = newPatient.id; + final response = CreatePatientResponse()..id = newPatient.id!; return MockResponseFuture.value(response); } diff --git a/packages/helpwave_service/lib/src/api/tasks/offline_clients/task_offline_client.dart b/packages/helpwave_service/lib/src/api/tasks/offline_clients/task_offline_client.dart index 75c84626..238d4fdd 100644 --- a/packages/helpwave_service/lib/src/api/tasks/offline_clients/task_offline_client.dart +++ b/packages/helpwave_service/lib/src/api/tasks/offline_clients/task_offline_client.dart @@ -134,7 +134,7 @@ class TaskOfflineService { final valueStore = OfflineClientStore().taskStore; valueStore.tasks = valueStore.tasks.where((value) => value.id != taskId).toList(); OfflineClientStore().subtaskStore.findSubtasks(taskId).forEach((subtask) { - OfflineClientStore().subtaskStore.delete(subtask.id); + OfflineClientStore().subtaskStore.delete(subtask.id!); }); } } @@ -196,7 +196,7 @@ class TaskOfflineClient extends TaskServiceClient { throw "Task with task id ${request.id} not found"; } - final patient = OfflineClientStore().patientStore.findPatient(task.patientId); + final patient = task.patientId == null ? null : OfflineClientStore().patientStore.findPatient(task.patientId!); if (patient == null) { throw "Inconsistency error: Patient with patient id ${task.patientId} not found"; } @@ -275,7 +275,7 @@ class TaskOfflineClient extends TaskServiceClient { done: subtask.isDone, )), ); - final patient = OfflineClientStore().patientStore.findPatient(task.patientId); + final patient = task.patientId == null ? null : OfflineClientStore().patientStore.findPatient(task.patientId!); if (patient == null) { throw "Inconsistency error: patient with id ${task.patientId} not found"; } @@ -352,7 +352,7 @@ class TaskOfflineClient extends TaskServiceClient { )); } - final response = CreateTaskResponse()..id = newTask.id; + final response = CreateTaskResponse()..id = newTask.id!; return MockResponseFuture.value(response); } @@ -417,7 +417,7 @@ class TaskOfflineClient extends TaskServiceClient { isDone: request.subtask.done); OfflineClientStore().subtaskStore.create(subtask); - final response = CreateSubtaskResponse()..subtaskId = subtask.id; + final response = CreateSubtaskResponse()..subtaskId = subtask.id!; return MockResponseFuture.value(response); } diff --git a/packages/helpwave_service/lib/src/api/tasks/offline_clients/template_offline_client.dart b/packages/helpwave_service/lib/src/api/tasks/offline_clients/template_offline_client.dart index 53d94cc9..f6359fb4 100644 --- a/packages/helpwave_service/lib/src/api/tasks/offline_clients/template_offline_client.dart +++ b/packages/helpwave_service/lib/src/api/tasks/offline_clients/template_offline_client.dart @@ -63,7 +63,7 @@ class TaskTemplateOfflineService { void delete(String taskId) { taskTemplates = taskTemplates.where((value) => value.id != taskId).toList(); OfflineClientStore().taskTemplateSubtaskStore.findTemplateSubtasks(taskId).forEach((templateSubtask) { - OfflineClientStore().taskTemplateSubtaskStore.delete(templateSubtask.id); + OfflineClientStore().taskTemplateSubtaskStore.delete(templateSubtask.id!); }); } } @@ -215,7 +215,7 @@ class TaskTemplateOfflineClient extends TaskTemplateServiceClient { ); OfflineClientStore().taskTemplateSubtaskStore.create(templateSubtask); - final response = CreateTaskTemplateSubTaskResponse()..id = templateSubtask.id; + final response = CreateTaskTemplateSubTaskResponse()..id = templateSubtask.id!; return MockResponseFuture.value(response); } diff --git a/packages/helpwave_service/lib/src/api/util/copy_with_interface.dart b/packages/helpwave_service/lib/src/api/util/copy_with_interface.dart index 057810c5..9514e667 100644 --- a/packages/helpwave_service/lib/src/api/util/copy_with_interface.dart +++ b/packages/helpwave_service/lib/src/api/util/copy_with_interface.dart @@ -1,3 +1,3 @@ abstract class CopyWithInterface { - T copyWith(U update); + T copyWith(U? update); } diff --git a/packages/helpwave_service/lib/src/api/util/identified_object.dart b/packages/helpwave_service/lib/src/api/util/identified_object.dart index 3cb522b9..5752a1ff 100644 --- a/packages/helpwave_service/lib/src/api/util/identified_object.dart +++ b/packages/helpwave_service/lib/src/api/util/identified_object.dart @@ -9,4 +9,9 @@ class IdentifiedObject { bool isReferencingSame(IdentifiedObject other) { return runtimeType == other.runtimeType && this.id == other.id; } + + @override + String toString() { + return "$runtimeType{id: $id}"; + } } diff --git a/packages/helpwave_service/lib/src/auth/identity.dart b/packages/helpwave_service/lib/src/auth/identity.dart index 91753ce4..f79826d1 100644 --- a/packages/helpwave_service/lib/src/auth/identity.dart +++ b/packages/helpwave_service/lib/src/auth/identity.dart @@ -35,11 +35,11 @@ class Identity { "eyJzdWIiOiIxODE1OTcxMy01ZDRlLTRhZDUtOTRhZC1mYmI2YmIxNDc5ODQiLCJuYW1lIjoiTWF4IE11c3Rlcm1hbm4iLCJuaWNrbmFt" "ZSI6Im1heC5tdXN0ZXJtYW5uIiwiZW1haWwiOiJtYXgubXVzdGVybWFubkBoZWxwd2F2ZS5kZSIsIm9yZ2FuaXphdGlvbnMiOlsiM2IyNWM2ZjUtNDcwNS00MDc0LTlmYzYtYTUwYzI4ZWJhNDA2Il19", // TODO add a default here - id: "", + id: "bb560b25-45b6-4a84-87f1-408e8c32dec8", name: "Max Mustermann", nickName: "Max M.", email: "max.mustermann@helpwave.de", - organizations: ['3b25c6f5-4705-4074-9fc6-a50c28eba406']); + organizations: ['bb560b25-45b6-4a84-87f1-408e8c32dec8']); } Identity({ diff --git a/packages/helpwave_widget/lib/src/bottom_sheets/bottom_sheet_base.dart b/packages/helpwave_widget/lib/src/bottom_sheets/bottom_sheet_base.dart index 6894b5aa..0e93fd56 100644 --- a/packages/helpwave_widget/lib/src/bottom_sheets/bottom_sheet_base.dart +++ b/packages/helpwave_widget/lib/src/bottom_sheets/bottom_sheet_base.dart @@ -193,7 +193,8 @@ class BottomSheetHeader extends StatelessWidget { this.padding = const EdgeInsets.only(bottom: paddingSmall), }); - factory BottomSheetHeader.navigation(BuildContext context, { + factory BottomSheetHeader.navigation( + BuildContext context, { BottomSheetAction? trailing, Widget? title, String? titleText, diff --git a/packages/helpwave_widget/lib/src/text_input/clickable_text_edit.dart b/packages/helpwave_widget/lib/src/text_input/clickable_text_edit.dart index c3742ab7..906e8ea0 100644 --- a/packages/helpwave_widget/lib/src/text_input/clickable_text_edit.dart +++ b/packages/helpwave_widget/lib/src/text_input/clickable_text_edit.dart @@ -114,6 +114,7 @@ class _ClickableTextEditState extends State { textAlign: widget.textAlign, focusNode: _focusNode, autofocus: true, + textInputAction: TextInputAction.done, ) : GestureDetector( onTap: () => setState(() { diff --git a/packages/helpwave_widget/lib/src/text_input/text_form_field_with_timer.dart b/packages/helpwave_widget/lib/src/text_input/text_form_field_with_timer.dart index e0a4d8af..342df680 100644 --- a/packages/helpwave_widget/lib/src/text_input/text_form_field_with_timer.dart +++ b/packages/helpwave_widget/lib/src/text_input/text_form_field_with_timer.dart @@ -13,20 +13,21 @@ class TextFormFieldWithTimer extends StatefulWidget { final InputDecoration? decoration; final FocusNode? focusNode; final bool autofocus; + final TextInputAction? textInputAction; - const TextFormFieldWithTimer({ - super.key, - required this.initialValue, - this.onChanged, - this.onUpdate, - this.timerDuration = const Duration(seconds: 3), - this.style, - this.textAlign = TextAlign.left, - this.maxLines, - this.decoration, - this.focusNode, - this.autofocus = false, - }); + const TextFormFieldWithTimer( + {super.key, + required this.initialValue, + this.onChanged, + this.onUpdate, + this.timerDuration = const Duration(seconds: 3), + this.style, + this.textAlign = TextAlign.left, + this.maxLines, + this.decoration, + this.focusNode, + this.autofocus = false, + this.textInputAction}); @override State createState() => _TextFormFieldWithTimerState(); @@ -60,7 +61,7 @@ class _TextFormFieldWithTimerState extends State { @override void dispose() { - if(widget.focusNode == null){ + if (widget.focusNode == null) { _focusNode.dispose(); } valueUpdater?.dispose(); @@ -83,6 +84,7 @@ class _TextFormFieldWithTimerState extends State { maxLines: widget.maxLines, decoration: widget.decoration, autofocus: widget.autofocus, + textInputAction: widget.textInputAction ?? (widget.maxLines == 1 ? TextInputAction.done : null), ); } } From 20e9857e01078e0b73e401d7cea2b3e26a66e621 Mon Sep 17 00:00:00 2001 From: Felix Thape Date: Mon, 21 Oct 2024 13:29:08 +0200 Subject: [PATCH 19/36] feat: add properties screen --- .../organization_bottom_sheet.dart | 3 +- .../properties_bottom_sheet.dart | 75 +++++++++++++++++++ .../bottom_sheet_pages/ward_bottom_sheet.dart | 3 +- apps/tasks/lib/main.dart | 4 + apps/tasks/lib/screens/settings_screen.dart | 3 + .../lib/util/subject_type_translations.dart | 22 ++++++ .../helpwave_localization/lib/l10n/app_de.arb | 9 ++- .../helpwave_localization/lib/l10n/app_en.arb | 9 ++- .../src/api/property/data_types/index.dart | 1 + .../lib/src/api/property/index.dart | 2 + .../lib/src/api/property/services/index.dart | 3 + .../services/property_views_service.dart | 3 +- .../lib/src/auth/identity.dart | 7 +- 13 files changed, 135 insertions(+), 9 deletions(-) create mode 100644 apps/tasks/lib/components/bottom_sheet_pages/properties_bottom_sheet.dart create mode 100644 apps/tasks/lib/util/subject_type_translations.dart create mode 100644 packages/helpwave_service/lib/src/api/property/services/index.dart diff --git a/apps/tasks/lib/components/bottom_sheet_pages/organization_bottom_sheet.dart b/apps/tasks/lib/components/bottom_sheet_pages/organization_bottom_sheet.dart index ba7e07e6..d663e745 100644 --- a/apps/tasks/lib/components/bottom_sheet_pages/organization_bottom_sheet.dart +++ b/apps/tasks/lib/components/bottom_sheet_pages/organization_bottom_sheet.dart @@ -11,6 +11,7 @@ import 'package:helpwave_widget/text_input.dart'; import 'package:helpwave_widget/widgets.dart'; import 'package:provider/provider.dart'; import 'package:tasks/components/bottom_sheet_pages/organization_members_bottom_sheet.dart'; +import 'package:tasks/components/bottom_sheet_pages/properties_bottom_sheet.dart'; import 'package:tasks/components/bottom_sheet_pages/wards_bottom_sheet_page.dart'; import 'package:tasks/screens/settings_screen.dart'; @@ -88,7 +89,7 @@ class OrganizationBottomSheetPage extends StatelessWidget { icon: Icons.label, title: context.localization!.properties, onTap: () { - // TODO navigate to properties page + NavigationStackController.of(context).push(const PropertiesBottomSheet()); }, ) ], diff --git a/apps/tasks/lib/components/bottom_sheet_pages/properties_bottom_sheet.dart b/apps/tasks/lib/components/bottom_sheet_pages/properties_bottom_sheet.dart new file mode 100644 index 00000000..80fad572 --- /dev/null +++ b/apps/tasks/lib/components/bottom_sheet_pages/properties_bottom_sheet.dart @@ -0,0 +1,75 @@ +import 'package:flutter/material.dart'; +import 'package:helpwave_localization/localization.dart'; +import 'package:helpwave_service/property.dart'; +import 'package:helpwave_service/tasks.dart'; +import 'package:helpwave_theme/constants.dart'; +import 'package:helpwave_theme/util.dart'; +import 'package:helpwave_widget/bottom_sheets.dart'; +import 'package:helpwave_widget/lists.dart'; +import 'package:helpwave_widget/loading.dart'; +import 'package:tasks/screens/settings_screen.dart'; + +import '../../util/subject_type_translations.dart'; + +/// A [BottomSheet] for showing a [Property] information +class PropertiesBottomSheet extends StatelessWidget { + final String? wardId; + + const PropertiesBottomSheet({super.key, this.wardId}); + + // TODO add the possibility of a ward + + @override + Widget build(BuildContext context) { + return BottomSheetPage( + header: BottomSheetHeader.navigation( + context, + title: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Text(context.localization!.properties, style: context.theme.textTheme.titleMedium), + wardId == null + ? const SizedBox() + : LoadingFutureBuilder( + future: WardService().get(id: wardId!), + loadingWidget: const PulsingContainer( + width: 60, + ), + thenBuilder: (context, data) => Text(data.name, style: TextStyle(color: context.theme.hintColor)), + ) + ], + ), + trailing: BottomSheetAction( + icon: Icons.add, + onPressed: () { + // TODO open property create screen + }, + ), + ), + child: Flexible( + child: LoadingFutureBuilder( + future: PropertyService().getMany(), + thenBuilder: (context, properties) { + return ListView( + padding: const EdgeInsets.only(top: paddingSmall, bottom: paddingMedium), + children: [ + RoundedListTiles( + children: properties + .map((property) => NavigationListTile( + icon: Icons.label, + title: property.name, + subtitle: propertyFieldTypeTranslations(context, property.fieldType), + onTap: () { + // TODO navigate + }, + )) + .toList(), + ) + ], + ); + }, + ), + ), + ); + } +} diff --git a/apps/tasks/lib/components/bottom_sheet_pages/ward_bottom_sheet.dart b/apps/tasks/lib/components/bottom_sheet_pages/ward_bottom_sheet.dart index 9050fece..b17c44ec 100644 --- a/apps/tasks/lib/components/bottom_sheet_pages/ward_bottom_sheet.dart +++ b/apps/tasks/lib/components/bottom_sheet_pages/ward_bottom_sheet.dart @@ -11,6 +11,7 @@ import 'package:helpwave_widget/navigation.dart'; import 'package:helpwave_widget/text_input.dart'; import 'package:helpwave_widget/widgets.dart'; import 'package:provider/provider.dart'; +import 'package:tasks/components/bottom_sheet_pages/properties_bottom_sheet.dart'; import 'package:tasks/components/bottom_sheet_pages/rooms_bottom_sheet.dart'; import 'package:tasks/components/bottom_sheet_pages/task_templates_bottom_sheet.dart'; import 'package:tasks/screens/settings_screen.dart'; @@ -88,7 +89,7 @@ class WardBottomSheetPage extends StatelessWidget { icon: Icons.label, title: context.localization!.properties, onTap: () { - // TODO navigate to properties page + NavigationStackController.of(context).push(PropertiesBottomSheet(wardId: wardId)); }, ) ], diff --git a/apps/tasks/lib/main.dart b/apps/tasks/lib/main.dart index 2914d8dd..bcdfa822 100644 --- a/apps/tasks/lib/main.dart +++ b/apps/tasks/lib/main.dart @@ -1,5 +1,6 @@ import 'package:flutter/material.dart'; import 'package:helpwave_service/auth.dart'; +import 'package:helpwave_service/property.dart'; import 'package:helpwave_service/user.dart'; import 'package:provider/provider.dart'; import 'package:helpwave_localization/l10n/app_localizations.dart'; @@ -19,6 +20,9 @@ void main() { UserAPIServiceClients() ..apiUrl = usedAPIURL ..offlineMode = isUsingOfflineClients; + PropertyAPIServiceClients().apiUrl = usedAPIURL; + // OfflineMode for PropertyAPIServiceClients does not work yet + // ..offlineMode = isUsingOfflineClients; UserSessionService().changeMode(isUsingDevModeLogin); runApp(const MyApp()); } diff --git a/apps/tasks/lib/screens/settings_screen.dart b/apps/tasks/lib/screens/settings_screen.dart index d3dce295..5556fa9b 100644 --- a/apps/tasks/lib/screens/settings_screen.dart +++ b/apps/tasks/lib/screens/settings_screen.dart @@ -155,6 +155,7 @@ class NavigationListTile extends StatelessWidget { final IconData icon; final Color? color; final String title; + final String? subtitle; final void Function() onTap; final String? trailingText; @@ -163,6 +164,7 @@ class NavigationListTile extends StatelessWidget { required this.icon, this.color, required this.title, + this.subtitle, required this.onTap, this.trailingText, }); @@ -176,6 +178,7 @@ class NavigationListTile extends StatelessWidget { ), onTap: onTap, title: Text(title, style: TextStyle(fontWeight: FontWeight.bold, color: color)), + subtitle: subtitle != null ? Text(subtitle!) : null, trailing: Row( mainAxisSize: MainAxisSize.min, mainAxisAlignment: MainAxisAlignment.end, diff --git a/apps/tasks/lib/util/subject_type_translations.dart b/apps/tasks/lib/util/subject_type_translations.dart new file mode 100644 index 00000000..146d9486 --- /dev/null +++ b/apps/tasks/lib/util/subject_type_translations.dart @@ -0,0 +1,22 @@ +import 'package:flutter/cupertino.dart'; +import 'package:helpwave_localization/localization.dart'; +import 'package:helpwave_service/property.dart'; + +String propertyFieldTypeTranslations(BuildContext context, PropertyFieldType fieldType) { + switch(fieldType){ + case PropertyFieldType.text: + return context.localization!.freeText; + case PropertyFieldType.number: + return context.localization!.number; + case PropertyFieldType.bool: + return context.localization!.checkBox; + case PropertyFieldType.date: + return context.localization!.datePicker; + case PropertyFieldType.dateTime: + return context.localization!.dateTimePicker; + case PropertyFieldType.singleSelect: + return context.localization!.select; + case PropertyFieldType.multiSelect: + return context.localization!.multiSelect; + } +} diff --git a/packages/helpwave_localization/lib/l10n/app_de.arb b/packages/helpwave_localization/lib/l10n/app_de.arb index 744e734c..10ba7365 100644 --- a/packages/helpwave_localization/lib/l10n/app_de.arb +++ b/packages/helpwave_localization/lib/l10n/app_de.arb @@ -193,5 +193,12 @@ "new_": "Neu", "newRoom": "Neuer Raum", "newBed": "Neues Bett", - "taskTemplates": "Task Vorlagen" + "taskTemplates": "Task Vorlagen", + "datePicker": "Tag Picker", + "dateTimePicker": "Tag und Zeit Picker", + "checkBox": "Checkbox", + "number": "Zahlenwert", + "freeText": "Freitext", + "select": "Single-Select", + "multiSelect": "Multi-Select" } diff --git a/packages/helpwave_localization/lib/l10n/app_en.arb b/packages/helpwave_localization/lib/l10n/app_en.arb index 3dbf8b63..91743273 100644 --- a/packages/helpwave_localization/lib/l10n/app_en.arb +++ b/packages/helpwave_localization/lib/l10n/app_en.arb @@ -193,5 +193,12 @@ "new_": "New", "newRoom": "New Room", "newBed": "New Bed", - "taskTemplates": "Task Templates" + "taskTemplates": "Task Templates", + "datePicker": "Date Picker", + "dateTimePicker": "Date and Time Picker", + "checkBox": "Checkbox", + "number": "Numbervalue", + "freeText": "Freetext", + "select": "Single-Select", + "multiSelect": "Multi-Select" } diff --git a/packages/helpwave_service/lib/src/api/property/data_types/index.dart b/packages/helpwave_service/lib/src/api/property/data_types/index.dart index be0c741f..90c35b7d 100644 --- a/packages/helpwave_service/lib/src/api/property/data_types/index.dart +++ b/packages/helpwave_service/lib/src/api/property/data_types/index.dart @@ -3,3 +3,4 @@ export 'field_type.dart'; export 'property.dart'; export 'select_data.dart'; export 'subject_type.dart'; +export 'property_view_filter_update.dart'; diff --git a/packages/helpwave_service/lib/src/api/property/index.dart b/packages/helpwave_service/lib/src/api/property/index.dart index a41c25f8..2662b569 100644 --- a/packages/helpwave_service/lib/src/api/property/index.dart +++ b/packages/helpwave_service/lib/src/api/property/index.dart @@ -1 +1,3 @@ export 'data_types/index.dart'; +export 'services/index.dart'; +export 'property_api_service_clients.dart'; diff --git a/packages/helpwave_service/lib/src/api/property/services/index.dart b/packages/helpwave_service/lib/src/api/property/services/index.dart new file mode 100644 index 00000000..54f986ae --- /dev/null +++ b/packages/helpwave_service/lib/src/api/property/services/index.dart @@ -0,0 +1,3 @@ +export 'property_service.dart'; +export 'property_value_service.dart'; +export 'property_views_service.dart'; diff --git a/packages/helpwave_service/lib/src/api/property/services/property_views_service.dart b/packages/helpwave_service/lib/src/api/property/services/property_views_service.dart index 4d4f537e..df4c76db 100644 --- a/packages/helpwave_service/lib/src/api/property/services/property_views_service.dart +++ b/packages/helpwave_service/lib/src/api/property/services/property_views_service.dart @@ -1,7 +1,6 @@ import 'package:grpc/grpc.dart'; import 'package:helpwave_proto_dart/services/property_svc/v1/property_value_svc.pb.dart'; import 'package:helpwave_proto_dart/services/property_svc/v1/property_views_svc.pbgrpc.dart'; -import 'package:helpwave_service/src/api/property/data_types/property_view_filter_update.dart'; import 'package:helpwave_service/src/api/property/property_api_service_clients.dart'; import '../../../../property.dart'; @@ -9,7 +8,7 @@ import '../../../../property.dart'; /// /// Provides queries and requests that load or alter [PropertyViewRules] objects on the server /// The server is defined in the underlying [PropertyAPIServiceClients] -class PropertyValueService { +class PropertyViewsService { /// The GRPC ServiceClient which handles GRPC PropertyViewsServiceClient service = PropertyAPIServiceClients().propertyViewsServiceClient; diff --git a/packages/helpwave_service/lib/src/auth/identity.dart b/packages/helpwave_service/lib/src/auth/identity.dart index f79826d1..0540be01 100644 --- a/packages/helpwave_service/lib/src/auth/identity.dart +++ b/packages/helpwave_service/lib/src/auth/identity.dart @@ -31,9 +31,10 @@ class Identity { /// The default login data factory Identity.defaultIdentity() { return Identity( - idToken: - "eyJzdWIiOiIxODE1OTcxMy01ZDRlLTRhZDUtOTRhZC1mYmI2YmIxNDc5ODQiLCJuYW1lIjoiTWF4IE11c3Rlcm1hbm4iLCJuaWNrbmFt" - "ZSI6Im1heC5tdXN0ZXJtYW5uIiwiZW1haWwiOiJtYXgubXVzdGVybWFubkBoZWxwd2F2ZS5kZSIsIm9yZ2FuaXphdGlvbnMiOlsiM2IyNWM2ZjUtNDcwNS00MDc0LTlmYzYtYTUwYzI4ZWJhNDA2Il19", + idToken: "eyJzdWIiOiIxODE1OTcxMy01ZDRlLTRhZDUtOTRhZC1mYmI2YmIxNDc5ODQiLCJlbWFpbCI6Im1heC5tdXN0ZXJtYW5uQGhlbHB" + "3YXZlLmRlIiwiZW1haWxfdmVyaWZpZWQiOnRydWUsIm5hbWUiOiJNYXggTXVzdGVybWFubiIsInByZWZlcnJlZF91c2Vybm" + "FtZSI6Im1heC5tdXN0ZXJtYW5uIiwiZ2l2ZW5fbmFtZSI6Ik1heCIsImZhbWlseV9uYW1lIjoiTXVzdGVybWFubiIsIm9yZ2FuaXphdG" + "lvbiI6eyJpZCI6IjNiMjVjNmY1LTQ3MDUtNDA3NC05ZmM2LWE1MGMyOGViYTQwNiIsIm5hbWUiOiJoZWxwd2F2ZSB0ZXN0In19", // TODO add a default here id: "bb560b25-45b6-4a84-87f1-408e8c32dec8", name: "Max Mustermann", From c3bf68fcfb58ec16c6860ec57d0df5ffb132eab1 Mon Sep 17 00:00:00 2001 From: Felix Thape Date: Mon, 21 Oct 2024 15:50:26 +0200 Subject: [PATCH 20/36] feat: add property screen base --- .../property_bottom_sheet.dart | 52 +++++++++++++ .../src/api/property/controllers/index.dart | 1 + .../controllers/property_controller.dart | 74 +++++++++++++++++++ .../src/api/property/data_types/property.dart | 2 +- .../lib/src/api/property/index.dart | 1 + 5 files changed, 129 insertions(+), 1 deletion(-) create mode 100644 apps/tasks/lib/components/bottom_sheet_pages/property_bottom_sheet.dart create mode 100644 packages/helpwave_service/lib/src/api/property/controllers/index.dart create mode 100644 packages/helpwave_service/lib/src/api/property/controllers/property_controller.dart diff --git a/apps/tasks/lib/components/bottom_sheet_pages/property_bottom_sheet.dart b/apps/tasks/lib/components/bottom_sheet_pages/property_bottom_sheet.dart new file mode 100644 index 00000000..763b2cea --- /dev/null +++ b/apps/tasks/lib/components/bottom_sheet_pages/property_bottom_sheet.dart @@ -0,0 +1,52 @@ +import 'package:flutter/cupertino.dart'; +import 'package:helpwave_service/property.dart'; +import 'package:helpwave_theme/constants.dart'; +import 'package:helpwave_theme/util.dart'; +import 'package:helpwave_widget/bottom_sheets.dart'; +import 'package:helpwave_widget/loading.dart'; +import 'package:provider/provider.dart'; + +class PropertyBottomSheetPage extends StatelessWidget { + final String? id; + + const PropertyBottomSheetPage({super.key, this.id}); + + @override + Widget build(BuildContext context) { + return ChangeNotifierProvider( + create: (context) => PropertyController(id: id), + child: BottomSheetPage( + header: BottomSheetHeader.navigation( + context, + title: Consumer( + builder: (context, controller, _) => LoadingAndErrorWidget( + state: controller.state, + loadingWidget: const PulsingContainer(width: 50), + child: Text(controller.property.name, style: context.theme.textTheme.titleMedium), + ), + ), + ), + child: Flexible( + child: Consumer( + builder: (context, controller, _) => LoadingAndErrorWidget( + state: controller.state, + child: ListView( + children: [ + Text( + "subjectType", + // TODO context.localization!.subjectType, + style: context.theme.textTheme.titleSmall?.copyWith( + color: context.theme.colorScheme.primary, + ), + ), + const SizedBox(height: paddingTiny), + + ], + ), + ), + ), + ), + ), + ); + } +} diff --git a/packages/helpwave_service/lib/src/api/property/controllers/index.dart b/packages/helpwave_service/lib/src/api/property/controllers/index.dart new file mode 100644 index 00000000..75851e94 --- /dev/null +++ b/packages/helpwave_service/lib/src/api/property/controllers/index.dart @@ -0,0 +1 @@ +export "property_controller.dart"; diff --git a/packages/helpwave_service/lib/src/api/property/controllers/property_controller.dart b/packages/helpwave_service/lib/src/api/property/controllers/property_controller.dart new file mode 100644 index 00000000..51511eee --- /dev/null +++ b/packages/helpwave_service/lib/src/api/property/controllers/property_controller.dart @@ -0,0 +1,74 @@ +import 'package:helpwave_service/property.dart'; +import 'package:helpwave_util/loading.dart'; + +/// The Controller for managing [Property]s in a Ward +class PropertyController extends LoadingChangeNotifier { + /// The current [Property] + Property _property = Property( + name: "", + subjectType: PropertySubjectType.patient, + fieldType: PropertyFieldType.multiSelect, + ); + + /// The current [Property] + Property get property => _property; + + set property(Property value) { + _property = value; + changeState(LoadingState.loaded); + } + + /// Is the current [Property] already saved on the server or are we creating? + get isCreating => _property.isCreating; + + PropertyController({String? id, Property? property}) { + assert(id == null || property == null || id == property.id); + if (property != null) { + _property = property; + } else { + _property = _property.copyWith(PropertyUpdate(id: id)); + } + load(); + } + + /// A function to load the [Property] + Future load() async { + loadProperty() async { + if (isCreating) { + return; + } + property = await PropertyService().get(property.id!); + } + + loadHandler( + future: loadProperty(), + ); + } + + /// Change the notes of the [Property] + Future update(PropertyUpdate update) async { + if (isCreating) { + property.copyWith(update); + return; + } + updateNotes() async { + await PropertyService().update(property.id!, update).then((hasSuccess) { + property.copyWith(update); + }); + } + + loadHandler(future: updateNotes()); + } + + /// Creates the [Property] + Future create() async { + assert(property.isCreating, "Only use create when the Property is creating"); + createProperty() async { + await PropertyService().create(property).then((newProperty) { + property = newProperty; + }); + } + + loadHandler(future: createProperty()); + } +} diff --git a/packages/helpwave_service/lib/src/api/property/data_types/property.dart b/packages/helpwave_service/lib/src/api/property/data_types/property.dart index 815d69d2..cd415083 100644 --- a/packages/helpwave_service/lib/src/api/property/data_types/property.dart +++ b/packages/helpwave_service/lib/src/api/property/data_types/property.dart @@ -50,7 +50,7 @@ class Property extends IdentifiedObject implements CopyWithInterface Date: Tue, 22 Oct 2024 19:13:23 +0200 Subject: [PATCH 21/36] feat: add translations --- .../bottom_sheet_pages/property_bottom_sheet.dart | 14 ++++++++++++++ packages/helpwave_localization/lib/l10n/app_de.arb | 12 +++++++++++- packages/helpwave_localization/lib/l10n/app_en.arb | 12 +++++++++++- 3 files changed, 36 insertions(+), 2 deletions(-) diff --git a/apps/tasks/lib/components/bottom_sheet_pages/property_bottom_sheet.dart b/apps/tasks/lib/components/bottom_sheet_pages/property_bottom_sheet.dart index 763b2cea..b7bc70a5 100644 --- a/apps/tasks/lib/components/bottom_sheet_pages/property_bottom_sheet.dart +++ b/apps/tasks/lib/components/bottom_sheet_pages/property_bottom_sheet.dart @@ -1,4 +1,5 @@ import 'package:flutter/cupertino.dart'; +import 'package:helpwave_localization/localization.dart'; import 'package:helpwave_service/property.dart'; import 'package:helpwave_theme/constants.dart'; import 'package:helpwave_theme/util.dart'; @@ -32,6 +33,19 @@ class PropertyBottomSheetPage extends StatelessWidget { state: controller.state, child: ListView( children: [ + Text( + context.localization!.name, + style: context.theme.textTheme.titleMedium?.copyWith( + color: context.theme.colorScheme.primary, + ), + ), + Text( + context.localization!.name, + style: context.theme.textTheme.titleSmall?.copyWith( + color: context.theme.colorScheme.primary, + ), + ), + const SizedBox(height: paddingTiny), Text( "subjectType", // TODO context.localization!.subjectType, diff --git a/packages/helpwave_localization/lib/l10n/app_de.arb b/packages/helpwave_localization/lib/l10n/app_de.arb index 10ba7365..56377187 100644 --- a/packages/helpwave_localization/lib/l10n/app_de.arb +++ b/packages/helpwave_localization/lib/l10n/app_de.arb @@ -200,5 +200,15 @@ "number": "Zahlenwert", "freeText": "Freitext", "select": "Single-Select", - "multiSelect": "Multi-Select" + "multiSelect": "Multi-Select", + "basic": "Grundlagen", + "subjectType": "Subjekt", + "field": "Feld", + "values": "Werte", + "allowCustomValues": "Nutzerwerte zulassen", + "allowCustomValuesDescription": "Nutzern erlauben Freitexteingaben zu nutzen, wenn vordefinierte Werte nicht ausreichen.", + "rules": "Regeln", + "importance": "Wichtigkeit", + "alwaysVisible": "Immer Sichtbar", + "alwaysVisibleDescription": "Will be displayed on entity even if the value of the property is empty." } diff --git a/packages/helpwave_localization/lib/l10n/app_en.arb b/packages/helpwave_localization/lib/l10n/app_en.arb index 91743273..f23a9b57 100644 --- a/packages/helpwave_localization/lib/l10n/app_en.arb +++ b/packages/helpwave_localization/lib/l10n/app_en.arb @@ -200,5 +200,15 @@ "number": "Numbervalue", "freeText": "Freetext", "select": "Single-Select", - "multiSelect": "Multi-Select" + "multiSelect": "Multi-Select", + "basic": "Basic", + "subjectType": "Subject", + "field": "Field", + "values": "Values", + "allowCustomValues": "Allow custom values", + "allowCustomValuesDescription": "Let users enter a free text when the predefined values are not enough.", + "rules": "Rules", + "importance": "Importance", + "alwaysVisible": "Always Visible", + "alwaysVisibleDescription": "Will be displayed on entity even if the value of the property is empty." } From b2d72b774400f132658b7efba83c703dec186984 Mon Sep 17 00:00:00 2001 From: Felix Thape Date: Tue, 22 Oct 2024 19:14:27 +0200 Subject: [PATCH 22/36] feat: add option to skip login with fake data --- .../properties_bottom_sheet.dart | 6 +++--- .../bottom_sheet_pages/property_bottom_sheet.dart | 1 - apps/tasks/lib/main.dart | 2 ++ .../lib/src/auth/current_ward_svc.dart | 15 ++++++++++++++- 4 files changed, 19 insertions(+), 5 deletions(-) diff --git a/apps/tasks/lib/components/bottom_sheet_pages/properties_bottom_sheet.dart b/apps/tasks/lib/components/bottom_sheet_pages/properties_bottom_sheet.dart index 80fad572..6af4b7b1 100644 --- a/apps/tasks/lib/components/bottom_sheet_pages/properties_bottom_sheet.dart +++ b/apps/tasks/lib/components/bottom_sheet_pages/properties_bottom_sheet.dart @@ -7,6 +7,8 @@ import 'package:helpwave_theme/util.dart'; import 'package:helpwave_widget/bottom_sheets.dart'; import 'package:helpwave_widget/lists.dart'; import 'package:helpwave_widget/loading.dart'; +import 'package:helpwave_widget/navigation.dart'; +import 'package:tasks/components/bottom_sheet_pages/property_bottom_sheet.dart'; import 'package:tasks/screens/settings_screen.dart'; import '../../util/subject_type_translations.dart'; @@ -17,8 +19,6 @@ class PropertiesBottomSheet extends StatelessWidget { const PropertiesBottomSheet({super.key, this.wardId}); - // TODO add the possibility of a ward - @override Widget build(BuildContext context) { return BottomSheetPage( @@ -60,7 +60,7 @@ class PropertiesBottomSheet extends StatelessWidget { title: property.name, subtitle: propertyFieldTypeTranslations(context, property.fieldType), onTap: () { - // TODO navigate + NavigationStackController.of(context).push(PropertyBottomSheetPage(id: property.id)); }, )) .toList(), diff --git a/apps/tasks/lib/components/bottom_sheet_pages/property_bottom_sheet.dart b/apps/tasks/lib/components/bottom_sheet_pages/property_bottom_sheet.dart index 763b2cea..3980acf0 100644 --- a/apps/tasks/lib/components/bottom_sheet_pages/property_bottom_sheet.dart +++ b/apps/tasks/lib/components/bottom_sheet_pages/property_bottom_sheet.dart @@ -40,7 +40,6 @@ class PropertyBottomSheetPage extends StatelessWidget { ), ), const SizedBox(height: paddingTiny), - ], ), ), diff --git a/apps/tasks/lib/main.dart b/apps/tasks/lib/main.dart index bcdfa822..e101ec86 100644 --- a/apps/tasks/lib/main.dart +++ b/apps/tasks/lib/main.dart @@ -24,6 +24,8 @@ void main() { // OfflineMode for PropertyAPIServiceClients does not work yet // ..offlineMode = isUsingOfflineClients; UserSessionService().changeMode(isUsingDevModeLogin); + // TODO the line below enables direct login, but behaves somewhat weirdly + UserSessionService().tokenLogin().then((value) => CurrentWardService().fakeLogin()); runApp(const MyApp()); } diff --git a/packages/helpwave_service/lib/src/auth/current_ward_svc.dart b/packages/helpwave_service/lib/src/auth/current_ward_svc.dart index 7bbacd43..f6cb447f 100644 --- a/packages/helpwave_service/lib/src/auth/current_ward_svc.dart +++ b/packages/helpwave_service/lib/src/auth/current_ward_svc.dart @@ -132,7 +132,8 @@ class CurrentWardService extends Listenable { if (!isLoaded) { fetch(); } - if (kDebugMode) { // TODO use logger + if (kDebugMode) { + // TODO use logger print(currentWard); } notifyListeners(); @@ -149,6 +150,18 @@ class CurrentWardService extends Listenable { }); } + Future fakeLogin() async { + try { + Organization organization = (await OrganizationService().getOrganizationsForUser())[0]; + WardMinimal ward = (await WardService().getWards(organizationId: organization.id))[0]; + currentWard = CurrentWardInformation(ward, organization); + } catch (e) { + if (kDebugMode) { + print(e); + } + } + } + /// Fetch [Ward] and [Organization] from backend Future fetch() async { if (!isInitialized) { From ecd86e1fcf8a93300bb8e2e0fe083cfd1c9f4d3a Mon Sep 17 00:00:00 2001 From: Felix Thape Date: Sat, 16 Nov 2024 12:19:15 +0100 Subject: [PATCH 23/36] feat: update property creation --- .../properties_bottom_sheet.dart | 4 +- .../property_bottom_sheet.dart | 152 +++++++++++++++++- .../lib/util/field_type_translations.dart | 22 +++ .../lib/util/subject_type_translations.dart | 22 +-- .../helpwave_localization/lib/l10n/app_de.arb | 3 +- .../helpwave_localization/lib/l10n/app_en.arb | 3 +- .../controllers/property_controller.dart | 20 ++- .../api/property/data_types/select_data.dart | 5 +- .../property/services/property_service.dart | 13 +- 9 files changed, 208 insertions(+), 36 deletions(-) create mode 100644 apps/tasks/lib/util/field_type_translations.dart diff --git a/apps/tasks/lib/components/bottom_sheet_pages/properties_bottom_sheet.dart b/apps/tasks/lib/components/bottom_sheet_pages/properties_bottom_sheet.dart index 6af4b7b1..78d7e339 100644 --- a/apps/tasks/lib/components/bottom_sheet_pages/properties_bottom_sheet.dart +++ b/apps/tasks/lib/components/bottom_sheet_pages/properties_bottom_sheet.dart @@ -11,7 +11,7 @@ import 'package:helpwave_widget/navigation.dart'; import 'package:tasks/components/bottom_sheet_pages/property_bottom_sheet.dart'; import 'package:tasks/screens/settings_screen.dart'; -import '../../util/subject_type_translations.dart'; +import '../../util/field_type_translations.dart'; /// A [BottomSheet] for showing a [Property] information class PropertiesBottomSheet extends StatelessWidget { @@ -42,7 +42,7 @@ class PropertiesBottomSheet extends StatelessWidget { trailing: BottomSheetAction( icon: Icons.add, onPressed: () { - // TODO open property create screen + NavigationStackController.of(context).push(const PropertyBottomSheetPage()); }, ), ), diff --git a/apps/tasks/lib/components/bottom_sheet_pages/property_bottom_sheet.dart b/apps/tasks/lib/components/bottom_sheet_pages/property_bottom_sheet.dart index b0a258e2..3b634dec 100644 --- a/apps/tasks/lib/components/bottom_sheet_pages/property_bottom_sheet.dart +++ b/apps/tasks/lib/components/bottom_sheet_pages/property_bottom_sheet.dart @@ -1,11 +1,17 @@ import 'package:flutter/cupertino.dart'; +import 'package:flutter/material.dart'; import 'package:helpwave_localization/localization.dart'; import 'package:helpwave_service/property.dart'; import 'package:helpwave_theme/constants.dart'; import 'package:helpwave_theme/util.dart'; import 'package:helpwave_widget/bottom_sheets.dart'; +import 'package:helpwave_widget/lists.dart'; import 'package:helpwave_widget/loading.dart'; +import 'package:helpwave_widget/text_input.dart'; import 'package:provider/provider.dart'; +import 'package:tasks/util/field_type_translations.dart'; + +import '../../util/subject_type_translations.dart'; class PropertyBottomSheetPage extends StatelessWidget { final String? id; @@ -14,6 +20,11 @@ class PropertyBottomSheetPage extends StatelessWidget { @override Widget build(BuildContext context) { + InputDecoration inputDecoration = InputDecoration( + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(borderRadiusMedium), + )); + return ChangeNotifierProvider( create: (context) => PropertyController(id: id), child: BottomSheetPage( @@ -34,26 +45,157 @@ class PropertyBottomSheetPage extends StatelessWidget { child: ListView( children: [ Text( - context.localization!.name, + context.localization!.basic, style: context.theme.textTheme.titleMedium?.copyWith( color: context.theme.colorScheme.primary, ), ), + const SizedBox(height: paddingSmall), Text( context.localization!.name, - style: context.theme.textTheme.titleSmall?.copyWith( + style: context.theme.textTheme.titleSmall, + ), + const SizedBox(height: paddingTiny), + TextFormFieldWithTimer( + initialValue: controller.property.name, + onUpdate: (value) => controller.update(PropertyUpdate(name: value)), + ), + const SizedBox(height: paddingSmall), + Text( + context.localization!.subjectType, + style: context.theme.textTheme.titleSmall, + ), + const SizedBox(height: paddingTiny), + DropdownButtonFormField( + decoration: inputDecoration, + value: controller.property.subjectType, + items: PropertySubjectType.values + .map((value) => DropdownMenuItem( + value: value, + child: Text(propertySubjectTypeTranslations(context, value)), + )) + .toList(), + onChanged: (value) => controller.update(PropertyUpdate(subjectType: value)), + ), + const SizedBox(height: paddingSmall), + Text( + context.localization!.description, + style: context.theme.textTheme.titleSmall, + ), + const SizedBox(height: paddingTiny), + TextFormFieldWithTimer( + initialValue: controller.property.description, + onUpdate: (value) => controller.update(PropertyUpdate(description: value)), + maxLines: 5, + ), + const SizedBox(height: paddingMedium), + // Field section + Text( + context.localization!.field, + style: context.theme.textTheme.titleMedium?.copyWith( color: context.theme.colorScheme.primary, ), ), + const SizedBox(height: paddingSmall), + Text( + "${context.localization!.field} ${context.localization!.type}", + style: context.theme.textTheme.titleSmall, + ), const SizedBox(height: paddingTiny), + DropdownButtonFormField( + decoration: inputDecoration, + value: controller.property.fieldType, + items: PropertyFieldType.values + .map((value) => DropdownMenuItem( + value: value, + child: Text(propertyFieldTypeTranslations(context, value)), + )) + .toList(), + onChanged: (value) => controller.update(PropertyUpdate(fieldType: value)), + ), + Visibility( + visible: controller.property.isSelectType, + child: Column( + children: [ + // TODO select option list + const SizedBox(height: paddingSmall), + RoundedListTiles( + children: [ + ListTile( + title: Text( + context.localization!.allowCustomValues, + style: context.theme.textTheme.titleSmall, + ), + subtitle: Text( + context.localization!.allowCustomValuesDescription, + style: TextStyle(color: context.theme.hintColor), + ), + trailing: Switch( + value: controller.property.selectData?.isAllowingFreeText ?? false, + onChanged: (value) => controller.update(PropertyUpdate( + selectDataUpdate: ( + isAllowingFreeText: value, + options: controller.property.selectData!.options, + removeOptions: null, + upsert: null + ), + )), + ), + ), + ], + ), + ], + )), + const SizedBox(height: paddingMedium), + // Rules Text( - "subjectType", - // TODO context.localization!.subjectType, - style: context.theme.textTheme.titleSmall?.copyWith( + context.localization!.rules, + style: context.theme.textTheme.titleMedium?.copyWith( color: context.theme.colorScheme.primary, ), ), + const SizedBox(height: paddingSmall), + Text( + context.localization!.importance, + style: context.theme.textTheme.titleSmall, + ), const SizedBox(height: paddingTiny), + RoundedListTiles( + children: [ + ListTile( + title: Text( + context.localization!.alwaysVisible, + style: context.theme.textTheme.titleSmall, + ), + subtitle: Text( + context.localization!.alwaysVisibleDescription, + style: TextStyle(color: context.theme.hintColor), + ), + trailing: Switch( + value: controller.property.alwaysIncludeForViewSource ?? false, + onChanged: (value) => controller.update(PropertyUpdate(alwaysIncludeForViewSource: value)), + ), + ), + ], + ), + Visibility( + visible: controller.property.isCreating, + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.end, + children: [ + Padding( + padding: const EdgeInsets.symmetric(vertical: paddingMedium), + child: FilledButton( + onPressed: () => controller.create(), + child: Text( + context.localization!.create, + ), + ), + ), + ], + ), + ), ], ), ), diff --git a/apps/tasks/lib/util/field_type_translations.dart b/apps/tasks/lib/util/field_type_translations.dart new file mode 100644 index 00000000..146d9486 --- /dev/null +++ b/apps/tasks/lib/util/field_type_translations.dart @@ -0,0 +1,22 @@ +import 'package:flutter/cupertino.dart'; +import 'package:helpwave_localization/localization.dart'; +import 'package:helpwave_service/property.dart'; + +String propertyFieldTypeTranslations(BuildContext context, PropertyFieldType fieldType) { + switch(fieldType){ + case PropertyFieldType.text: + return context.localization!.freeText; + case PropertyFieldType.number: + return context.localization!.number; + case PropertyFieldType.bool: + return context.localization!.checkBox; + case PropertyFieldType.date: + return context.localization!.datePicker; + case PropertyFieldType.dateTime: + return context.localization!.dateTimePicker; + case PropertyFieldType.singleSelect: + return context.localization!.select; + case PropertyFieldType.multiSelect: + return context.localization!.multiSelect; + } +} diff --git a/apps/tasks/lib/util/subject_type_translations.dart b/apps/tasks/lib/util/subject_type_translations.dart index 146d9486..bd0ab9fa 100644 --- a/apps/tasks/lib/util/subject_type_translations.dart +++ b/apps/tasks/lib/util/subject_type_translations.dart @@ -2,21 +2,11 @@ import 'package:flutter/cupertino.dart'; import 'package:helpwave_localization/localization.dart'; import 'package:helpwave_service/property.dart'; -String propertyFieldTypeTranslations(BuildContext context, PropertyFieldType fieldType) { - switch(fieldType){ - case PropertyFieldType.text: - return context.localization!.freeText; - case PropertyFieldType.number: - return context.localization!.number; - case PropertyFieldType.bool: - return context.localization!.checkBox; - case PropertyFieldType.date: - return context.localization!.datePicker; - case PropertyFieldType.dateTime: - return context.localization!.dateTimePicker; - case PropertyFieldType.singleSelect: - return context.localization!.select; - case PropertyFieldType.multiSelect: - return context.localization!.multiSelect; +String propertySubjectTypeTranslations(BuildContext context, PropertySubjectType subjectType) { + switch(subjectType){ + case PropertySubjectType.patient: + return context.localization!.patient; + case PropertySubjectType.task: + return context.localization!.task; } } diff --git a/packages/helpwave_localization/lib/l10n/app_de.arb b/packages/helpwave_localization/lib/l10n/app_de.arb index 56377187..3d5c070f 100644 --- a/packages/helpwave_localization/lib/l10n/app_de.arb +++ b/packages/helpwave_localization/lib/l10n/app_de.arb @@ -210,5 +210,6 @@ "rules": "Regeln", "importance": "Wichtigkeit", "alwaysVisible": "Immer Sichtbar", - "alwaysVisibleDescription": "Will be displayed on entity even if the value of the property is empty." + "alwaysVisibleDescription": "Will be displayed on entity even if the value of the property is empty.", + "description": "Beschreibung" } diff --git a/packages/helpwave_localization/lib/l10n/app_en.arb b/packages/helpwave_localization/lib/l10n/app_en.arb index f23a9b57..aeb539b9 100644 --- a/packages/helpwave_localization/lib/l10n/app_en.arb +++ b/packages/helpwave_localization/lib/l10n/app_en.arb @@ -210,5 +210,6 @@ "rules": "Rules", "importance": "Importance", "alwaysVisible": "Always Visible", - "alwaysVisibleDescription": "Will be displayed on entity even if the value of the property is empty." + "alwaysVisibleDescription": "Will be displayed on entity even if the value of the property is empty.", + "description": "Description" } diff --git a/packages/helpwave_service/lib/src/api/property/controllers/property_controller.dart b/packages/helpwave_service/lib/src/api/property/controllers/property_controller.dart index 51511eee..5c8165c0 100644 --- a/packages/helpwave_service/lib/src/api/property/controllers/property_controller.dart +++ b/packages/helpwave_service/lib/src/api/property/controllers/property_controller.dart @@ -8,6 +8,14 @@ class PropertyController extends LoadingChangeNotifier { name: "", subjectType: PropertySubjectType.patient, fieldType: PropertyFieldType.multiSelect, + selectData: PropertySelectData( + options: [ + PropertySelectOption(name: "Option 1"), + PropertySelectOption(name: "Option 2"), + PropertySelectOption(name: "Option 3"), + ], + isAllowingFreeText: true + ) ); /// The current [Property] @@ -37,7 +45,7 @@ class PropertyController extends LoadingChangeNotifier { if (isCreating) { return; } - property = await PropertyService().get(property.id!); + _property = await PropertyService().get(property.id!); } loadHandler( @@ -47,13 +55,13 @@ class PropertyController extends LoadingChangeNotifier { /// Change the notes of the [Property] Future update(PropertyUpdate update) async { - if (isCreating) { - property.copyWith(update); - return; - } updateNotes() async { + if (isCreating) { + _property = property.copyWith(update); + return; + } await PropertyService().update(property.id!, update).then((hasSuccess) { - property.copyWith(update); + _property = property.copyWith(update); }); } diff --git a/packages/helpwave_service/lib/src/api/property/data_types/select_data.dart b/packages/helpwave_service/lib/src/api/property/data_types/select_data.dart index d186f1db..6b6125a5 100644 --- a/packages/helpwave_service/lib/src/api/property/data_types/select_data.dart +++ b/packages/helpwave_service/lib/src/api/property/data_types/select_data.dart @@ -59,8 +59,9 @@ class PropertySelectData implements CopyWithInterface a.id!) - .where((element) => !update.removeOptions!.any((id) => id == element.id)) + // TODO change this potentially same value items are replaced + .upsert(update?.upsert ?? [], (a) => a.id ?? a.name) + .where((element) => !(update?.removeOptions ?? []).any((id) => id == element.id)) .toList(), ); } diff --git a/packages/helpwave_service/lib/src/api/property/services/property_service.dart b/packages/helpwave_service/lib/src/api/property/services/property_service.dart index c61839a2..804990b8 100644 --- a/packages/helpwave_service/lib/src/api/property/services/property_service.dart +++ b/packages/helpwave_service/lib/src/api/property/services/property_service.dart @@ -31,7 +31,14 @@ class PropertyService implements CRUDInterface PropertySelectOption( + id: option.id, name: option.name, description: option.description, isCustom: option.isCustom)) + .toList(), + isAllowingFreeText: response.selectData.allowFreetext) + : null); } Future> getMany({PropertySubjectType? subjectType}) async { @@ -44,7 +51,7 @@ class PropertyService implements CRUDInterface beds = response.properties + List properties = response.properties .map((value) => Property( id: value.id, name: value.name, @@ -56,7 +63,7 @@ class PropertyService implements CRUDInterface Date: Sat, 16 Nov 2024 16:46:30 +0100 Subject: [PATCH 24/36] feat: add display of select options --- .../property_bottom_sheet.dart | 21 +++++++++++++++++-- .../helpwave_localization/lib/l10n/app_de.arb | 5 ++++- .../helpwave_localization/lib/l10n/app_en.arb | 5 ++++- .../controllers/property_controller.dart | 2 +- 4 files changed, 28 insertions(+), 5 deletions(-) diff --git a/apps/tasks/lib/components/bottom_sheet_pages/property_bottom_sheet.dart b/apps/tasks/lib/components/bottom_sheet_pages/property_bottom_sheet.dart index 3b634dec..0e734550 100644 --- a/apps/tasks/lib/components/bottom_sheet_pages/property_bottom_sheet.dart +++ b/apps/tasks/lib/components/bottom_sheet_pages/property_bottom_sheet.dart @@ -1,4 +1,3 @@ -import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:helpwave_localization/localization.dart'; import 'package:helpwave_service/property.dart'; @@ -25,6 +24,9 @@ class PropertyBottomSheetPage extends StatelessWidget { borderRadius: BorderRadius.circular(borderRadiusMedium), )); + String createTitle = "${context.localization!.create} ${context.localization!.property}"; + String editTitle = context.localization!.editProperty; + return ChangeNotifierProvider( create: (context) => PropertyController(id: id), child: BottomSheetPage( @@ -34,7 +36,8 @@ class PropertyBottomSheetPage extends StatelessWidget { builder: (context, controller, _) => LoadingAndErrorWidget( state: controller.state, loadingWidget: const PulsingContainer(width: 50), - child: Text(controller.property.name, style: context.theme.textTheme.titleMedium), + child: Text(controller.property.isCreating ? createTitle : editTitle, + style: context.theme.textTheme.titleMedium), ), ), ), @@ -117,6 +120,20 @@ class PropertyBottomSheetPage extends StatelessWidget { visible: controller.property.isSelectType, child: Column( children: [ + const SizedBox(height: paddingSmall), + RoundedListTiles( + children: [ + ExpansionTile( + shape: const Border(), + title: Text( + "${controller.property.selectData?.options.length} ${context.localization!.options}"), + children: (controller.property.selectData?.options ?? []) + .map((selectOption) => ListTile(title: Text(selectOption.name))) + .toList(), + + ), + ], + ), // TODO select option list const SizedBox(height: paddingSmall), RoundedListTiles( diff --git a/packages/helpwave_localization/lib/l10n/app_de.arb b/packages/helpwave_localization/lib/l10n/app_de.arb index 3d5c070f..e364a3c8 100644 --- a/packages/helpwave_localization/lib/l10n/app_de.arb +++ b/packages/helpwave_localization/lib/l10n/app_de.arb @@ -183,6 +183,7 @@ "contactEmail": "Kontakt E-Mail", "member": "Mitglied", "members": "Mitglieder", + "property": "Eigenschaft", "properties": "Eigenschaften", "dangerZone": "Gefahrenzone", "organizationDangerZoneDescription": "Vorsicht, das löschen einer Organization ist permanent und kann nicht rückgängig gemacht werden!", @@ -211,5 +212,7 @@ "importance": "Wichtigkeit", "alwaysVisible": "Immer Sichtbar", "alwaysVisibleDescription": "Will be displayed on entity even if the value of the property is empty.", - "description": "Beschreibung" + "description": "Beschreibung", + "options": "Optionen", + "editProperty": "Property ändern" } diff --git a/packages/helpwave_localization/lib/l10n/app_en.arb b/packages/helpwave_localization/lib/l10n/app_en.arb index aeb539b9..ceca3f7c 100644 --- a/packages/helpwave_localization/lib/l10n/app_en.arb +++ b/packages/helpwave_localization/lib/l10n/app_en.arb @@ -183,6 +183,7 @@ "contactEmail": "Contact E-Mail", "member": "Member", "members": "Members", + "property": "Property", "properties": "Properties", "dangerZone": "Danger Zone", "organizationDangerZoneDescription": "Deleting the organization is a permanent action and cannot be undone be careful!", @@ -211,5 +212,7 @@ "importance": "Importance", "alwaysVisible": "Always Visible", "alwaysVisibleDescription": "Will be displayed on entity even if the value of the property is empty.", - "description": "Description" + "description": "Description", + "options": "Options", + "editProperty": "Edit Property" } diff --git a/packages/helpwave_service/lib/src/api/property/controllers/property_controller.dart b/packages/helpwave_service/lib/src/api/property/controllers/property_controller.dart index 5c8165c0..3bcf6960 100644 --- a/packages/helpwave_service/lib/src/api/property/controllers/property_controller.dart +++ b/packages/helpwave_service/lib/src/api/property/controllers/property_controller.dart @@ -5,7 +5,7 @@ import 'package:helpwave_util/loading.dart'; class PropertyController extends LoadingChangeNotifier { /// The current [Property] Property _property = Property( - name: "", + name: "Property Name", subjectType: PropertySubjectType.patient, fieldType: PropertyFieldType.multiSelect, selectData: PropertySelectData( From 05b6ac559fdfd01167e312d80a27f8d003c448f8 Mon Sep 17 00:00:00 2001 From: Felix Thape Date: Tue, 19 Nov 2024 16:04:50 +0100 Subject: [PATCH 25/36] feat: add ward creation and make some clean ups --- .../organization_bottom_sheet.dart | 7 +- .../properties_bottom_sheet.dart | 5 +- .../rooms_bottom_sheet.dart | 4 +- .../task_templates_bottom_sheet.dart | 4 +- .../bottom_sheet_pages/user_bottom_sheet.dart | 2 +- .../bottom_sheet_pages/ward_bottom_sheet.dart | 163 ++++++++++-------- .../wards_bottom_sheet_page.dart | 45 +++-- apps/tasks/lib/screens/settings_screen.dart | 61 +------ .../tasks/lib/screens/ward_select_screen.dart | 15 +- apps/tasks/pubspec.lock | 4 +- .../helpwave_localization/lib/l10n/app_de.arb | 3 +- .../helpwave_localization/lib/l10n/app_en.arb | 3 +- .../tasks/controllers/ward_controller.dart | 35 ++-- .../lib/src/api/tasks/data_types/ward.dart | 14 +- .../src/api/user/data_types/invitation.dart | 7 +- .../src/api/user/data_types/organization.dart | 12 +- .../lib/src/api/user/data_types/user.dart | 20 ++- .../organization_offline_client.dart | 18 -- .../lib/src/auth/identity.dart | 12 +- packages/helpwave_service/pubspec.yaml | 2 +- .../src/widgets/forward_navigation_tile.dart | 51 ++++++ .../lib/src/widgets/index.dart | 1 + 22 files changed, 254 insertions(+), 234 deletions(-) create mode 100644 packages/helpwave_widget/lib/src/widgets/forward_navigation_tile.dart diff --git a/apps/tasks/lib/components/bottom_sheet_pages/organization_bottom_sheet.dart b/apps/tasks/lib/components/bottom_sheet_pages/organization_bottom_sheet.dart index d663e745..4490ff6c 100644 --- a/apps/tasks/lib/components/bottom_sheet_pages/organization_bottom_sheet.dart +++ b/apps/tasks/lib/components/bottom_sheet_pages/organization_bottom_sheet.dart @@ -13,7 +13,6 @@ import 'package:provider/provider.dart'; import 'package:tasks/components/bottom_sheet_pages/organization_members_bottom_sheet.dart'; import 'package:tasks/components/bottom_sheet_pages/properties_bottom_sheet.dart'; import 'package:tasks/components/bottom_sheet_pages/wards_bottom_sheet_page.dart'; -import 'package:tasks/screens/settings_screen.dart'; class OrganizationBottomSheetPage extends StatelessWidget { final String organizationId; @@ -69,7 +68,7 @@ class OrganizationBottomSheetPage extends StatelessWidget { const SizedBox(height: distanceTiny), RoundedListTiles( children: [ - NavigationListTile( + ForwardNavigationTile( icon: Icons.house_rounded, title: context.localization!.wards, onTap: () { @@ -77,7 +76,7 @@ class OrganizationBottomSheetPage extends StatelessWidget { .push(WardsBottomSheetPage(organizationId: organizationId)); }, ), - NavigationListTile( + ForwardNavigationTile( icon: Icons.person, title: context.localization!.members, onTap: () { @@ -85,7 +84,7 @@ class OrganizationBottomSheetPage extends StatelessWidget { .push(OrganizationMembersBottomSheetPage(organizationId: organizationId)); }, ), - NavigationListTile( + ForwardNavigationTile( icon: Icons.label, title: context.localization!.properties, onTap: () { diff --git a/apps/tasks/lib/components/bottom_sheet_pages/properties_bottom_sheet.dart b/apps/tasks/lib/components/bottom_sheet_pages/properties_bottom_sheet.dart index 78d7e339..15d36193 100644 --- a/apps/tasks/lib/components/bottom_sheet_pages/properties_bottom_sheet.dart +++ b/apps/tasks/lib/components/bottom_sheet_pages/properties_bottom_sheet.dart @@ -8,9 +8,8 @@ import 'package:helpwave_widget/bottom_sheets.dart'; import 'package:helpwave_widget/lists.dart'; import 'package:helpwave_widget/loading.dart'; import 'package:helpwave_widget/navigation.dart'; +import 'package:helpwave_widget/widgets.dart'; import 'package:tasks/components/bottom_sheet_pages/property_bottom_sheet.dart'; -import 'package:tasks/screens/settings_screen.dart'; - import '../../util/field_type_translations.dart'; /// A [BottomSheet] for showing a [Property] information @@ -55,7 +54,7 @@ class PropertiesBottomSheet extends StatelessWidget { children: [ RoundedListTiles( children: properties - .map((property) => NavigationListTile( + .map((property) => ForwardNavigationTile( icon: Icons.label, title: property.name, subtitle: propertyFieldTypeTranslations(context, property.fieldType), diff --git a/apps/tasks/lib/components/bottom_sheet_pages/rooms_bottom_sheet.dart b/apps/tasks/lib/components/bottom_sheet_pages/rooms_bottom_sheet.dart index 0c59a73e..83c3f7fb 100644 --- a/apps/tasks/lib/components/bottom_sheet_pages/rooms_bottom_sheet.dart +++ b/apps/tasks/lib/components/bottom_sheet_pages/rooms_bottom_sheet.dart @@ -6,9 +6,9 @@ import 'package:helpwave_widget/bottom_sheets.dart'; import 'package:helpwave_widget/lists.dart'; import 'package:helpwave_widget/loading.dart'; import 'package:helpwave_widget/navigation.dart'; +import 'package:helpwave_widget/widgets.dart'; import 'package:provider/provider.dart'; import 'package:tasks/components/bottom_sheet_pages/room_overview_bottom_sheet.dart'; -import 'package:tasks/screens/settings_screen.dart'; class RoomsBottomSheetPage extends StatelessWidget { final String wardId; @@ -54,7 +54,7 @@ class RoomsBottomSheetPage extends StatelessWidget { child: RoundedListTiles( children: controller.rooms .map( - (room) => NavigationListTile( + (room) => ForwardNavigationTile( icon: Icons.meeting_room_rounded, title: room.name, trailingText: "${room.beds.length} ${context.localization!.beds}", diff --git a/apps/tasks/lib/components/bottom_sheet_pages/task_templates_bottom_sheet.dart b/apps/tasks/lib/components/bottom_sheet_pages/task_templates_bottom_sheet.dart index b42ec0ae..0446e268 100644 --- a/apps/tasks/lib/components/bottom_sheet_pages/task_templates_bottom_sheet.dart +++ b/apps/tasks/lib/components/bottom_sheet_pages/task_templates_bottom_sheet.dart @@ -7,8 +7,8 @@ import 'package:helpwave_widget/bottom_sheets.dart'; import 'package:helpwave_widget/lists.dart'; import 'package:helpwave_widget/loading.dart'; import 'package:helpwave_widget/navigation.dart'; +import 'package:helpwave_widget/widgets.dart'; import 'package:tasks/components/bottom_sheet_pages/task_template_bottom_sheet.dart'; -import 'package:tasks/screens/settings_screen.dart'; class TaskTemplatesBottomSheetPage extends StatelessWidget { final bool isPersonal; @@ -66,7 +66,7 @@ class TaskTemplatesBottomSheetPage extends StatelessWidget { RoundedListTiles( children: data .map( - (template) => NavigationListTile( + (template) => ForwardNavigationTile( icon: Icons.fact_check_rounded, title: template.name, onTap: () { diff --git a/apps/tasks/lib/components/bottom_sheet_pages/user_bottom_sheet.dart b/apps/tasks/lib/components/bottom_sheet_pages/user_bottom_sheet.dart index 8527af33..07ae6e01 100644 --- a/apps/tasks/lib/components/bottom_sheet_pages/user_bottom_sheet.dart +++ b/apps/tasks/lib/components/bottom_sheet_pages/user_bottom_sheet.dart @@ -81,7 +81,7 @@ class UserBottomSheetPage extends StatelessWidget { style: context.theme.textTheme.labelLarge, ); }), - const Icon(Icons.chevron_right_rounded), + const Icon(Icons.expand_more_rounded), ], ), onTap: () { diff --git a/apps/tasks/lib/components/bottom_sheet_pages/ward_bottom_sheet.dart b/apps/tasks/lib/components/bottom_sheet_pages/ward_bottom_sheet.dart index b17c44ec..e486f05e 100644 --- a/apps/tasks/lib/components/bottom_sheet_pages/ward_bottom_sheet.dart +++ b/apps/tasks/lib/components/bottom_sheet_pages/ward_bottom_sheet.dart @@ -14,100 +14,123 @@ import 'package:provider/provider.dart'; import 'package:tasks/components/bottom_sheet_pages/properties_bottom_sheet.dart'; import 'package:tasks/components/bottom_sheet_pages/rooms_bottom_sheet.dart'; import 'package:tasks/components/bottom_sheet_pages/task_templates_bottom_sheet.dart'; -import 'package:tasks/screens/settings_screen.dart'; class WardBottomSheetPage extends StatelessWidget { - final String wardId; + final String? wardId; - const WardBottomSheetPage({super.key, required this.wardId}); + const WardBottomSheetPage({super.key, this.wardId}); @override Widget build(BuildContext context) { return ChangeNotifierProvider( - create: (context) => WardController(wardId: wardId), + create: (context) => WardController(id: wardId), child: BottomSheetPage( header: BottomSheetHeader.navigation( context, title: Consumer( builder: (context, controller, _) { + if (controller.isCreating) { + return Text(context.localization!.createWard, style: context.theme.textTheme.titleMedium); + } return LoadingAndErrorWidget( loadingWidget: const PulsingContainer(height: 20), state: controller.state, child: Text( controller.ward.name, - style: const TextStyle(fontWeight: FontWeight.w800, fontSize: 16), + style: context.theme.textTheme.titleMedium, ), ); }, ), ), child: Flexible( - child: ListView( - shrinkWrap: true, - children: [ - Consumer( - builder: (context, controller, child) { - return LoadingAndErrorWidget( - state: controller.state, - loadingWidget: const PulsingContainer(height: 80), - child: Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text(context.localization!.name, style: context.theme.textTheme.titleSmall), - const SizedBox(height: distanceTiny), - TextFormFieldWithTimer( - initialValue: controller.state == LoadingState.loaded ? controller.ward.name : "", - onUpdate: (value) => controller.update(name: value), - maxLines: 1, - ), - ], + child: Consumer( + builder: (context, controller, child) { + return LoadingAndErrorWidget( + state: controller.state, + loadingWidget: const PulsingContainer(height: 80), + child: ListView( + children: [ + Text(context.localization!.name, style: context.theme.textTheme.titleSmall), + const SizedBox(height: distanceTiny), + TextFormFieldWithTimer( + initialValue: controller.state == LoadingState.loaded ? controller.ward.name : "", + onUpdate: (value) => controller.update(name: value), + maxLines: 1, + ), + !controller.isCreating + ? Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const SizedBox(height: distanceMedium), + Text(context.localization!.settings, style: context.theme.textTheme.titleMedium), + const SizedBox(height: distanceTiny), + RoundedListTiles( + children: [ + ForwardNavigationTile( + icon: Icons.house_rounded, + title: context.localization!.rooms, + onTap: () { + NavigationStackController.of(context).push(RoomsBottomSheetPage(wardId: wardId!)); + }, + ), + ForwardNavigationTile( + icon: Icons.checklist_rounded, + title: context.localization!.taskTemplates, + onTap: () { + NavigationStackController.of(context) + .push(TaskTemplatesBottomSheetPage(wardId: wardId)); + }, + ), + ForwardNavigationTile( + icon: Icons.label, + title: context.localization!.properties, + onTap: () { + NavigationStackController.of(context).push(PropertiesBottomSheet(wardId: wardId)); + }, + ) + ], + ), + const SizedBox(height: distanceMedium), + Text(context.localization!.dangerZone, style: context.theme.textTheme.titleMedium), + Text( + context.localization!.organizationDangerZoneDescription, + style: TextStyle(color: context.theme.hintColor), + ), + PressableText( + text: "${context.localization!.delete} ${context.localization!.organization}", + style: const TextStyle( + color: Colors.red, fontWeight: FontWeight.w700), // TODO get from theme + onPressed: () { + // TODO show modal and delete organization + }, + ), + ], + ) + : const SizedBox(), + Visibility( + visible: controller.isCreating, + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.end, + children: [ + Padding( + padding: const EdgeInsets.symmetric(vertical: paddingMedium), + child: FilledButton( + onPressed: () => controller.create(), + child: Text( + context.localization!.create, + ), + ), + ), + ], + ), ), - ); - }, - ), - const SizedBox(height: distanceMedium), - Text(context.localization!.settings, style: context.theme.textTheme.titleMedium), - const SizedBox(height: distanceTiny), - RoundedListTiles( - children: [ - NavigationListTile( - icon: Icons.house_rounded, - title: context.localization!.rooms, - onTap: () { - NavigationStackController.of(context).push(RoomsBottomSheetPage(wardId: wardId)); - }, - ), - NavigationListTile( - icon: Icons.checklist_rounded, - title: context.localization!.taskTemplates, - onTap: () { - NavigationStackController.of(context).push(TaskTemplatesBottomSheetPage(wardId: wardId)); - }, - ), - NavigationListTile( - icon: Icons.label, - title: context.localization!.properties, - onTap: () { - NavigationStackController.of(context).push(PropertiesBottomSheet(wardId: wardId)); - }, - ) - ], - ), - const SizedBox(height: distanceMedium), - Text(context.localization!.dangerZone, style: context.theme.textTheme.titleMedium), - Text( - context.localization!.organizationDangerZoneDescription, - style: TextStyle(color: context.theme.hintColor), - ), - PressableText( - text: "${context.localization!.delete} ${context.localization!.organization}", - style: const TextStyle(color: Colors.red, fontWeight: FontWeight.w700), // TODO get from theme - onPressed: () { - // TODO show modal and delete organization - }, - ), - ], + ], + ), + ); + }, ), ), ), diff --git a/apps/tasks/lib/components/bottom_sheet_pages/wards_bottom_sheet_page.dart b/apps/tasks/lib/components/bottom_sheet_pages/wards_bottom_sheet_page.dart index 2cdf0145..b12e0971 100644 --- a/apps/tasks/lib/components/bottom_sheet_pages/wards_bottom_sheet_page.dart +++ b/apps/tasks/lib/components/bottom_sheet_pages/wards_bottom_sheet_page.dart @@ -8,8 +8,8 @@ import 'package:helpwave_widget/bottom_sheets.dart'; import 'package:helpwave_widget/lists.dart'; import 'package:helpwave_widget/loading.dart'; import 'package:helpwave_widget/navigation.dart'; +import 'package:helpwave_widget/widgets.dart'; import 'package:tasks/components/bottom_sheet_pages/ward_bottom_sheet.dart'; -import 'package:tasks/screens/settings_screen.dart'; class WardsBottomSheetPage extends StatelessWidget { final String organizationId; @@ -38,28 +38,35 @@ class WardsBottomSheetPage extends StatelessWidget { ), loadingWidget: const PulsingContainer(width: 60, height: 40), ), + trailing: BottomSheetAction( + icon: Icons.add, + onPressed: () { + NavigationStackController.of(context).push(const WardBottomSheetPage()); + }), ), child: Flexible( child: LoadingFutureBuilder( future: WardService().getWards(organizationId: organizationId), - thenBuilder: (context, data) => ListView( - shrinkWrap: true, - children: [ - const SizedBox(height: distanceMedium), - RoundedListTiles( - children: data - .map( - (ward) => NavigationListTile( - icon: Icons.house_rounded, - title: ward.name, - onTap: () { - NavigationStackController.of(context).push(WardBottomSheetPage(wardId: ward.id)); - }, - ), - ) - .toList()), - ], - ), + thenBuilder: (context, data) { + return ListView( + shrinkWrap: true, + children: [ + const SizedBox(height: distanceMedium), + RoundedListTiles( + children: data + .map( + (ward) => ForwardNavigationTile( + icon: Icons.house_rounded, + title: ward.name, + onTap: () { + NavigationStackController.of(context).push(WardBottomSheetPage(wardId: ward.id)); + }, + ), + ) + .toList()), + ], + ); + }, ), ), ); diff --git a/apps/tasks/lib/screens/settings_screen.dart b/apps/tasks/lib/screens/settings_screen.dart index 5556fa9b..ac73619f 100644 --- a/apps/tasks/lib/screens/settings_screen.dart +++ b/apps/tasks/lib/screens/settings_screen.dart @@ -10,6 +10,7 @@ import 'package:helpwave_widget/bottom_sheets.dart'; import 'package:helpwave_widget/lists.dart'; import 'package:helpwave_widget/loading.dart'; import 'package:helpwave_widget/navigation.dart'; +import 'package:helpwave_widget/widgets.dart'; import 'package:provider/provider.dart'; import 'package:tasks/components/bottom_sheet_pages/organization_bottom_sheet.dart'; import 'package:tasks/components/bottom_sheet_pages/task_templates_bottom_sheet.dart'; @@ -151,54 +152,6 @@ class SettingsScreen extends StatelessWidget { } } -class NavigationListTile extends StatelessWidget { - final IconData icon; - final Color? color; - final String title; - final String? subtitle; - final void Function() onTap; - final String? trailingText; - - const NavigationListTile({ - super.key, - required this.icon, - this.color, - required this.title, - this.subtitle, - required this.onTap, - this.trailingText, - }); - - @override - Widget build(BuildContext context) { - return ListTile( - leading: Icon( - icon, - color: color ?? context.theme.colorScheme.primary, - ), - onTap: onTap, - title: Text(title, style: TextStyle(fontWeight: FontWeight.bold, color: color)), - subtitle: subtitle != null ? Text(subtitle!) : null, - trailing: Row( - mainAxisSize: MainAxisSize.min, - mainAxisAlignment: MainAxisAlignment.end, - children: [ - trailingText != null - ? Text( - trailingText!, - style: context.theme.textTheme.labelLarge, - ) - : const SizedBox(), - Icon( - Icons.chevron_right_rounded, - color: context.theme.colorScheme.onBackground.withOpacity(0.7), - ), - ], - ), - ); - } -} - class SettingsBottomSheetPage extends StatelessWidget { const SettingsBottomSheetPage({super.key}); @@ -225,17 +178,17 @@ class SettingsBottomSheetPage extends StatelessWidget { titleBuilder(context.localization!.personalSettings), RoundedListTiles( children: [ - NavigationListTile( + ForwardNavigationTile( icon: Icons.person, title: context.localization!.personalData, onTap: () {}, ), - NavigationListTile( + ForwardNavigationTile( icon: Icons.security_rounded, title: context.localization!.passwordAndSecurity, onTap: () {}, ), - NavigationListTile( + ForwardNavigationTile( icon: Icons.checklist_rounded, title: context.localization!.myTaskTemplates, onTap: () { @@ -251,7 +204,7 @@ class SettingsBottomSheetPage extends StatelessWidget { thenBuilder: (context, data) { return RoundedListTiles( children: data - .map((organization) => NavigationListTile( + .map((organization) => ForwardNavigationTile( icon: Icons.apartment_rounded, title: organization.longName, onTap: () { @@ -363,14 +316,14 @@ class SettingsBottomSheetPage extends StatelessWidget { titleBuilder(context.localization!.other), RoundedListTiles( children: [ - NavigationListTile( + ForwardNavigationTile( icon: Icons.info_outline, title: context.localization!.licenses, onTap: () => {showLicensePage(context: context)}, ), Consumer( builder: (context, currentWardService, _) { - return NavigationListTile( + return ForwardNavigationTile( icon: Icons.logout, title: context.localization!.logout, color: Colors.red.withOpacity(0.7), // TODO get this from theme diff --git a/apps/tasks/lib/screens/ward_select_screen.dart b/apps/tasks/lib/screens/ward_select_screen.dart index af0be9c2..93cacabe 100644 --- a/apps/tasks/lib/screens/ward_select_screen.dart +++ b/apps/tasks/lib/screens/ward_select_screen.dart @@ -5,6 +5,7 @@ import 'package:helpwave_service/tasks.dart'; import 'package:helpwave_service/user.dart'; import 'package:helpwave_theme/constants.dart'; import 'package:helpwave_widget/content_selection.dart'; +import 'package:helpwave_widget/widgets.dart'; import 'package:provider/provider.dart'; import 'package:tasks/screens/settings_screen.dart'; @@ -41,10 +42,9 @@ class _WardSelectScreen extends State { ), body: Column( children: [ - ListTile( - title: Text(organization?.longName ?? context.localization!.none), - subtitle: Text(context.localization!.organization), - trailing: const Icon(Icons.arrow_forward), + ForwardNavigationTile( + title:organization?.longName ?? context.localization!.none, + subtitle: context.localization!.organization, onTap: () => Navigator.push( context, MaterialPageRoute( @@ -69,10 +69,9 @@ class _WardSelectScreen extends State { } }), ), - ListTile( - title: Text(ward?.name ?? context.localization!.none), - subtitle: Text(context.localization!.ward), - trailing: const Icon(Icons.arrow_forward), + ForwardNavigationTile( + title: ward?.name ?? context.localization!.none, + subtitle: context.localization!.ward, onTap: organization == null ? null : () => Navigator.push( diff --git a/apps/tasks/pubspec.lock b/apps/tasks/pubspec.lock index e171c686..54e89c51 100644 --- a/apps/tasks/pubspec.lock +++ b/apps/tasks/pubspec.lock @@ -272,10 +272,10 @@ packages: dependency: transitive description: name: helpwave_proto_dart - sha256: b3cb1c868319e384e341f438a1a7d9e1f07bd283c2f63ab35e76745e7ce37d7e + sha256: "354e0677387867f3900add1e7a6b899d6c6725e8516ee07023d0c145414bc2d5" url: "https://pub.dev" source: hosted - version: "0.56.0-29ad8c8" + version: "0.58.0-1191f09" helpwave_service: dependency: "direct main" description: diff --git a/packages/helpwave_localization/lib/l10n/app_de.arb b/packages/helpwave_localization/lib/l10n/app_de.arb index e364a3c8..0eca992f 100644 --- a/packages/helpwave_localization/lib/l10n/app_de.arb +++ b/packages/helpwave_localization/lib/l10n/app_de.arb @@ -214,5 +214,6 @@ "alwaysVisibleDescription": "Will be displayed on entity even if the value of the property is empty.", "description": "Beschreibung", "options": "Optionen", - "editProperty": "Property ändern" + "editProperty": "Property ändern", + "createWard": "Station erstellen" } diff --git a/packages/helpwave_localization/lib/l10n/app_en.arb b/packages/helpwave_localization/lib/l10n/app_en.arb index ceca3f7c..d30f2dd4 100644 --- a/packages/helpwave_localization/lib/l10n/app_en.arb +++ b/packages/helpwave_localization/lib/l10n/app_en.arb @@ -214,5 +214,6 @@ "alwaysVisibleDescription": "Will be displayed on entity even if the value of the property is empty.", "description": "Description", "options": "Options", - "editProperty": "Edit Property" + "editProperty": "Edit Property", + "createWard": "Create Ward" } diff --git a/packages/helpwave_service/lib/src/api/tasks/controllers/ward_controller.dart b/packages/helpwave_service/lib/src/api/tasks/controllers/ward_controller.dart index b5a68655..a716ea6c 100644 --- a/packages/helpwave_service/lib/src/api/tasks/controllers/ward_controller.dart +++ b/packages/helpwave_service/lib/src/api/tasks/controllers/ward_controller.dart @@ -9,39 +9,37 @@ import 'package:helpwave_util/loading.dart'; /// only used locally class WardController extends LoadingChangeNotifier { /// The [WardMinimal] - WardMinimal? _ward; + WardMinimal _ward = WardMinimal(name: "Ward"); - WardMinimal get ward { - return _ward ?? WardMinimal(id: "", name: ""); - } + WardMinimal get ward => _ward; set ward(WardMinimal value) { _ward = value; notifyListeners(); } - bool get isCreating => wardId == null || wardId!.isEmpty; - - String? wardId; + bool get isCreating => ward.isCreating; - WardController({this.wardId = "", WardMinimal? ward}) { - assert(ward == null || ward.id == wardId); + WardController({String? id, WardMinimal? ward}) { + assert( + (id == null && ward?.id == null) || ward == null || id == ward.id, + "Ensure that both the id and id within the ward object are the same", + ); if (ward != null) { _ward = ward; - wardId = ward.id; - } - if (!isCreating) { - load(); + } else if (id != null) { + _ward = _ward.copyWith(id: id); } + load(); } /// Loads the [WardMinimal]s Future load() async { - if (isCreating) { - return; - } loadOp() async { - ward = await WardService().get(id: wardId!); + if (isCreating) { + return; + } + ward = await WardService().get(id: ward.id!); } loadHandler(future: loadOp()); @@ -58,11 +56,10 @@ class WardController extends LoadingChangeNotifier { } /// Add the [WardMinimal] - Future create(WardMinimal ward) async { + Future create() async { assert(isCreating); createOp() async { await WardService().create(ward: ward).then((value) { - wardId = value.id; ward = value; }); } diff --git a/packages/helpwave_service/lib/src/api/tasks/data_types/ward.dart b/packages/helpwave_service/lib/src/api/tasks/data_types/ward.dart index 1921e6f8..95bbf11c 100644 --- a/packages/helpwave_service/lib/src/api/tasks/data_types/ward.dart +++ b/packages/helpwave_service/lib/src/api/tasks/data_types/ward.dart @@ -1,14 +1,14 @@ +import 'package:helpwave_service/src/api/util/identified_object.dart'; + /// data class for [Ward] -class WardMinimal { - String id; +class WardMinimal extends IdentifiedObject { String name; WardMinimal({ - required this.id, + super.id, required this.name, }); - bool get isCreating => id == ""; WardMinimal copyWith({ String? id, @@ -22,7 +22,7 @@ class WardMinimal { @override String toString() { - return "{id: $id, name: $name}"; + return "$runtimeType{id: $id, name: $name}"; } } @@ -30,7 +30,7 @@ class Ward extends WardMinimal { String organizationId; Ward({ - required super.id, + super.id, required super.name, required this.organizationId, }); @@ -43,7 +43,7 @@ class WardOverview extends WardMinimal { int tasksInDone; WardOverview({ - required super.id, + super.id, required super.name, required this.bedCount, required this.tasksInTodo, diff --git a/packages/helpwave_service/lib/src/api/user/data_types/invitation.dart b/packages/helpwave_service/lib/src/api/user/data_types/invitation.dart index 6aae8bdd..6f690889 100644 --- a/packages/helpwave_service/lib/src/api/user/data_types/invitation.dart +++ b/packages/helpwave_service/lib/src/api/user/data_types/invitation.dart @@ -1,3 +1,5 @@ +import 'package:helpwave_service/src/api/util/identified_object.dart'; + import '../../../../user.dart'; enum InvitationState { @@ -8,8 +10,7 @@ enum InvitationState { revoked, } -class Invitation { - String id; +class Invitation extends IdentifiedObject { InvitationState state; /// The email of the invited [User] @@ -22,7 +23,7 @@ class Invitation { Organization? organization; Invitation({ - required this.id, + super.id, required this.organizationId, required this.email, required this.state, diff --git a/packages/helpwave_service/lib/src/api/user/data_types/organization.dart b/packages/helpwave_service/lib/src/api/user/data_types/organization.dart index 6ab7bb0e..8f977fbd 100644 --- a/packages/helpwave_service/lib/src/api/user/data_types/organization.dart +++ b/packages/helpwave_service/lib/src/api/user/data_types/organization.dart @@ -1,10 +1,11 @@ -class OrganizationMinimal { - String id; +import 'package:helpwave_service/src/api/util/identified_object.dart'; + +class OrganizationMinimal extends IdentifiedObject { String shortName; String longName; OrganizationMinimal({ - required this.id, + super.id, required this.shortName, required this.longName, }); @@ -30,7 +31,7 @@ class Organization extends OrganizationMinimal { bool isVerified; Organization({ - required super.id, + super.id, required super.shortName, required super.longName, required this.avatarURL, @@ -49,6 +50,7 @@ class Organization extends OrganizationMinimal { isVerified: false, ); + @override Organization copyWith({ String? id, String? shortName, @@ -73,6 +75,6 @@ class Organization extends OrganizationMinimal { @override String toString() { - return "{id: $id, name: $longName, shortName: $shortName}"; + return "$runtimeType{id: $id, name: $longName, shortName: $shortName}"; } } diff --git a/packages/helpwave_service/lib/src/api/user/data_types/user.dart b/packages/helpwave_service/lib/src/api/user/data_types/user.dart index 0abd1c09..e42a194b 100644 --- a/packages/helpwave_service/lib/src/api/user/data_types/user.dart +++ b/packages/helpwave_service/lib/src/api/user/data_types/user.dart @@ -1,25 +1,24 @@ +import 'package:helpwave_service/src/api/util/identified_object.dart'; + /// data class for [User] -class User { - String id; +class User extends IdentifiedObject { String name; String nickName; String email; Uri profileUrl; User({ - required this.id, + super.id, required this.name, required this.nickName, required this.email, required this.profileUrl, }); - factory User.empty({String id = ""}) => User(id: id, name: "", nickName: "", email: "", profileUrl: Uri.base); - - bool get isCreating => id == ""; + factory User.empty({String? id}) => User(id: id, name: "User", nickName: "", email: "", profileUrl: Uri.base); - User copyWith({String? name, String? nickName, Uri? profileUrl, String? email}) => User( - id: id, + User copyWith({String? id, String? name, String? nickName, Uri? profileUrl, String? email}) => User( + id: id ?? this.id, name: name ?? this.name, nickName: nickName ?? this.nickName, profileUrl: profileUrl ?? this.profileUrl, @@ -34,4 +33,9 @@ class User { @override int get hashCode => id.hashCode; + + @override + String toString() { + return "$runtimeType{id: $id, name: $name}"; + } } diff --git a/packages/helpwave_service/lib/src/api/user/offline_clients/organization_offline_client.dart b/packages/helpwave_service/lib/src/api/user/offline_clients/organization_offline_client.dart index 2c52669f..e3b39978 100644 --- a/packages/helpwave_service/lib/src/api/user/offline_clients/organization_offline_client.dart +++ b/packages/helpwave_service/lib/src/api/user/offline_clients/organization_offline_client.dart @@ -91,24 +91,6 @@ class OrganizationOfflineClient extends OrganizationServiceClient { return MockResponseFuture.value(CreateOrganizationResponse()..id = newOrganization.id); } - @override - ResponseFuture createOrganizationForUser(CreateOrganizationForUserRequest request, - {CallOptions? options}) { - final newOrganization = Organization( - id: DateTime.now().millisecondsSinceEpoch.toString(), - shortName: request.shortName, - longName: request.longName, - avatarURL: 'https://helpwave.de/favicon.ico', - email: request.contactEmail, - isPersonal: request.isPersonal, - isVerified: true, - ); - - OfflineClientStore().organizationStore.create(newOrganization); - - return MockResponseFuture.value(CreateOrganizationForUserResponse()..id = newOrganization.id); - } - @override ResponseFuture getOrganization(GetOrganizationRequest request, {CallOptions? options}) { final organization = OfflineClientStore().organizationStore.find(request.id); diff --git a/packages/helpwave_service/lib/src/auth/identity.dart b/packages/helpwave_service/lib/src/auth/identity.dart index 0540be01..c10376eb 100644 --- a/packages/helpwave_service/lib/src/auth/identity.dart +++ b/packages/helpwave_service/lib/src/auth/identity.dart @@ -31,16 +31,16 @@ class Identity { /// The default login data factory Identity.defaultIdentity() { return Identity( - idToken: "eyJzdWIiOiIxODE1OTcxMy01ZDRlLTRhZDUtOTRhZC1mYmI2YmIxNDc5ODQiLCJlbWFpbCI6Im1heC5tdXN0ZXJtYW5uQGhlbHB" - "3YXZlLmRlIiwiZW1haWxfdmVyaWZpZWQiOnRydWUsIm5hbWUiOiJNYXggTXVzdGVybWFubiIsInByZWZlcnJlZF91c2Vybm" - "FtZSI6Im1heC5tdXN0ZXJtYW5uIiwiZ2l2ZW5fbmFtZSI6Ik1heCIsImZhbWlseV9uYW1lIjoiTXVzdGVybWFubiIsIm9yZ2FuaXphdG" - "lvbiI6eyJpZCI6IjNiMjVjNmY1LTQ3MDUtNDA3NC05ZmM2LWE1MGMyOGViYTQwNiIsIm5hbWUiOiJoZWxwd2F2ZSB0ZXN0In19", + idToken: "eyJzdWIiOiIxODE1OTcxMy01ZDRlLTRhZDUtOTRhZC1mYmI2YmIxNDc5ODQiLCJlbWFpbCI6Im1heC5tdXN0ZXJtYW5uQGhlbHB3Y" + "XZlLmRlIiwiZW1haWxfdmVyaWZpZWQiOnRydWUsIm5hbWUiOiJNYXggTXVzdGVybWFubiIsInByZWZlcnJlZF91c2VybmFtZSI6Im1he" + "C5tdXN0ZXJtYW5uIiwiZ2l2ZW5fbmFtZSI6Ik1heCIsImZhbWlseV9uYW1lIjoiTXVzdGVybWFubiIsIm9yZ2FuaXphdGlvbiI6ey" + "JpZCI6IjNiMjVjNmY1LTQ3MDUtNDA3NC05ZmM2LWE1MGMyOGViYTQwNiIsIm5hbWUiOiJoZWxwd2F2ZSB0ZXN0In19", // TODO add a default here - id: "bb560b25-45b6-4a84-87f1-408e8c32dec8", + id: "18159713-5d4e-4ad5-94ad-fbb6bb147984", name: "Max Mustermann", nickName: "Max M.", email: "max.mustermann@helpwave.de", - organizations: ['bb560b25-45b6-4a84-87f1-408e8c32dec8']); + organizations: ['3b25c6f5-4705-4074-9fc6-a50c28eba406']); } Identity({ diff --git a/packages/helpwave_service/pubspec.yaml b/packages/helpwave_service/pubspec.yaml index e25a6e2b..5ca01d29 100644 --- a/packages/helpwave_service/pubspec.yaml +++ b/packages/helpwave_service/pubspec.yaml @@ -17,7 +17,7 @@ dependencies: flutter_secure_storage: 9.0.0 jose: ^0.3.4 logger: ^2.0.2+1 - helpwave_proto_dart: ^0.56.0-29ad8c8 + helpwave_proto_dart: ^0.58.0-1191f09 grpc: ^3.2.4 helpwave_util: path: "../helpwave_util" diff --git a/packages/helpwave_widget/lib/src/widgets/forward_navigation_tile.dart b/packages/helpwave_widget/lib/src/widgets/forward_navigation_tile.dart new file mode 100644 index 00000000..2682ade3 --- /dev/null +++ b/packages/helpwave_widget/lib/src/widgets/forward_navigation_tile.dart @@ -0,0 +1,51 @@ +import 'package:flutter/material.dart'; +import 'package:helpwave_theme/util.dart'; + +class ForwardNavigationTile extends StatelessWidget { + final IconData? icon; + final Color? color; + final String title; + final String? subtitle; + final void Function()? onTap; + final String? trailingText; + + const ForwardNavigationTile({ + super.key, + this.icon, + this.color, + required this.title, + this.subtitle, + this.onTap, + this.trailingText, + }); + + @override + Widget build(BuildContext context) { + return ListTile( + leading: icon != null ? Icon( + icon, + color: color ?? context.theme.colorScheme.primary, + ) : null, + onTap: onTap, + title: Text(title, style: TextStyle(fontWeight: FontWeight.bold, color: color)), + subtitle: subtitle != null ? Text(subtitle!) : null, + trailing: Row( + mainAxisSize: MainAxisSize.min, + mainAxisAlignment: MainAxisAlignment.end, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + trailingText != null + ? Text( + trailingText!, + style: context.theme.textTheme.labelLarge, + ) + : const SizedBox(), + Icon( + Icons.chevron_right_rounded, + color: context.theme.colorScheme.onBackground.withOpacity(0.7), + ), + ], + ), + ); + } +} diff --git a/packages/helpwave_widget/lib/src/widgets/index.dart b/packages/helpwave_widget/lib/src/widgets/index.dart index 8e0ad7fc..cc0ab08f 100644 --- a/packages/helpwave_widget/lib/src/widgets/index.dart +++ b/packages/helpwave_widget/lib/src/widgets/index.dart @@ -1,3 +1,4 @@ export 'fallback_avatar.dart'; export 'list_tile_card.dart'; export 'pressable_text.dart'; +export 'forward_navigation_tile.dart'; From f21d947c573ee1694b7cee3429324b6573fe87d1 Mon Sep 17 00:00:00 2001 From: Felix Thape Date: Tue, 19 Nov 2024 16:10:29 +0100 Subject: [PATCH 26/36] feat: make localizations easier to use --- .../bottom_sheet_pages/assignee_select.dart | 4 +- .../organization_bottom_sheet.dart | 20 +++---- .../organization_members_bottom_sheet.dart | 4 +- .../patient_bottom_sheet.dart | 22 +++---- .../properties_bottom_sheet.dart | 2 +- .../property_bottom_sheet.dart | 32 +++++----- .../room_overview_bottom_sheet.dart | 12 ++-- .../rooms_bottom_sheet.dart | 6 +- .../bottom_sheet_pages/task_bottom_sheet.dart | 16 ++--- .../task_template_bottom_sheet.dart | 12 ++-- .../task_templates_bottom_sheet.dart | 4 +- .../bottom_sheet_pages/user_bottom_sheet.dart | 6 +- .../bottom_sheet_pages/ward_bottom_sheet.dart | 20 +++---- .../ward_select_bottom_sheet.dart | 2 +- .../wards_bottom_sheet_page.dart | 2 +- .../lib/components/navigation_drawer.dart | 4 +- apps/tasks/lib/components/patient_card.dart | 2 +- .../lib/components/patient_selector.dart | 2 +- .../patient_status_chip_select.dart | 8 +-- apps/tasks/lib/components/subtask_list.dart | 12 ++-- apps/tasks/lib/components/task_card.dart | 10 ++-- .../lib/components/visibility_select.dart | 14 ++--- apps/tasks/lib/debug/theme_visualizer.dart | 2 +- apps/tasks/lib/screens/landing_screen.dart | 2 +- apps/tasks/lib/screens/main_screen.dart | 10 ++-- .../my_tasks_screen.dart | 6 +- .../patient_screen.dart | 6 +- apps/tasks/lib/screens/settings_screen.dart | 58 +++++++++---------- .../tasks/lib/screens/ward_select_screen.dart | 16 ++--- .../lib/util/field_type_translations.dart | 14 ++--- .../lib/util/subject_type_translations.dart | 4 +- .../lib/src/localization_impl.dart | 2 +- .../content_selection/content_selector.dart | 4 +- .../src/content_selection/list_search.dart | 10 ++-- .../helpwave_widget/lib/src/dialog_impl.dart | 4 +- .../lib/src/lists/rounded_list_tiles.dart | 2 +- .../lib/src/loading/load_error_widget.dart | 2 +- .../lib/src/loading/loading_spinner.dart | 4 +- 38 files changed, 181 insertions(+), 181 deletions(-) diff --git a/apps/tasks/lib/components/bottom_sheet_pages/assignee_select.dart b/apps/tasks/lib/components/bottom_sheet_pages/assignee_select.dart index 230a0dce..447e3ed0 100644 --- a/apps/tasks/lib/components/bottom_sheet_pages/assignee_select.dart +++ b/apps/tasks/lib/components/bottom_sheet_pages/assignee_select.dart @@ -22,13 +22,13 @@ class AssigneeSelectBottomSheet extends StatelessWidget { Widget build(BuildContext context) { return BottomSheetBase( header: BottomSheetHeader( - titleText: context.localization!.assignee, + titleText: context.localization.assignee, ), onClosing: () => {}, child: Column( children: [ TextButton( - child: Text(context.localization!.remove), + child: Text(context.localization.remove), onPressed: () => onChanged(null), ), const SizedBox(height: 10), diff --git a/apps/tasks/lib/components/bottom_sheet_pages/organization_bottom_sheet.dart b/apps/tasks/lib/components/bottom_sheet_pages/organization_bottom_sheet.dart index 4490ff6c..8ff061af 100644 --- a/apps/tasks/lib/components/bottom_sheet_pages/organization_bottom_sheet.dart +++ b/apps/tasks/lib/components/bottom_sheet_pages/organization_bottom_sheet.dart @@ -42,21 +42,21 @@ class OrganizationBottomSheetPage extends StatelessWidget { shrinkWrap: true, children: [ const SizedBox(height: distanceMedium), - Text(context.localization!.shortName, style: context.theme.textTheme.titleMedium), + Text(context.localization.shortName, style: context.theme.textTheme.titleMedium), const SizedBox(height: distanceTiny), TextFormFieldWithTimer( initialValue: controller.organization.shortName, onUpdate: (value) => controller.update(shortName: value), ), const SizedBox(height: distanceMedium), - Text(context.localization!.longName, style: context.theme.textTheme.titleMedium), + Text(context.localization.longName, style: context.theme.textTheme.titleMedium), const SizedBox(height: distanceTiny), TextFormFieldWithTimer( initialValue: controller.organization.longName, onUpdate: (value) => controller.update(longName: value), ), const SizedBox(height: distanceMedium), - Text(context.localization!.contactEmail, style: context.theme.textTheme.titleMedium), + Text(context.localization.contactEmail, style: context.theme.textTheme.titleMedium), const SizedBox(height: distanceTiny), TextFormFieldWithTimer( initialValue: controller.organization.email, @@ -64,13 +64,13 @@ class OrganizationBottomSheetPage extends StatelessWidget { onUpdate: (value) => controller.update(email: value), ), const SizedBox(height: distanceMedium), - Text(context.localization!.settings, style: context.theme.textTheme.titleMedium), + Text(context.localization.settings, style: context.theme.textTheme.titleMedium), const SizedBox(height: distanceTiny), RoundedListTiles( children: [ ForwardNavigationTile( icon: Icons.house_rounded, - title: context.localization!.wards, + title: context.localization.wards, onTap: () { NavigationStackController.of(context) .push(WardsBottomSheetPage(organizationId: organizationId)); @@ -78,7 +78,7 @@ class OrganizationBottomSheetPage extends StatelessWidget { ), ForwardNavigationTile( icon: Icons.person, - title: context.localization!.members, + title: context.localization.members, onTap: () { NavigationStackController.of(context) .push(OrganizationMembersBottomSheetPage(organizationId: organizationId)); @@ -86,7 +86,7 @@ class OrganizationBottomSheetPage extends StatelessWidget { ), ForwardNavigationTile( icon: Icons.label, - title: context.localization!.properties, + title: context.localization.properties, onTap: () { NavigationStackController.of(context).push(const PropertiesBottomSheet()); }, @@ -94,13 +94,13 @@ class OrganizationBottomSheetPage extends StatelessWidget { ], ), const SizedBox(height: distanceMedium), - Text(context.localization!.dangerZone, style: context.theme.textTheme.titleMedium), + Text(context.localization.dangerZone, style: context.theme.textTheme.titleMedium), Text( - context.localization!.organizationDangerZoneDescription, + context.localization.organizationDangerZoneDescription, style: TextStyle(color: context.theme.hintColor), ), PressableText( - text: "${context.localization!.delete} ${context.localization!.organization}", + text: "${context.localization.delete} ${context.localization.organization}", style: const TextStyle(color: Colors.red, fontWeight: FontWeight.w700), // TODO get from theme onPressed: () { // TODO show modal and delete organization diff --git a/apps/tasks/lib/components/bottom_sheet_pages/organization_members_bottom_sheet.dart b/apps/tasks/lib/components/bottom_sheet_pages/organization_members_bottom_sheet.dart index 4f3e1be4..fbe5d59a 100644 --- a/apps/tasks/lib/components/bottom_sheet_pages/organization_members_bottom_sheet.dart +++ b/apps/tasks/lib/components/bottom_sheet_pages/organization_members_bottom_sheet.dart @@ -24,7 +24,7 @@ class OrganizationMembersBottomSheetPage extends StatelessWidget { title: Column( mainAxisSize: MainAxisSize.min, children: [ - Text(context.localization!.members, style: context.theme.textTheme.titleMedium), + Text(context.localization.members, style: context.theme.textTheme.titleMedium), Text(CurrentWardService().currentWard?.organizationName ?? "", style: TextStyle(color: context.theme.hintColor)), ], @@ -70,7 +70,7 @@ class OrganizationMembersBottomSheetPage extends StatelessWidget { Padding( padding: const EdgeInsets.only(top: distanceMedium, bottom: distanceTiny), child: Text( - context.localization!.invitations, + context.localization.invitations, style: context.theme.textTheme.titleMedium, ), ), diff --git a/apps/tasks/lib/components/bottom_sheet_pages/patient_bottom_sheet.dart b/apps/tasks/lib/components/bottom_sheet_pages/patient_bottom_sheet.dart index 3ae537f6..d81bed70 100644 --- a/apps/tasks/lib/components/bottom_sheet_pages/patient_bottom_sheet.dart +++ b/apps/tasks/lib/components/bottom_sheet_pages/patient_bottom_sheet.dart @@ -85,7 +85,7 @@ class _PatientBottomSheetState extends State { FilledButton( style: buttonStyleBig, onPressed: patientController.create, - child: Text(context.localization!.create), + child: Text(context.localization.create), ) ] : [ @@ -107,7 +107,7 @@ class _PatientBottomSheetState extends State { context: context, ), ), - child: Text(context.localization!.unassigne), + child: Text(context.localization.unassigne), ), ), SizedBox( @@ -120,7 +120,7 @@ class _PatientBottomSheetState extends State { showDialog( context: context, builder: (context) => - AcceptDialog(titleText: context.localization!.dischargePatient), + AcceptDialog(titleText: context.localization.dischargePatient), ).then((value) { if (value) { patientController.discharge(); @@ -137,7 +137,7 @@ class _PatientBottomSheetState extends State { context: context, ), ), - child: Text(context.localization!.discharge), + child: Text(context.localization.discharge), ), ), ], @@ -155,7 +155,7 @@ class _PatientBottomSheetState extends State { thenBuilder: (context, beds) { if (beds.isEmpty) { return Text( - context.localization!.noFreeBeds, + context.localization.noFreeBeds, style: TextStyle(color: context.theme.disabledColor, fontWeight: FontWeight.bold), ); } @@ -165,7 +165,7 @@ class _PatientBottomSheetState extends State { padding: EdgeInsets.zero, isDense: true, hint: Text( - context.localization!.assignBed, + context.localization.assignBed, style: TextStyle(color: context.theme.colorScheme.primary.withOpacity(0.6)), ), value: beds.where((beds) => beds.bed.id == patientController.patient.bed?.id).firstOrNull, @@ -192,7 +192,7 @@ class _PatientBottomSheetState extends State { }), ), Text( - context.localization!.notes, + context.localization.notes, style: const TextStyle(fontSize: fontSizeBig, fontWeight: FontWeight.bold), ), const SizedBox(height: distanceSmall), @@ -226,7 +226,7 @@ class _PatientBottomSheetState extends State { patient: patient, )) .toList(), - title: context.localization!.upcoming, + title: context.localization.upcoming, color: upcomingColor, ); } @@ -238,7 +238,7 @@ class _PatientBottomSheetState extends State { patient: patient, )) .toList(), - title: context.localization!.inProgress, + title: context.localization.inProgress, color: inProgressColor, ); } @@ -249,12 +249,12 @@ class _PatientBottomSheetState extends State { patient: patient, )) .toList(), - title: context.localization!.done, + title: context.localization.done, color: doneColor, ); }, title: Text( - context.localization!.tasks, + context.localization.tasks, style: const TextStyle(fontSize: fontSizeBig, fontWeight: FontWeight.bold), ), // TODO use return value to add it to task list or force a refetch diff --git a/apps/tasks/lib/components/bottom_sheet_pages/properties_bottom_sheet.dart b/apps/tasks/lib/components/bottom_sheet_pages/properties_bottom_sheet.dart index 15d36193..0d5e38d8 100644 --- a/apps/tasks/lib/components/bottom_sheet_pages/properties_bottom_sheet.dart +++ b/apps/tasks/lib/components/bottom_sheet_pages/properties_bottom_sheet.dart @@ -26,7 +26,7 @@ class PropertiesBottomSheet extends StatelessWidget { title: Column( mainAxisSize: MainAxisSize.min, children: [ - Text(context.localization!.properties, style: context.theme.textTheme.titleMedium), + Text(context.localization.properties, style: context.theme.textTheme.titleMedium), wardId == null ? const SizedBox() : LoadingFutureBuilder( diff --git a/apps/tasks/lib/components/bottom_sheet_pages/property_bottom_sheet.dart b/apps/tasks/lib/components/bottom_sheet_pages/property_bottom_sheet.dart index 0e734550..7d1b983d 100644 --- a/apps/tasks/lib/components/bottom_sheet_pages/property_bottom_sheet.dart +++ b/apps/tasks/lib/components/bottom_sheet_pages/property_bottom_sheet.dart @@ -24,8 +24,8 @@ class PropertyBottomSheetPage extends StatelessWidget { borderRadius: BorderRadius.circular(borderRadiusMedium), )); - String createTitle = "${context.localization!.create} ${context.localization!.property}"; - String editTitle = context.localization!.editProperty; + String createTitle = "${context.localization.create} ${context.localization.property}"; + String editTitle = context.localization.editProperty; return ChangeNotifierProvider( create: (context) => PropertyController(id: id), @@ -48,14 +48,14 @@ class PropertyBottomSheetPage extends StatelessWidget { child: ListView( children: [ Text( - context.localization!.basic, + context.localization.basic, style: context.theme.textTheme.titleMedium?.copyWith( color: context.theme.colorScheme.primary, ), ), const SizedBox(height: paddingSmall), Text( - context.localization!.name, + context.localization.name, style: context.theme.textTheme.titleSmall, ), const SizedBox(height: paddingTiny), @@ -65,7 +65,7 @@ class PropertyBottomSheetPage extends StatelessWidget { ), const SizedBox(height: paddingSmall), Text( - context.localization!.subjectType, + context.localization.subjectType, style: context.theme.textTheme.titleSmall, ), const SizedBox(height: paddingTiny), @@ -82,7 +82,7 @@ class PropertyBottomSheetPage extends StatelessWidget { ), const SizedBox(height: paddingSmall), Text( - context.localization!.description, + context.localization.description, style: context.theme.textTheme.titleSmall, ), const SizedBox(height: paddingTiny), @@ -94,14 +94,14 @@ class PropertyBottomSheetPage extends StatelessWidget { const SizedBox(height: paddingMedium), // Field section Text( - context.localization!.field, + context.localization.field, style: context.theme.textTheme.titleMedium?.copyWith( color: context.theme.colorScheme.primary, ), ), const SizedBox(height: paddingSmall), Text( - "${context.localization!.field} ${context.localization!.type}", + "${context.localization.field} ${context.localization.type}", style: context.theme.textTheme.titleSmall, ), const SizedBox(height: paddingTiny), @@ -126,7 +126,7 @@ class PropertyBottomSheetPage extends StatelessWidget { ExpansionTile( shape: const Border(), title: Text( - "${controller.property.selectData?.options.length} ${context.localization!.options}"), + "${controller.property.selectData?.options.length} ${context.localization.options}"), children: (controller.property.selectData?.options ?? []) .map((selectOption) => ListTile(title: Text(selectOption.name))) .toList(), @@ -140,11 +140,11 @@ class PropertyBottomSheetPage extends StatelessWidget { children: [ ListTile( title: Text( - context.localization!.allowCustomValues, + context.localization.allowCustomValues, style: context.theme.textTheme.titleSmall, ), subtitle: Text( - context.localization!.allowCustomValuesDescription, + context.localization.allowCustomValuesDescription, style: TextStyle(color: context.theme.hintColor), ), trailing: Switch( @@ -166,14 +166,14 @@ class PropertyBottomSheetPage extends StatelessWidget { const SizedBox(height: paddingMedium), // Rules Text( - context.localization!.rules, + context.localization.rules, style: context.theme.textTheme.titleMedium?.copyWith( color: context.theme.colorScheme.primary, ), ), const SizedBox(height: paddingSmall), Text( - context.localization!.importance, + context.localization.importance, style: context.theme.textTheme.titleSmall, ), const SizedBox(height: paddingTiny), @@ -181,11 +181,11 @@ class PropertyBottomSheetPage extends StatelessWidget { children: [ ListTile( title: Text( - context.localization!.alwaysVisible, + context.localization.alwaysVisible, style: context.theme.textTheme.titleSmall, ), subtitle: Text( - context.localization!.alwaysVisibleDescription, + context.localization.alwaysVisibleDescription, style: TextStyle(color: context.theme.hintColor), ), trailing: Switch( @@ -206,7 +206,7 @@ class PropertyBottomSheetPage extends StatelessWidget { child: FilledButton( onPressed: () => controller.create(), child: Text( - context.localization!.create, + context.localization.create, ), ), ), diff --git a/apps/tasks/lib/components/bottom_sheet_pages/room_overview_bottom_sheet.dart b/apps/tasks/lib/components/bottom_sheet_pages/room_overview_bottom_sheet.dart index 44f7d28c..9f259c25 100644 --- a/apps/tasks/lib/components/bottom_sheet_pages/room_overview_bottom_sheet.dart +++ b/apps/tasks/lib/components/bottom_sheet_pages/room_overview_bottom_sheet.dart @@ -28,7 +28,7 @@ class RoomOverviewBottomSheetPage extends StatelessWidget { title: Column( mainAxisSize: MainAxisSize.min, children: [ - Text(context.localization!.rooms, style: context.theme.textTheme.titleMedium), + Text(context.localization.rooms, style: context.theme.textTheme.titleMedium), Consumer(builder: (context, controller, child) { return LoadingAndErrorWidget( state: controller.state, @@ -51,7 +51,7 @@ class RoomOverviewBottomSheetPage extends StatelessWidget { mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, children: [ - Text(context.localization!.name, style: context.theme.textTheme.titleSmall), + Text(context.localization.name, style: context.theme.textTheme.titleSmall), const SizedBox(height: distanceTiny), TextFormFieldWithTimer( initialValue: controller.state == LoadingState.loaded ? controller.room.name : "", @@ -66,7 +66,7 @@ class RoomOverviewBottomSheetPage extends StatelessWidget { Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ - Text(context.localization!.beds, style: context.theme.textTheme.titleSmall), + Text(context.localization.beds, style: context.theme.textTheme.titleSmall), Consumer( builder: (context, controller, _) { return LoadingAndErrorWidget( @@ -74,9 +74,9 @@ class RoomOverviewBottomSheetPage extends StatelessWidget { loadingWidget: const PulsingContainer(width: 40), child: TextButton( onPressed: () => { - controller.create(Bed(id: "", name: context.localization!.newBed, roomId: roomId)) + controller.create(Bed(id: "", name: context.localization.newBed, roomId: roomId)) }, - child: Text("+ ${context.localization!.add} ${context.localization!.bed}"), + child: Text("+ ${context.localization.add} ${context.localization.bed}"), ), ); }, @@ -94,7 +94,7 @@ class RoomOverviewBottomSheetPage extends StatelessWidget { (bed) => ListTile( leading: const Icon(Icons.bed_rounded), title: Text(bed.name), - trailing: Text(bed.patient?.name ?? context.localization!.unassigned), + trailing: Text(bed.patient?.name ?? context.localization.unassigned), ), ) .toList(), diff --git a/apps/tasks/lib/components/bottom_sheet_pages/rooms_bottom_sheet.dart b/apps/tasks/lib/components/bottom_sheet_pages/rooms_bottom_sheet.dart index 83c3f7fb..2c3bec0e 100644 --- a/apps/tasks/lib/components/bottom_sheet_pages/rooms_bottom_sheet.dart +++ b/apps/tasks/lib/components/bottom_sheet_pages/rooms_bottom_sheet.dart @@ -27,7 +27,7 @@ class RoomsBottomSheetPage extends StatelessWidget { title: Column( mainAxisSize: MainAxisSize.min, children: [ - Text(context.localization!.rooms, style: context.theme.textTheme.titleMedium), + Text(context.localization.rooms, style: context.theme.textTheme.titleMedium), LoadingFutureBuilder( future: WardService().get(id: wardId), loadingWidget: const PulsingContainer(width: 50), @@ -41,7 +41,7 @@ class RoomsBottomSheetPage extends StatelessWidget { icon: Icons.add, onPressed: () { controller - .create(RoomWithBedWithMinimalPatient(id: "", name: context.localization!.newRoom, beds: [])); + .create(RoomWithBedWithMinimalPatient(id: "", name: context.localization.newRoom, beds: [])); }, ), ), @@ -57,7 +57,7 @@ class RoomsBottomSheetPage extends StatelessWidget { (room) => ForwardNavigationTile( icon: Icons.meeting_room_rounded, title: room.name, - trailingText: "${room.beds.length} ${context.localization!.beds}", + trailingText: "${room.beds.length} ${context.localization.beds}", onTap: () { NavigationStackController.of(context) .push(RoomOverviewBottomSheetPage(roomId: room.id)); diff --git a/apps/tasks/lib/components/bottom_sheet_pages/task_bottom_sheet.dart b/apps/tasks/lib/components/bottom_sheet_pages/task_bottom_sheet.dart index 7cc3fb10..b009a7cf 100644 --- a/apps/tasks/lib/components/bottom_sheet_pages/task_bottom_sheet.dart +++ b/apps/tasks/lib/components/bottom_sheet_pages/task_bottom_sheet.dart @@ -158,7 +158,7 @@ class _TaskBottomSheetState extends State { }); } : null, - child: Text(context.localization!.create), + child: Text(context.localization.create), ), ), ), @@ -194,7 +194,7 @@ class _TaskBottomSheetState extends State { Consumer(builder: (context, taskController, __) { return _SheetListTile( icon: Icons.person, - label: context.localization!.assignedTo, + label: context.localization.assignedTo, onTap: () => context.pushModal( context: context, builder: (BuildContext context) => AssigneeSelectBottomSheet( @@ -219,7 +219,7 @@ class _TaskBottomSheetState extends State { ), ) : Text( - context.localization!.unassigned, + context.localization.unassigned, style: editableValueTextStyle(context), ), ); @@ -230,7 +230,7 @@ class _TaskBottomSheetState extends State { loadingWidget: const PulsingContainer(width: 60, height: 24), child: _SheetListTile( icon: Icons.access_time, - label: context.localization!.due, + label: context.localization.due, // TODO localization and date formatting here valueWidget: Builder(builder: (context) { DateTime? dueDate = taskController.task.dueDate; @@ -248,7 +248,7 @@ class _TaskBottomSheetState extends State { ], ); } - return Text(context.localization!.none); + return Text(context.localization.none); }), onTap: () => showDatePicker( context: context, @@ -290,7 +290,7 @@ class _TaskBottomSheetState extends State { state: taskController.state, child: _SheetListTile( icon: Icons.lock, - label: context.localization!.visibility, + label: context.localization.visibility, valueWidget: VisibilitySelect( isPublicVisible: taskController.task.isPublicVisible, onChanged: taskController.changeIsPublic, @@ -302,7 +302,7 @@ class _TaskBottomSheetState extends State { ), const SizedBox(height: distanceMedium), Text( - context.localization!.notes, + context.localization.notes, style: const TextStyle(fontWeight: FontWeight.bold, fontSize: 16), ), const SizedBox(height: distanceTiny), @@ -327,7 +327,7 @@ class _TaskBottomSheetState extends State { maxLines: 6, decoration: InputDecoration( contentPadding: const EdgeInsets.all(paddingMedium), - hintText: context.localization!.yourNotes, + hintText: context.localization.yourNotes, ), ), ), diff --git a/apps/tasks/lib/components/bottom_sheet_pages/task_template_bottom_sheet.dart b/apps/tasks/lib/components/bottom_sheet_pages/task_template_bottom_sheet.dart index 33563628..2d7c2068 100644 --- a/apps/tasks/lib/components/bottom_sheet_pages/task_template_bottom_sheet.dart +++ b/apps/tasks/lib/components/bottom_sheet_pages/task_template_bottom_sheet.dart @@ -80,35 +80,35 @@ class TaskTemplateBottomSheetPage extends StatelessWidget { state: controller.state, child: ListView( children: [ - Text(context.localization!.name, style: context.theme.textTheme.titleSmall), + Text(context.localization.name, style: context.theme.textTheme.titleSmall), const SizedBox(height: distanceTiny), TextFormFieldWithTimer( initialValue: controller.taskTemplate.name, onUpdate: (value) => controller.update(name: value), ), const SizedBox(height: distanceMedium), - Text(context.localization!.notes, style: context.theme.textTheme.titleSmall), + Text(context.localization.notes, style: context.theme.textTheme.titleSmall), const SizedBox(height: distanceTiny), TextFormFieldWithTimer( initialValue: controller.taskTemplate.description, onUpdate: (value) => controller.update(description: value), maxLines: 6, decoration: - InputDecoration(hintText: "${context.localization!.add} ${context.localization!.notes}"), + InputDecoration(hintText: "${context.localization.add} ${context.localization.notes}"), ), const SizedBox(height: distanceMedium), Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ - Text(context.localization!.subtasks, style: context.theme.textTheme.titleSmall), + Text(context.localization.subtasks, style: context.theme.textTheme.titleSmall), TextButton( onPressed: () { controller.createSubtask(TaskTemplateSubtask( templateId: controller.templateId, - name: context.localization!.subtask, + name: context.localization.subtask, )); }, - child: Text("+ ${context.localization!.add} ${context.localization!.subtask}"), + child: Text("+ ${context.localization.add} ${context.localization.subtask}"), ), ], ), diff --git a/apps/tasks/lib/components/bottom_sheet_pages/task_templates_bottom_sheet.dart b/apps/tasks/lib/components/bottom_sheet_pages/task_templates_bottom_sheet.dart index 0446e268..a675753b 100644 --- a/apps/tasks/lib/components/bottom_sheet_pages/task_templates_bottom_sheet.dart +++ b/apps/tasks/lib/components/bottom_sheet_pages/task_templates_bottom_sheet.dart @@ -29,7 +29,7 @@ class TaskTemplatesBottomSheetPage extends StatelessWidget { mainAxisSize: MainAxisSize.min, children: [ Text( - context.localization!.taskTemplates, + context.localization.taskTemplates, style: const TextStyle(fontWeight: FontWeight.w800, fontSize: 16), ), wardId == null @@ -49,7 +49,7 @@ class TaskTemplatesBottomSheetPage extends StatelessWidget { onPressed: () { NavigationStackController.of(context).push(TaskTemplateBottomSheetPage( template: TaskTemplate( - name: context.localization!.task, + name: context.localization.task, wardId: wardId, isPublicVisible: !isPersonal, ), diff --git a/apps/tasks/lib/components/bottom_sheet_pages/user_bottom_sheet.dart b/apps/tasks/lib/components/bottom_sheet_pages/user_bottom_sheet.dart index 07ae6e01..305735ee 100644 --- a/apps/tasks/lib/components/bottom_sheet_pages/user_bottom_sheet.dart +++ b/apps/tasks/lib/components/bottom_sheet_pages/user_bottom_sheet.dart @@ -55,7 +55,7 @@ class UserBottomSheetPage extends StatelessWidget { }), Consumer( builder: (context, currentWardController, __) => Text( - currentWardController.currentWard?.organizationName ?? context.localization!.loading, + currentWardController.currentWard?.organizationName ?? context.localization.loading, style: TextStyle( fontSize: fontSizeSmall, color: context.theme.hintColor, @@ -70,7 +70,7 @@ class UserBottomSheetPage extends StatelessWidget { Icons.house_rounded, color: context.theme.colorScheme.primary, ), - title: Text(context.localization!.currentWard, style: const TextStyle(fontWeight: FontWeight.bold)), + title: Text(context.localization.currentWard, style: const TextStyle(fontWeight: FontWeight.bold)), trailing: Row( mainAxisSize: MainAxisSize.min, mainAxisAlignment: MainAxisAlignment.end, @@ -116,7 +116,7 @@ class UserBottomSheetPage extends StatelessWidget { currentWardService.clear(); }, child: Text( - context.localization!.logout, + context.localization.logout, ), ); }), diff --git a/apps/tasks/lib/components/bottom_sheet_pages/ward_bottom_sheet.dart b/apps/tasks/lib/components/bottom_sheet_pages/ward_bottom_sheet.dart index e486f05e..d26a6e55 100644 --- a/apps/tasks/lib/components/bottom_sheet_pages/ward_bottom_sheet.dart +++ b/apps/tasks/lib/components/bottom_sheet_pages/ward_bottom_sheet.dart @@ -30,7 +30,7 @@ class WardBottomSheetPage extends StatelessWidget { title: Consumer( builder: (context, controller, _) { if (controller.isCreating) { - return Text(context.localization!.createWard, style: context.theme.textTheme.titleMedium); + return Text(context.localization.createWard, style: context.theme.textTheme.titleMedium); } return LoadingAndErrorWidget( loadingWidget: const PulsingContainer(height: 20), @@ -51,7 +51,7 @@ class WardBottomSheetPage extends StatelessWidget { loadingWidget: const PulsingContainer(height: 80), child: ListView( children: [ - Text(context.localization!.name, style: context.theme.textTheme.titleSmall), + Text(context.localization.name, style: context.theme.textTheme.titleSmall), const SizedBox(height: distanceTiny), TextFormFieldWithTimer( initialValue: controller.state == LoadingState.loaded ? controller.ward.name : "", @@ -64,20 +64,20 @@ class WardBottomSheetPage extends StatelessWidget { crossAxisAlignment: CrossAxisAlignment.start, children: [ const SizedBox(height: distanceMedium), - Text(context.localization!.settings, style: context.theme.textTheme.titleMedium), + Text(context.localization.settings, style: context.theme.textTheme.titleMedium), const SizedBox(height: distanceTiny), RoundedListTiles( children: [ ForwardNavigationTile( icon: Icons.house_rounded, - title: context.localization!.rooms, + title: context.localization.rooms, onTap: () { NavigationStackController.of(context).push(RoomsBottomSheetPage(wardId: wardId!)); }, ), ForwardNavigationTile( icon: Icons.checklist_rounded, - title: context.localization!.taskTemplates, + title: context.localization.taskTemplates, onTap: () { NavigationStackController.of(context) .push(TaskTemplatesBottomSheetPage(wardId: wardId)); @@ -85,7 +85,7 @@ class WardBottomSheetPage extends StatelessWidget { ), ForwardNavigationTile( icon: Icons.label, - title: context.localization!.properties, + title: context.localization.properties, onTap: () { NavigationStackController.of(context).push(PropertiesBottomSheet(wardId: wardId)); }, @@ -93,13 +93,13 @@ class WardBottomSheetPage extends StatelessWidget { ], ), const SizedBox(height: distanceMedium), - Text(context.localization!.dangerZone, style: context.theme.textTheme.titleMedium), + Text(context.localization.dangerZone, style: context.theme.textTheme.titleMedium), Text( - context.localization!.organizationDangerZoneDescription, + context.localization.organizationDangerZoneDescription, style: TextStyle(color: context.theme.hintColor), ), PressableText( - text: "${context.localization!.delete} ${context.localization!.organization}", + text: "${context.localization.delete} ${context.localization.organization}", style: const TextStyle( color: Colors.red, fontWeight: FontWeight.w700), // TODO get from theme onPressed: () { @@ -120,7 +120,7 @@ class WardBottomSheetPage extends StatelessWidget { child: FilledButton( onPressed: () => controller.create(), child: Text( - context.localization!.create, + context.localization.create, ), ), ), diff --git a/apps/tasks/lib/components/bottom_sheet_pages/ward_select_bottom_sheet.dart b/apps/tasks/lib/components/bottom_sheet_pages/ward_select_bottom_sheet.dart index 1ecf1653..db2816bc 100644 --- a/apps/tasks/lib/components/bottom_sheet_pages/ward_select_bottom_sheet.dart +++ b/apps/tasks/lib/components/bottom_sheet_pages/ward_select_bottom_sheet.dart @@ -24,7 +24,7 @@ class WardSelectBottomSheet extends StatelessWidget { Widget build(BuildContext context) { return BottomSheetBase( onClosing: () {}, - header: BottomSheetHeader(titleText: context.localization!.selectWard), + header: BottomSheetHeader(titleText: context.localization.selectWard), mainAxisSize: MainAxisSize.min, child: LoadingFutureBuilder( loadingWidget: const SizedBox(), diff --git a/apps/tasks/lib/components/bottom_sheet_pages/wards_bottom_sheet_page.dart b/apps/tasks/lib/components/bottom_sheet_pages/wards_bottom_sheet_page.dart index b12e0971..d6eb281e 100644 --- a/apps/tasks/lib/components/bottom_sheet_pages/wards_bottom_sheet_page.dart +++ b/apps/tasks/lib/components/bottom_sheet_pages/wards_bottom_sheet_page.dart @@ -27,7 +27,7 @@ class WardsBottomSheetPage extends StatelessWidget { mainAxisSize: MainAxisSize.min, children: [ Text( - context.localization!.wards, + context.localization.wards, style: const TextStyle(fontWeight: FontWeight.w800, fontSize: 16), ), Text( diff --git a/apps/tasks/lib/components/navigation_drawer.dart b/apps/tasks/lib/components/navigation_drawer.dart index cfe12f1e..71e9ae1b 100644 --- a/apps/tasks/lib/components/navigation_drawer.dart +++ b/apps/tasks/lib/components/navigation_drawer.dart @@ -69,7 +69,7 @@ class TasksNavigationDrawer extends StatelessWidget { onTap: currentPage == NavigationOptions.myTasks ? null : () => pushReplace(context, const MyTasksScreen()), - title: Text(context.localization!.myTasks), + title: Text(context.localization.myTasks), ), ListTile( leading: const Icon(Icons.settings), @@ -77,7 +77,7 @@ class TasksNavigationDrawer extends StatelessWidget { onTap: currentPage == NavigationOptions.settings ? null : () => pushReplace(context, const SettingsScreen()), - title: Text(context.localization!.settings), + title: Text(context.localization.settings), ), Flexible(child: Container()), const Padding( diff --git a/apps/tasks/lib/components/patient_card.dart b/apps/tasks/lib/components/patient_card.dart index 28658333..cd4500b5 100644 --- a/apps/tasks/lib/components/patient_card.dart +++ b/apps/tasks/lib/components/patient_card.dart @@ -46,7 +46,7 @@ class PatientCard extends StatelessWidget { Text( patient.bed != null && patient.room != null ? "${patient.room?.name} - ${patient.bed?.name}" - : context.localization!.unassigned, + : context.localization.unassigned, style: const TextStyle( fontWeight: FontWeight.w400, ), diff --git a/apps/tasks/lib/components/patient_selector.dart b/apps/tasks/lib/components/patient_selector.dart index a6545d3c..df012d6a 100644 --- a/apps/tasks/lib/components/patient_selector.dart +++ b/apps/tasks/lib/components/patient_selector.dart @@ -23,7 +23,7 @@ class PatientSelector extends StatelessWidget { // removes the default underline padding: EdgeInsets.zero, hint: Text( - context.localization!.selectPatient, + context.localization.selectPatient, style: TextStyle(color: context.theme.colorScheme.primary.withOpacity(0.6)), ), isDense: true, diff --git a/apps/tasks/lib/components/patient_status_chip_select.dart b/apps/tasks/lib/components/patient_status_chip_select.dart index 093c5c2f..427c6660 100644 --- a/apps/tasks/lib/components/patient_status_chip_select.dart +++ b/apps/tasks/lib/components/patient_status_chip_select.dart @@ -49,10 +49,10 @@ class PatientStatusChipSelect extends StatelessWidget { onChange: (value) => onChange(_toPatientAssignmentStatus(value ?? PatientStatusChipSelectOptions.all)), labeling: (value) { var translationMap = { - PatientStatusChipSelectOptions.all: context.localization!.all, - PatientStatusChipSelectOptions.active: context.localization!.active, - PatientStatusChipSelectOptions.unassigned: context.localization!.unassigned, - PatientStatusChipSelectOptions.discharged: context.localization!.discharged, + PatientStatusChipSelectOptions.all: context.localization.all, + PatientStatusChipSelectOptions.active: context.localization.active, + PatientStatusChipSelectOptions.unassigned: context.localization.unassigned, + PatientStatusChipSelectOptions.discharged: context.localization.discharged, }; return translationMap[value]!; }, diff --git a/apps/tasks/lib/components/subtask_list.dart b/apps/tasks/lib/components/subtask_list.dart index 1c652479..ec3fccf0 100644 --- a/apps/tasks/lib/components/subtask_list.dart +++ b/apps/tasks/lib/components/subtask_list.dart @@ -37,7 +37,7 @@ class SubtaskList extends StatelessWidget { maxHeight: sizeForSubtasks, items: subtasksController.subtasks, title: Text( - context.localization!.subtasks, + context.localization.subtasks, style: const TextStyle(fontWeight: FontWeight.bold, fontSize: 16), ), onAdd: () => subtasksController @@ -80,7 +80,7 @@ class SubtaskList extends StatelessWidget { .then((value) => onChange(subtasksController.subtasks)); }, child: Text( - context.localization!.delete, + context.localization.delete, style: const TextStyle(color: negativeColor, fontSize: 17), ), ), @@ -116,7 +116,7 @@ class _SubTaskChangeDialogState extends State { children: [ Center( child: Text( - context.localization!.changeSubtask, + context.localization.changeSubtask, style: const TextStyle( fontSize: 20, fontWeight: FontWeight.bold, @@ -125,7 +125,7 @@ class _SubTaskChangeDialogState extends State { ), const SizedBox(height: distanceDefault), TextFormField( - decoration: InputDecoration(labelText: context.localization!.subtaskName), + decoration: InputDecoration(labelText: context.localization.subtaskName), initialValue: widget.initialName, onChanged: (value) => updatedName = value, ), @@ -135,11 +135,11 @@ class _SubTaskChangeDialogState extends State { children: [ TextButton( onPressed: () => Navigator.pop(context, null), - child: Text(context.localization!.cancel, style: TextStyle(color: context.theme.hintColor),), + child: Text(context.localization.cancel, style: TextStyle(color: context.theme.hintColor),), ), TextButton( onPressed: () => Navigator.pop(context, updatedName), - child: Text(context.localization!.update), + child: Text(context.localization.update), ) ], ), diff --git a/apps/tasks/lib/components/task_card.dart b/apps/tasks/lib/components/task_card.dart index d88d195a..18c78b7a 100644 --- a/apps/tasks/lib/components/task_card.dart +++ b/apps/tasks/lib/components/task_card.dart @@ -20,24 +20,24 @@ class TaskCard extends StatelessWidget { /// Determines the text shown for indicating the remaining time for the [Task] String getDueText(BuildContext context) { - String result = context.localization!.dueIn; + String result = context.localization.dueIn; if (task.dueDate == null) { return "$result -"; } if (task.isOverdue) { - return context.localization!.overdue; + return context.localization.overdue; } if (task.inNextHour) { - return "$result ${context.localization!.nMinutes(task.remainingTime.inMinutes)}"; + return "$result ${context.localization.nMinutes(task.remainingTime.inMinutes)}"; } if (task.inNextTwoDays) { - return "$result ${context.localization!.nHours(task.remainingTime.inHours)}"; + return "$result ${context.localization.nHours(task.remainingTime.inHours)}"; } - return "$result ${context.localization!.nDays(task.remainingTime.inDays)}"; + return "$result ${context.localization.nDays(task.remainingTime.inDays)}"; } /// Depending on the remaining time of the [task] the use a different background color diff --git a/apps/tasks/lib/components/visibility_select.dart b/apps/tasks/lib/components/visibility_select.dart index 4362f6a4..51c2a8d9 100644 --- a/apps/tasks/lib/components/visibility_select.dart +++ b/apps/tasks/lib/components/visibility_select.dart @@ -27,7 +27,7 @@ class _VisibilityBottomSheet extends StatelessWidget { ), mainAxisSize: MainAxisSize.min, header: BottomSheetHeader( - titleText: context.localization!.visibility, + titleText: context.localization.visibility, ), child: Column( children: [ @@ -37,7 +37,7 @@ class _VisibilityBottomSheet extends StatelessWidget { onChange(true); Navigator.of(context).pop(); }, - child: Text(context.localization!.public, style: style), + child: Text(context.localization.public, style: style), ), const SizedBox(height: distanceSmall), GestureDetector( @@ -45,7 +45,7 @@ class _VisibilityBottomSheet extends StatelessWidget { onChange(false); Navigator.of(context).pop(); }, - child: Text(context.localization!.private, style: style), + child: Text(context.localization.private, style: style), ), ], ), @@ -94,14 +94,14 @@ class VisibilitySelect extends StatelessWidget { if (isPublicVisible) { ScaffoldMessenger.of(context) - .showSnackBar(SnackBar(content: Text(context.localization!.publicTasksCannotBeMadePrivate))); + .showSnackBar(SnackBar(content: Text(context.localization.publicTasksCannotBeMadePrivate))); } else { showDialog( context: context, builder: (context) => AcceptDialog( - titleText: context.localization!.makeTaskPublic, + titleText: context.localization.makeTaskPublic, content: Text( - context.localization!.thisCannotBeUndone, + context.localization.thisCannotBeUndone, // TODO replace with warning color by theme style: const TextStyle(color: warningColor), ), @@ -110,7 +110,7 @@ class VisibilitySelect extends StatelessWidget { } }, child: Text( - isPublicVisible ? context.localization!.public : context.localization!.private, + isPublicVisible ? context.localization.public : context.localization.private, style: textStyle, ), ); diff --git a/apps/tasks/lib/debug/theme_visualizer.dart b/apps/tasks/lib/debug/theme_visualizer.dart index be3cee9f..2460446d 100644 --- a/apps/tasks/lib/debug/theme_visualizer.dart +++ b/apps/tasks/lib/debug/theme_visualizer.dart @@ -17,7 +17,7 @@ class ThemeVisualizer extends StatelessWidget { children: [ ListTile( leading: const Icon(Icons.brightness_medium), - title: Text(context.localization!.darkMode), + title: Text(context.localization.darkMode), trailing: Consumer(builder: (_, ThemeModel themeNotifier, __) { return Switch( value: themeNotifier.getIsDarkNullSafe(context), diff --git a/apps/tasks/lib/screens/landing_screen.dart b/apps/tasks/lib/screens/landing_screen.dart index b6d4a1ee..6e2184cc 100644 --- a/apps/tasks/lib/screens/landing_screen.dart +++ b/apps/tasks/lib/screens/landing_screen.dart @@ -28,7 +28,7 @@ class LandingScreen extends StatelessWidget { style: buttonStyleBig.copyWith(side: MaterialStatePropertyAll(buttonBorderSideBig.copyWith(color: context .theme.colorScheme.onBackground))), child: Text( - context.localization!.loginSlogan, + context.localization.loginSlogan, style: context.theme.textTheme.labelLarge, ), onPressed: () { diff --git a/apps/tasks/lib/screens/main_screen.dart b/apps/tasks/lib/screens/main_screen.dart index e6befd3c..bff9b5a7 100644 --- a/apps/tasks/lib/screens/main_screen.dart +++ b/apps/tasks/lib/screens/main_screen.dart @@ -59,15 +59,15 @@ class _MainScreenState extends State { destinations: [ NavigationDestination( icon: const Icon(Icons.check_circle_outline), - label: context.localization!.myTasks, + label: context.localization.myTasks, ), NavigationDestination( icon: const Icon(Icons.add_circle_outline), - label: context.localization!.newTaskOrPatient, + label: context.localization.newTaskOrPatient, ), NavigationDestination( icon: const Icon(Icons.person), - label: context.localization!.patients, + label: context.localization.patients, ), ], selectedIndex: index, @@ -127,7 +127,7 @@ class _TaskPatientFloatingActionButton extends StatelessWidget { label: SizedBox( width: chipWidth, height: chipHeight, - child: Center(child: Text(context.localization!.task)), + child: Center(child: Text(context.localization.task)), ), onPressed: () { context.pushModal( @@ -151,7 +151,7 @@ class _TaskPatientFloatingActionButton extends StatelessWidget { label: SizedBox( width: chipWidth, height: chipHeight, - child: Center(child: Text(context.localization!.patient)), + child: Center(child: Text(context.localization.patient)), ), onPressed: () { context.pushModal( diff --git a/apps/tasks/lib/screens/main_screen_subscreens/my_tasks_screen.dart b/apps/tasks/lib/screens/main_screen_subscreens/my_tasks_screen.dart index 7db67b62..1e1942a2 100644 --- a/apps/tasks/lib/screens/main_screen_subscreens/my_tasks_screen.dart +++ b/apps/tasks/lib/screens/main_screen_subscreens/my_tasks_screen.dart @@ -36,17 +36,17 @@ class _MyTasksScreenState extends State { TaskExpansionTile( tasks: tasksController.todo, color: upcomingColor, - title: context.localization!.upcoming, + title: context.localization.upcoming, ), TaskExpansionTile( tasks: tasksController.inProgress, color: inProgressColor, - title: context.localization!.inProgress, + title: context.localization.inProgress, ), TaskExpansionTile( tasks: tasksController.done, color: doneColor, - title: context.localization!.done, + title: context.localization.done, ), ], ), diff --git a/apps/tasks/lib/screens/main_screen_subscreens/patient_screen.dart b/apps/tasks/lib/screens/main_screen_subscreens/patient_screen.dart index 3f4b1a33..f6d7daac 100644 --- a/apps/tasks/lib/screens/main_screen_subscreens/patient_screen.dart +++ b/apps/tasks/lib/screens/main_screen_subscreens/patient_screen.dart @@ -33,7 +33,7 @@ class _PatientScreenState extends State { left: paddingSmall, right: paddingSmall, bottom: paddingMedium, top: paddingSmall), child: Consumer(builder: (_, patientController, __) { return SearchBar( - hintText: context.localization!.searchPatient, + hintText: context.localization.searchPatient, trailing: [ IconButton( onPressed: () { @@ -107,7 +107,7 @@ class _PatientScreenState extends State { child: Align( alignment: Alignment.centerLeft, child: Text( - context.localization!.addTask, + context.localization.addTask, ), ), ), @@ -124,7 +124,7 @@ class _PatientScreenState extends State { child: Padding( padding: const EdgeInsets.only(right: paddingMedium), child: Text( - context.localization!.discharge, + context.localization.discharge, )), ), ), diff --git a/apps/tasks/lib/screens/settings_screen.dart b/apps/tasks/lib/screens/settings_screen.dart index ac73619f..d7f5647c 100644 --- a/apps/tasks/lib/screens/settings_screen.dart +++ b/apps/tasks/lib/screens/settings_screen.dart @@ -24,7 +24,7 @@ class SettingsScreen extends StatelessWidget { Widget build(BuildContext context) { return Scaffold( appBar: AppBar( - title: Text(context.localization!.settings), + title: Text(context.localization.settings), ), body: ListView( children: [ @@ -34,16 +34,16 @@ class SettingsScreen extends StatelessWidget { tiles: [ ListTile( leading: const Icon(Icons.brightness_medium), - title: Text(context.localization!.darkMode), + title: Text(context.localization.darkMode), trailing: Consumer( builder: (_, ThemeModel themeNotifier, __) { return PopupMenuButton( initialValue: themeNotifier.themeMode, position: PopupMenuPosition.under, itemBuilder: (context) => [ - PopupMenuItem(value: ThemeMode.dark, child: Text(context.localization!.darkMode)), - PopupMenuItem(value: ThemeMode.light, child: Text(context.localization!.lightMode)), - PopupMenuItem(value: ThemeMode.system, child: Text(context.localization!.system)), + PopupMenuItem(value: ThemeMode.dark, child: Text(context.localization.darkMode)), + PopupMenuItem(value: ThemeMode.light, child: Text(context.localization.lightMode)), + PopupMenuItem(value: ThemeMode.system, child: Text(context.localization.system)), ], onSelected: (value) { if (value == ThemeMode.system) { @@ -58,9 +58,9 @@ class SettingsScreen extends StatelessWidget { children: [ Text( { - ThemeMode.dark: context.localization!.darkMode, - ThemeMode.light: context.localization!.lightMode, - ThemeMode.system: context.localization!.system, + ThemeMode.dark: context.localization.darkMode, + ThemeMode.light: context.localization.lightMode, + ThemeMode.system: context.localization.system, }[themeNotifier.themeMode]!, style: const TextStyle( fontSize: 14, @@ -84,7 +84,7 @@ class SettingsScreen extends StatelessWidget { builder: (context, languageModel, child) { return ListTile( leading: const Icon(Icons.language), - title: Text(context.localization!.language), + title: Text(context.localization.language), trailing: PopupMenuButton( position: PopupMenuPosition.under, initialValue: languageModel.local, @@ -124,7 +124,7 @@ class SettingsScreen extends StatelessWidget { ), ListTile( leading: const Icon(Icons.info_outline), - title: Text(context.localization!.licenses), + title: Text(context.localization.licenses), trailing: const Icon(Icons.arrow_forward), onTap: () => {showLicensePage(context: context)}, ), @@ -132,7 +132,7 @@ class SettingsScreen extends StatelessWidget { builder: (context, currentWardService, _) { return ListTile( leading: const Icon(Icons.logout), - title: Text(context.localization!.logout), + title: Text(context.localization.logout), onTap: () { UserSessionService().logout(); currentWardService.clear(); @@ -170,27 +170,27 @@ class SettingsBottomSheetPage extends StatelessWidget { return BottomSheetPage( header: BottomSheetHeader.navigation( context, - titleText: context.localization!.settings, + titleText: context.localization.settings, ), child: Flexible( child: ListView( children: [ - titleBuilder(context.localization!.personalSettings), + titleBuilder(context.localization.personalSettings), RoundedListTiles( children: [ ForwardNavigationTile( icon: Icons.person, - title: context.localization!.personalData, + title: context.localization.personalData, onTap: () {}, ), ForwardNavigationTile( icon: Icons.security_rounded, - title: context.localization!.passwordAndSecurity, + title: context.localization.passwordAndSecurity, onTap: () {}, ), ForwardNavigationTile( icon: Icons.checklist_rounded, - title: context.localization!.myTaskTemplates, + title: context.localization.myTaskTemplates, onTap: () { NavigationStackController.of(context).push(const TaskTemplatesBottomSheetPage(isPersonal: true)); }, @@ -198,7 +198,7 @@ class SettingsBottomSheetPage extends StatelessWidget { ], ), const SizedBox(height: distanceMedium), - titleBuilder(context.localization!.myOrganizations), + titleBuilder(context.localization.myOrganizations), LoadingFutureBuilder( future: OrganizationService().getOrganizationsForUser(), thenBuilder: (context, data) { @@ -218,21 +218,21 @@ class SettingsBottomSheetPage extends StatelessWidget { loadingWidget: const PulsingContainer(height: 50), ), const SizedBox(height: distanceMedium), - titleBuilder(context.localization!.appearance), + titleBuilder(context.localization.appearance), RoundedListTiles( children: [ ListTile( leading: Icon(Icons.brightness_medium, color: context.theme.colorScheme.primary), - title: Text(context.localization!.darkMode, style: const TextStyle(fontWeight: FontWeight.bold)), + title: Text(context.localization.darkMode, style: const TextStyle(fontWeight: FontWeight.bold)), trailing: Consumer( builder: (_, ThemeModel themeNotifier, __) { return PopupMenuButton( initialValue: themeNotifier.themeMode, position: PopupMenuPosition.under, itemBuilder: (context) => [ - PopupMenuItem(value: ThemeMode.dark, child: Text(context.localization!.darkMode)), - PopupMenuItem(value: ThemeMode.light, child: Text(context.localization!.lightMode)), - PopupMenuItem(value: ThemeMode.system, child: Text(context.localization!.system)), + PopupMenuItem(value: ThemeMode.dark, child: Text(context.localization.darkMode)), + PopupMenuItem(value: ThemeMode.light, child: Text(context.localization.lightMode)), + PopupMenuItem(value: ThemeMode.system, child: Text(context.localization.system)), ], onSelected: (value) { if (value == ThemeMode.system) { @@ -246,9 +246,9 @@ class SettingsBottomSheetPage extends StatelessWidget { children: [ Text( { - ThemeMode.dark: context.localization!.darkMode, - ThemeMode.light: context.localization!.lightMode, - ThemeMode.system: context.localization!.system, + ThemeMode.dark: context.localization.darkMode, + ThemeMode.light: context.localization.lightMode, + ThemeMode.system: context.localization.system, }[themeNotifier.themeMode]!, style: const TextStyle( fontSize: 14, @@ -272,7 +272,7 @@ class SettingsBottomSheetPage extends StatelessWidget { return ListTile( leading: Icon(Icons.language, color: context.theme.colorScheme.primary), title: Text( - context.localization!.language, + context.localization.language, style: const TextStyle(fontWeight: FontWeight.bold), ), trailing: PopupMenuButton( @@ -313,19 +313,19 @@ class SettingsBottomSheetPage extends StatelessWidget { ], ), const SizedBox(height: distanceMedium), - titleBuilder(context.localization!.other), + titleBuilder(context.localization.other), RoundedListTiles( children: [ ForwardNavigationTile( icon: Icons.info_outline, - title: context.localization!.licenses, + title: context.localization.licenses, onTap: () => {showLicensePage(context: context)}, ), Consumer( builder: (context, currentWardService, _) { return ForwardNavigationTile( icon: Icons.logout, - title: context.localization!.logout, + title: context.localization.logout, color: Colors.red.withOpacity(0.7), // TODO get this from theme onTap: () { // TODO add confirm dialog diff --git a/apps/tasks/lib/screens/ward_select_screen.dart b/apps/tasks/lib/screens/ward_select_screen.dart index 93cacabe..def1d8dd 100644 --- a/apps/tasks/lib/screens/ward_select_screen.dart +++ b/apps/tasks/lib/screens/ward_select_screen.dart @@ -25,7 +25,7 @@ class _WardSelectScreen extends State { Widget build(BuildContext context) { return Scaffold( appBar: AppBar( - title: Text(context.localization!.selectWard), + title: Text(context.localization.selectWard), actions: [ IconButton( onPressed: () { @@ -43,13 +43,13 @@ class _WardSelectScreen extends State { body: Column( children: [ ForwardNavigationTile( - title:organization?.longName ?? context.localization!.none, - subtitle: context.localization!.organization, + title:organization?.longName ?? context.localization.none, + subtitle: context.localization.organization, onTap: () => Navigator.push( context, MaterialPageRoute( builder: (context) => ListSearch( - title: context.localization!.organization, + title: context.localization.organization, asyncItems: (_) async { List organizations = await OrganizationService().getOrganizationsForUser(); return organizations; @@ -70,15 +70,15 @@ class _WardSelectScreen extends State { }), ), ForwardNavigationTile( - title: ward?.name ?? context.localization!.none, - subtitle: context.localization!.ward, + title: ward?.name ?? context.localization.none, + subtitle: context.localization.ward, onTap: organization == null ? null : () => Navigator.push( context, MaterialPageRoute( builder: (context) => ListSearch( - title: context.localization!.ward, + title: context.localization.ward, asyncItems: (_) async => await WardService().getWards(organizationId: organization!.id), elementToString: (WardMinimal ward) => ward.name, @@ -101,7 +101,7 @@ class _WardSelectScreen extends State { } currentWardService.currentWard = CurrentWardInformation(ward!, organization!); }, - child: Text(context.localization!.switch_), + child: Text(context.localization.switch_), ), ), ], diff --git a/apps/tasks/lib/util/field_type_translations.dart b/apps/tasks/lib/util/field_type_translations.dart index 146d9486..f305474e 100644 --- a/apps/tasks/lib/util/field_type_translations.dart +++ b/apps/tasks/lib/util/field_type_translations.dart @@ -5,18 +5,18 @@ import 'package:helpwave_service/property.dart'; String propertyFieldTypeTranslations(BuildContext context, PropertyFieldType fieldType) { switch(fieldType){ case PropertyFieldType.text: - return context.localization!.freeText; + return context.localization.freeText; case PropertyFieldType.number: - return context.localization!.number; + return context.localization.number; case PropertyFieldType.bool: - return context.localization!.checkBox; + return context.localization.checkBox; case PropertyFieldType.date: - return context.localization!.datePicker; + return context.localization.datePicker; case PropertyFieldType.dateTime: - return context.localization!.dateTimePicker; + return context.localization.dateTimePicker; case PropertyFieldType.singleSelect: - return context.localization!.select; + return context.localization.select; case PropertyFieldType.multiSelect: - return context.localization!.multiSelect; + return context.localization.multiSelect; } } diff --git a/apps/tasks/lib/util/subject_type_translations.dart b/apps/tasks/lib/util/subject_type_translations.dart index bd0ab9fa..29bb5f8d 100644 --- a/apps/tasks/lib/util/subject_type_translations.dart +++ b/apps/tasks/lib/util/subject_type_translations.dart @@ -5,8 +5,8 @@ import 'package:helpwave_service/property.dart'; String propertySubjectTypeTranslations(BuildContext context, PropertySubjectType subjectType) { switch(subjectType){ case PropertySubjectType.patient: - return context.localization!.patient; + return context.localization.patient; case PropertySubjectType.task: - return context.localization!.task; + return context.localization.task; } } diff --git a/packages/helpwave_localization/lib/src/localization_impl.dart b/packages/helpwave_localization/lib/src/localization_impl.dart index b0689163..7ec3767e 100644 --- a/packages/helpwave_localization/lib/src/localization_impl.dart +++ b/packages/helpwave_localization/lib/src/localization_impl.dart @@ -2,5 +2,5 @@ import 'package:flutter/material.dart'; import 'package:helpwave_localization/l10n/app_localizations.dart'; extension LocalizationExtension on BuildContext { - AppLocalizations? get localization => AppLocalizations.of(this); + AppLocalizations get localization => AppLocalizations.of(this)!; } diff --git a/packages/helpwave_widget/lib/src/content_selection/content_selector.dart b/packages/helpwave_widget/lib/src/content_selection/content_selector.dart index 16204d88..dab1ac9b 100644 --- a/packages/helpwave_widget/lib/src/content_selection/content_selector.dart +++ b/packages/helpwave_widget/lib/src/content_selection/content_selector.dart @@ -291,8 +291,8 @@ class _ContentSelectorState extends State> { return Column( children: [ ListTile( - title: Text(widget.title ?? context.localization!.list), - subtitle: Text("${currentSelection.length} ${widget.entryName ?? context.localization!.entries}"), + title: Text(widget.title ?? context.localization.list), + subtitle: Text("${currentSelection.length} ${widget.entryName ?? context.localization.entries}"), leading: widget.icon, onTap: () { setState(() { diff --git a/packages/helpwave_widget/lib/src/content_selection/list_search.dart b/packages/helpwave_widget/lib/src/content_selection/list_search.dart index 1961d444..6af4ab03 100644 --- a/packages/helpwave_widget/lib/src/content_selection/list_search.dart +++ b/packages/helpwave_widget/lib/src/content_selection/list_search.dart @@ -140,7 +140,7 @@ class _ListSearchState extends State> { }, child: Scaffold( appBar: AppBar( - title: Text(widget.title ?? context.localization!.listSearch), + title: Text(widget.title ?? context.localization.listSearch), ), body: SizedBox( height: MediaQuery.of(context).size.height, @@ -165,7 +165,7 @@ class _ListSearchState extends State> { }, icon: const Icon(Icons.close), ), - hintText: widget.searchHintText ?? "${context.localization!.search}...", + hintText: widget.searchHintText ?? "${context.localization.search}...", border: const OutlineInputBorder(), ), ), @@ -203,14 +203,14 @@ class _ListSearchState extends State> { Text( widget.elementNotFoundText != null ? widget.elementNotFoundText!(_searchController.text) - : "${context.localization!.noItem} ${_searchController.text} ${context.localization!.found}", + : "${context.localization.noItem} ${_searchController.text} ${context.localization.found}", style: context.theme.textTheme.titleLarge, ), const SizedBox(height: distanceDefault), widget.allowSelectAnyway ? TextButton( onPressed: () => Navigator.pop(context, _searchController.text.trim()), - child: Text(widget.addAnywayText ?? "${context.localization!.addAnyway}!"), + child: Text(widget.addAnywayText ?? "${context.localization.addAnyway}!"), ) : const SizedBox(), ], @@ -225,7 +225,7 @@ class _ListSearchState extends State> { size: iconSizeBig, ), const SizedBox(height: distanceBig), - Text('${context.localization!.error}: ${snapshot.error}'), + Text('${context.localization.error}: ${snapshot.error}'), ]; } return Expanded( diff --git a/packages/helpwave_widget/lib/src/dialog_impl.dart b/packages/helpwave_widget/lib/src/dialog_impl.dart index a5e66b23..6fa1c174 100644 --- a/packages/helpwave_widget/lib/src/dialog_impl.dart +++ b/packages/helpwave_widget/lib/src/dialog_impl.dart @@ -38,11 +38,11 @@ class AcceptDialog extends StatelessWidget { content: content, actions: [ TextButton( - child: Text(yesText ?? context.localization!.yes), + child: Text(yesText ?? context.localization.yes), onPressed: () => Navigator.of(context).pop(true), ), TextButton( - child: Text(noText ?? context.localization!.no), + child: Text(noText ?? context.localization.no), onPressed: () => Navigator.of(context).pop(false), ), ], diff --git a/packages/helpwave_widget/lib/src/lists/rounded_list_tiles.dart b/packages/helpwave_widget/lib/src/lists/rounded_list_tiles.dart index 0e2881ed..8f6cf1ad 100644 --- a/packages/helpwave_widget/lib/src/lists/rounded_list_tiles.dart +++ b/packages/helpwave_widget/lib/src/lists/rounded_list_tiles.dart @@ -18,7 +18,7 @@ class RoundedListTiles extends StatelessWidget { height: 60, child: Center( child: Text( - context.localization!.nothingYet, + context.localization.nothingYet, style: const TextStyle(fontWeight: FontWeight.w700), ), ), diff --git a/packages/helpwave_widget/lib/src/loading/load_error_widget.dart b/packages/helpwave_widget/lib/src/loading/load_error_widget.dart index 91666188..3ed17b0c 100644 --- a/packages/helpwave_widget/lib/src/loading/load_error_widget.dart +++ b/packages/helpwave_widget/lib/src/loading/load_error_widget.dart @@ -33,7 +33,7 @@ class LoadErrorWidget extends StatelessWidget { color: iconColor, ), const SizedBox(height: distanceBig), - Text(errorText ?? context.localization!.errorOnLoad), + Text(errorText ?? context.localization.errorOnLoad), ], ), ); diff --git a/packages/helpwave_widget/lib/src/loading/loading_spinner.dart b/packages/helpwave_widget/lib/src/loading/loading_spinner.dart index 6b6f05e3..45c6e9c0 100644 --- a/packages/helpwave_widget/lib/src/loading/loading_spinner.dart +++ b/packages/helpwave_widget/lib/src/loading/loading_spinner.dart @@ -36,12 +36,12 @@ class LoadingSpinner extends StatelessWidget { width: size, child: CircularProgressIndicator( strokeWidth: width, - semanticsLabel: text ?? context.localization!.loading, + semanticsLabel: text ?? context.localization.loading, color: color ?? context.theme.colorScheme.primary, ), ), const SizedBox(height: distanceBig), - Text(text ?? context.localization!.loading), + Text(text ?? context.localization.loading), ], ); } From 84ae1e2adaf4b1810fcbed68f2ff2437abded9b1 Mon Sep 17 00:00:00 2001 From: Felix Thape Date: Tue, 19 Nov 2024 16:15:01 +0100 Subject: [PATCH 27/36] fix: add padding to settings screen --- apps/tasks/lib/screens/settings_screen.dart | 1 + 1 file changed, 1 insertion(+) diff --git a/apps/tasks/lib/screens/settings_screen.dart b/apps/tasks/lib/screens/settings_screen.dart index d7f5647c..6619fc46 100644 --- a/apps/tasks/lib/screens/settings_screen.dart +++ b/apps/tasks/lib/screens/settings_screen.dart @@ -174,6 +174,7 @@ class SettingsBottomSheetPage extends StatelessWidget { ), child: Flexible( child: ListView( + padding: const EdgeInsets.only(bottom: paddingMedium), children: [ titleBuilder(context.localization.personalSettings), RoundedListTiles( From da066d9f0ef676c64a3ae253dd57817f40fd85af Mon Sep 17 00:00:00 2001 From: Felix Thape Date: Tue, 19 Nov 2024 23:38:24 +0100 Subject: [PATCH 28/36] fix: fix adding of patients --- .../patient_bottom_sheet.dart | 7 +++---- .../lib/components/task_expansion_tile.dart | 11 ++++++++-- apps/tasks/lib/screens/main_screen.dart | 2 +- .../tasks/controllers/patient_controller.dart | 20 ++++++++++++------- .../lib/src/api/tasks/data_types/patient.dart | 4 ++-- .../lib/src/api/user/data_types/user.dart | 8 +++++++- 6 files changed, 35 insertions(+), 17 deletions(-) diff --git a/apps/tasks/lib/components/bottom_sheet_pages/patient_bottom_sheet.dart b/apps/tasks/lib/components/bottom_sheet_pages/patient_bottom_sheet.dart index d81bed70..c68f1ea1 100644 --- a/apps/tasks/lib/components/bottom_sheet_pages/patient_bottom_sheet.dart +++ b/apps/tasks/lib/components/bottom_sheet_pages/patient_bottom_sheet.dart @@ -17,9 +17,9 @@ import 'package:helpwave_util/loading.dart'; /// A [BottomSheet] for showing [Patient] information and [Task]s for that [Patient] class PatientBottomSheet extends StatefulWidget { /// The identifier of the [Patient] - final String patentId; + final String? patentId; - const PatientBottomSheet({Key? key, required this.patentId}) : super(key: key); + const PatientBottomSheet({Key? key, this.patentId}) : super(key: key); @override State createState() => _PatientBottomSheetState(); @@ -48,7 +48,7 @@ class _PatientBottomSheetState extends State { return MultiProvider( providers: [ ChangeNotifierProvider( - create: (_) => PatientController(Patient.empty(id: widget.patentId)), + create: (_) => PatientController(id: widget.patentId), ), ], child: BottomSheetPage( @@ -83,7 +83,6 @@ class _PatientBottomSheetState extends State { children: patientController.isCreating ? [ FilledButton( - style: buttonStyleBig, onPressed: patientController.create, child: Text(context.localization.create), ) diff --git a/apps/tasks/lib/components/task_expansion_tile.dart b/apps/tasks/lib/components/task_expansion_tile.dart index 4b050704..2f7c0bf6 100644 --- a/apps/tasks/lib/components/task_expansion_tile.dart +++ b/apps/tasks/lib/components/task_expansion_tile.dart @@ -1,3 +1,4 @@ +import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:helpwave_theme/constants.dart'; import 'package:helpwave_theme/util.dart'; @@ -58,8 +59,14 @@ class TaskExpansionTile extends StatelessWidget { context: context, builder: (context) => TaskBottomSheet(task: task, patient: task.patient), ).then((_) { - AssignedTasksController controller = Provider.of(context, listen: false); - controller.load(); + try { + AssignedTasksController controller = Provider.of(context, listen: false); + controller.load(); + } catch (e) { + if (kDebugMode) { + print(e); + } + } }); }, child: TaskCard( diff --git a/apps/tasks/lib/screens/main_screen.dart b/apps/tasks/lib/screens/main_screen.dart index bff9b5a7..1fb970be 100644 --- a/apps/tasks/lib/screens/main_screen.dart +++ b/apps/tasks/lib/screens/main_screen.dart @@ -156,7 +156,7 @@ class _TaskPatientFloatingActionButton extends StatelessWidget { onPressed: () { context.pushModal( context: context, - builder: (context) => const PatientBottomSheet(patentId: ""), + builder: (context) => const PatientBottomSheet(), ); }, ), diff --git a/packages/helpwave_service/lib/src/api/tasks/controllers/patient_controller.dart b/packages/helpwave_service/lib/src/api/tasks/controllers/patient_controller.dart index f3355cb9..590a46da 100644 --- a/packages/helpwave_service/lib/src/api/tasks/controllers/patient_controller.dart +++ b/packages/helpwave_service/lib/src/api/tasks/controllers/patient_controller.dart @@ -4,7 +4,7 @@ import 'package:helpwave_service/src/api/tasks/index.dart'; /// The Controller for managing [Patient]s in a Ward class PatientController extends LoadingChangeNotifier { /// The current [Patient] - Patient _patient; + Patient _patient = Patient.empty(); /// The current [Patient] Patient get patient => _patient; @@ -17,7 +17,13 @@ class PatientController extends LoadingChangeNotifier { /// Is the current [Patient] already saved on the server or are we creating? get isCreating => _patient.isCreating; - PatientController(this._patient) { + PatientController({String? id, Patient? patient}) { + assert(patient == null || id == patient.id, "The id and patient id must be equal or not provided."); + if(patient != null) { + _patient = patient; + } else if(id != null) { + _patient.copyWith(id: id); + } load(); } @@ -72,12 +78,12 @@ class PatientController extends LoadingChangeNotifier { /// Assigns the [Patient] to a [Bed] and [Room] Future assignToBed(RoomMinimal room, BedMinimal bed) async { - if (isCreating) { - patient.room = room; - patient.bed = bed; - return; - } assignPatientToBed() async { + if (isCreating) { + patient.room = room; + patient.bed = bed; + return; + } await PatientService().assignBed(patientId: patient.id!, bedId: bed.id).then((value) { patient = patient.copyWith(bed: bed, room: room); }); diff --git a/packages/helpwave_service/lib/src/api/tasks/data_types/patient.dart b/packages/helpwave_service/lib/src/api/tasks/data_types/patient.dart index caaeca1b..3bc6eab2 100644 --- a/packages/helpwave_service/lib/src/api/tasks/data_types/patient.dart +++ b/packages/helpwave_service/lib/src/api/tasks/data_types/patient.dart @@ -91,12 +91,12 @@ class Patient extends PatientMinimal { get doneCount => doneTasks.length; - factory Patient.empty({String id = ""}) { + factory Patient.empty({String? id}) { return Patient(id: id, name: "Patient", tasks: [], notes: "", isDischarged: false); } Patient({ - required super.id, + super.id, required super.name, required this.tasks, required this.notes, diff --git a/packages/helpwave_service/lib/src/api/user/data_types/user.dart b/packages/helpwave_service/lib/src/api/user/data_types/user.dart index e42a194b..e27b7cdf 100644 --- a/packages/helpwave_service/lib/src/api/user/data_types/user.dart +++ b/packages/helpwave_service/lib/src/api/user/data_types/user.dart @@ -15,7 +15,13 @@ class User extends IdentifiedObject { required this.profileUrl, }); - factory User.empty({String? id}) => User(id: id, name: "User", nickName: "", email: "", profileUrl: Uri.base); + factory User.empty({String? id}) => User( + id: id, + name: "User", + nickName: "", + email: "", + profileUrl: Uri.parse("https://cdn.helpwave.de/boringavatar.svg"), + ); User copyWith({String? id, String? name, String? nickName, Uri? profileUrl, String? email}) => User( id: id ?? this.id, From 013cbe41c26b34b54c21e71df7732d366f86cd61 Mon Sep 17 00:00:00 2001 From: Felix Thape Date: Wed, 20 Nov 2024 23:42:18 +0100 Subject: [PATCH 29/36] chore: update dependencies --- apps/tasks/android/settings.gradle | 2 +- apps/tasks/android/version.properties | 4 +- apps/tasks/pubspec.lock | 300 +++++++++++--------- apps/tasks/pubspec.yaml | 4 +- packages/helpwave_localization/pubspec.yaml | 6 +- packages/helpwave_service/pubspec.yaml | 7 +- packages/helpwave_theme/pubspec.yaml | 4 +- packages/helpwave_util/pubspec.yaml | 6 +- packages/helpwave_widget/pubspec.yaml | 6 +- 9 files changed, 178 insertions(+), 161 deletions(-) diff --git a/apps/tasks/android/settings.gradle b/apps/tasks/android/settings.gradle index 78f69f5b..d0f044b6 100644 --- a/apps/tasks/android/settings.gradle +++ b/apps/tasks/android/settings.gradle @@ -19,7 +19,7 @@ pluginManagement { plugins { id "dev.flutter.flutter-plugin-loader" version "1.0.0" id "com.android.application" version "7.3.0" apply false - id "org.jetbrains.kotlin.android" version "1.7.10" apply false + id "org.jetbrains.kotlin.android" version "2.0.21" apply false } include ':app' diff --git a/apps/tasks/android/version.properties b/apps/tasks/android/version.properties index 27a14cef..fd74d489 100644 --- a/apps/tasks/android/version.properties +++ b/apps/tasks/android/version.properties @@ -2,6 +2,6 @@ # lowest possible flutter.minSdkVersion=20 -flutter.targetSdkVersion=33 +flutter.targetSdkVersion=34 # highest possible -flutter.compileSdkVersion=33 +flutter.compileSdkVersion=34 diff --git a/apps/tasks/pubspec.lock b/apps/tasks/pubspec.lock index fa0366a5..a2f233b6 100644 --- a/apps/tasks/pubspec.lock +++ b/apps/tasks/pubspec.lock @@ -1,30 +1,38 @@ # Generated by pub # See https://dart.dev/tools/pub/glossary#lockfile packages: + ansicolor: + dependency: transitive + description: + name: ansicolor + sha256: "50e982d500bc863e1d703448afdbf9e5a72eb48840a4f766fa361ffd6877055f" + url: "https://pub.dev" + source: hosted + version: "2.0.3" archive: dependency: transitive description: name: archive - sha256: "49b1fad315e57ab0bbc15bcbb874e83116a1d78f77ebd500a4af6c9407d6b28e" + sha256: cb6a278ef2dbb298455e1a713bda08524a175630ec643a242c399c932a0a1f7d url: "https://pub.dev" source: hosted - version: "3.3.8" + version: "3.6.1" args: dependency: transitive description: name: args - sha256: eef6c46b622e0494a36c5a12d10d77fb4e855501a91c1b9ef9339326e58f0596 + sha256: bf9f5caeea8d8fe6721a9c358dd8a5c1947b27f1cfaa18b39c301273594919e6 url: "https://pub.dev" source: hosted - version: "2.4.2" + version: "2.6.0" asn1lib: dependency: transitive description: name: asn1lib - sha256: "21afe4333076c02877d14f4a89df111e658a6d466cbfc802eb705eb91bd5adfd" + sha256: "4bae5ae63e6d6dd17c4aac8086f3dec26c0236f6a0f03416c6c19d830c367cf5" url: "https://pub.dev" source: hosted - version: "1.5.0" + version: "1.5.8" async: dependency: transitive description: @@ -61,10 +69,10 @@ packages: dependency: transitive description: name: cli_util - sha256: b8db3080e59b2503ca9e7922c3df2072cf13992354d5e944074ffa836fba43b7 + sha256: ff6785f7e9e3c38ac98b2fb035701789de90154024a75b6cb926445e83197d1c url: "https://pub.dev" source: hosted - version: "0.4.0" + version: "0.4.2" clock: dependency: transitive description: @@ -85,18 +93,18 @@ packages: dependency: transitive description: name: convert - sha256: "0f08b14755d163f6e2134cb58222dd25ea2a2ee8a195e53983d57c075324d592" + sha256: b30acd5944035672bc15c6b7a8b47d773e41e2f17de064350988c5d02adb1c68 url: "https://pub.dev" source: hosted - version: "3.1.1" + version: "3.1.2" crypto: dependency: transitive description: name: crypto - sha256: ff625774173754681d66daaf4a448684fb04b78f902da9cb3d308c19cc5e8bab + sha256: "1e445881f28f22d6140f181e07737b22f1e099a5e1ff94b0af2f9e4a463f4855" url: "https://pub.dev" source: hosted - version: "3.0.3" + version: "3.0.6" crypto_keys: dependency: transitive description: @@ -109,18 +117,18 @@ packages: dependency: transitive description: name: csslib - sha256: "706b5707578e0c1b4b7550f64078f0a0f19dec3f50a178ffae7006b0a9ca58fb" + sha256: "09bad715f418841f976c77db72d5398dc1253c21fb9c0c7f0b0b985860b2d58e" url: "https://pub.dev" source: hosted - version: "1.0.0" + version: "1.0.2" cupertino_icons: dependency: "direct main" description: name: cupertino_icons - sha256: d57953e10f9f8327ce64a508a355f0b1ec902193f66288e8cb5070e7c47eeb2d + sha256: ba631d1c7f7bef6b729a622b7b752645a2d076dba9976925b8f25725a30e1ee6 url: "https://pub.dev" source: hosted - version: "1.0.6" + version: "1.0.8" fake_async: dependency: transitive description: @@ -133,26 +141,26 @@ packages: dependency: transitive description: name: ffi - sha256: "7bf0adc28a23d395f19f3f1eb21dd7cfd1dd9f8e1c50051c069122e6853bc878" + sha256: "16ed7b077ef01ad6170a3d0c57caa4a112a38d7a2ed5602e0aca9ca6f3d98da6" url: "https://pub.dev" source: hosted - version: "2.1.0" + version: "2.1.3" file: dependency: transitive description: name: file - sha256: "1b92bec4fc2a72f59a8e15af5f52cd441e4a7860b49499d69dfa817af20e925d" + sha256: a3b4f84adafef897088c160faf7dfffb7696046cb13ae90b508c2cbc95d3b8d4 url: "https://pub.dev" source: hosted - version: "6.1.4" + version: "7.0.1" fixnum: dependency: transitive description: name: fixnum - sha256: "25517a4deb0c03aa0f32fd12db525856438902d9c16536311e76cdc57b31d7d1" + sha256: b6dc7065e46c974bc7c5f143080a6764ec7a4be6da1285ececdc37be96de53be url: "https://pub.dev" source: hosted - version: "1.1.0" + version: "1.1.1" flutter: dependency: "direct main" description: flutter @@ -170,10 +178,10 @@ packages: dependency: "direct dev" description: name: flutter_lints - sha256: a25a15ebbdfc33ab1cd26c63a6ee519df92338a9c10f122adda92938253bef04 + sha256: "5398f14efa795ffb7a33e9b6a08798b26a180edac4ad7db3f231e40f82ce11e1" url: "https://pub.dev" source: hosted - version: "2.0.3" + version: "5.0.0" flutter_localizations: dependency: transitive description: flutter @@ -183,10 +191,10 @@ packages: dependency: "direct main" description: name: flutter_native_splash - sha256: ecff62b3b893f2f665de7e4ad3de89f738941fcfcaaba8ee601e749efafa4698 + sha256: "1152ab0067ca5a2ebeb862fe0a762057202cceb22b7e62692dcbabf6483891bb" url: "https://pub.dev" source: hosted - version: "2.3.2" + version: "2.4.3" flutter_secure_storage: dependency: transitive description: @@ -199,42 +207,42 @@ packages: dependency: transitive description: name: flutter_secure_storage_linux - sha256: "3d5032e314774ee0e1a7d0a9f5e2793486f0dff2dd9ef5a23f4e3fb2a0ae6a9e" + sha256: "4d91bfc23047422cbcd73ac684bc169859ee766482517c22172c86596bf1464b" url: "https://pub.dev" source: hosted - version: "1.2.0" + version: "1.2.1" flutter_secure_storage_macos: dependency: transitive description: name: flutter_secure_storage_macos - sha256: bd33935b4b628abd0b86c8ca20655c5b36275c3a3f5194769a7b3f37c905369c + sha256: "1693ab11121a5f925bbea0be725abfcfbbcf36c1e29e571f84a0c0f436147a81" url: "https://pub.dev" source: hosted - version: "3.0.1" + version: "3.1.2" flutter_secure_storage_platform_interface: dependency: transitive description: name: flutter_secure_storage_platform_interface - sha256: "0d4d3a5dd4db28c96ae414d7ba3b8422fd735a8255642774803b2532c9a61d7e" + sha256: cf91ad32ce5adef6fba4d736a542baca9daf3beac4db2d04be350b87f69ac4a8 url: "https://pub.dev" source: hosted - version: "1.0.2" + version: "1.1.2" flutter_secure_storage_web: dependency: transitive description: name: flutter_secure_storage_web - sha256: "30f84f102df9dcdaa2241866a958c2ec976902ebdaa8883fbfe525f1f2f3cf20" + sha256: f4ebff989b4f07b2656fb16b47852c0aab9fed9b4ec1c70103368337bc1886a9 url: "https://pub.dev" source: hosted - version: "1.1.2" + version: "1.2.1" flutter_secure_storage_windows: dependency: transitive description: name: flutter_secure_storage_windows - sha256: "5809c66f9dd3b4b93b0a6e2e8561539405322ee767ac2f64d084e2ab5429d108" + sha256: b20b07cb5ed4ed74fc567b78a72936203f587eba460af1df11281c9326cd3709 url: "https://pub.dev" source: hosted - version: "3.0.0" + version: "3.1.2" flutter_test: dependency: "direct dev" description: flutter @@ -245,14 +253,22 @@ packages: description: flutter source: sdk version: "0.0.0" + google_identity_services_web: + dependency: transitive + description: + name: google_identity_services_web + sha256: "55580f436822d64c8ff9a77e37d61f5fb1e6c7ec9d632a43ee324e2a05c3c6c9" + url: "https://pub.dev" + source: hosted + version: "0.3.3" googleapis_auth: dependency: transitive description: name: googleapis_auth - sha256: af7c3a3edf9d0de2e1e0a77e994fae0a581c525fa7012af4fa0d4a52ed9484da + sha256: befd71383a955535060acde8792e7efc11d2fccd03dd1d3ec434e85b68775938 url: "https://pub.dev" source: hosted - version: "1.4.1" + version: "1.6.0" grpc: dependency: "direct main" description: @@ -272,10 +288,10 @@ packages: dependency: "direct main" description: name: helpwave_proto_dart - sha256: "60e912fcb781e16b9b5bd6b5c27585d9481ae554be2bfc2e58c76dcfa4735d60" + sha256: a7579482d1b84299b2d666d29cb53c764424ac724aaa6de4fd135bf9505bd060 url: "https://pub.dev" source: hosted - version: "0.39.0-aa8fd45" + version: "0.39.2-8628d8b" helpwave_service: dependency: "direct main" description: @@ -308,26 +324,26 @@ packages: dependency: transitive description: name: html - sha256: "3a7812d5bcd2894edf53dfaf8cd640876cf6cef50a8f238745c8b8120ea74d3a" + sha256: "1fc58edeaec4307368c60d59b7e15b9d658b57d7f3125098b6294153c75337ec" url: "https://pub.dev" source: hosted - version: "0.15.4" + version: "0.15.5" http: dependency: transitive description: name: http - sha256: "5895291c13fa8a3bd82e76d5627f69e0d85ca6a30dcac95c4ea19a5d555879c2" + sha256: b9c29a161230ee03d3ccf545097fccd9b87a5264228c5d348202e0f0c28f9010 url: "https://pub.dev" source: hosted - version: "0.13.6" + version: "1.2.2" http2: dependency: transitive description: name: http2 - sha256: "38db0c4aa9f1cd238a5d2e86aa0cc7cc91c77e0c6c94ba64bbe85e4ff732a952" + sha256: "9ced024a160b77aba8fb8674e38f70875e321d319e6f303ec18e87bd5a4b0c1d" url: "https://pub.dev" source: hosted - version: "2.2.0" + version: "2.3.0" http_parser: dependency: transitive description: @@ -340,18 +356,18 @@ packages: dependency: transitive description: name: image - sha256: a72242c9a0ffb65d03de1b7113bc4e189686fc07c7147b8b41811d0dd0e0d9bf + sha256: f31d52537dc417fdcde36088fdf11d191026fd5e4fae742491ebd40e5a8bea7d url: "https://pub.dev" source: hosted - version: "4.0.17" + version: "4.3.0" intl: dependency: transitive description: name: intl - sha256: "3bc132a9dbce73a7e4a21a17d06e1878839ffbf975568bc875c60537824b0c4d" + sha256: d6f56758b7d3014a48af9701c085700aac781a92a87a62b1333b46d8879661cf url: "https://pub.dev" source: hosted - version: "0.18.1" + version: "0.19.0" jose: dependency: transitive description: @@ -372,58 +388,58 @@ packages: dependency: transitive description: name: json_annotation - sha256: b10a7b2ff83d83c777edba3c6a0f97045ddadd56c944e1a23a3fdf43a1bf4467 + sha256: "1ce844379ca14835a50d2f019a3099f419082cfdd231cd86a142af94dd5c6bb1" url: "https://pub.dev" source: hosted - version: "4.8.1" + version: "4.9.0" leak_tracker: dependency: transitive description: name: leak_tracker - sha256: "78eb209deea09858f5269f5a5b02be4049535f568c07b275096836f01ea323fa" + sha256: "3f87a60e8c63aecc975dda1ceedbc8f24de75f09e4856ea27daf8958f2f0ce05" url: "https://pub.dev" source: hosted - version: "10.0.0" + version: "10.0.5" leak_tracker_flutter_testing: dependency: transitive description: name: leak_tracker_flutter_testing - sha256: b46c5e37c19120a8a01918cfaf293547f47269f7cb4b0058f21531c2465d6ef0 + sha256: "932549fb305594d82d7183ecd9fa93463e9914e1b67cacc34bc40906594a1806" url: "https://pub.dev" source: hosted - version: "2.0.1" + version: "3.0.5" leak_tracker_testing: dependency: transitive description: name: leak_tracker_testing - sha256: a597f72a664dbd293f3bfc51f9ba69816f84dcd403cdac7066cb3f6003f3ab47 + sha256: "6ba465d5d76e67ddf503e1161d1f4a6bc42306f9d66ca1e8f079a47290fb06d3" url: "https://pub.dev" source: hosted - version: "2.0.1" + version: "3.0.1" lints: dependency: transitive description: name: lints - sha256: "0a217c6c989d21039f1498c3ed9f3ed71b354e69873f13a8dfc3c9fe76f1b452" + sha256: "3315600f3fb3b135be672bf4a178c55f274bebe368325ae18462c89ac1e3b413" url: "https://pub.dev" source: hosted - version: "2.1.1" + version: "5.0.0" logger: dependency: "direct main" description: name: logger - sha256: "6bbb9d6f7056729537a4309bda2e74e18e5d9f14302489cc1e93f33b3fe32cac" + sha256: be4b23575aac7ebf01f225a241eb7f6b5641eeaf43c6a8613510fc2f8cf187d1 url: "https://pub.dev" source: hosted - version: "2.0.2+1" + version: "2.5.0" logging: dependency: transitive description: name: logging - sha256: "623a88c9594aa774443aa3eb2d41807a48486b5613e67599fb4c41c0ad47c340" + sha256: c8245ada5f1717ed44271ed1c26b8ce85ca3228fd2ffdb75468ab01979309d61 url: "https://pub.dev" source: hosted - version: "1.2.0" + version: "1.3.0" matcher: dependency: transitive description: @@ -436,18 +452,18 @@ packages: dependency: transitive description: name: material_color_utilities - sha256: "0e0a020085b65b6083975e499759762399b4475f766c21668c4ecca34ea74e5a" + sha256: f7142bb1154231d7ea5f96bc7bde4bda2a0945d2806bb11670e30b850d56bdec url: "https://pub.dev" source: hosted - version: "0.8.0" + version: "0.11.1" meta: dependency: transitive description: name: meta - sha256: d584fa6707a52763a52446f02cc621b077888fb63b93bbcb1143a7be5a0c0c04 + sha256: bdb68674043280c3428e9ec998512fb681678676b3c54e773629ffe74419f8c7 url: "https://pub.dev" source: hosted - version: "1.11.0" + version: "1.15.0" nested: dependency: transitive description: @@ -460,10 +476,10 @@ packages: dependency: transitive description: name: openid_client - sha256: "514c0ba645b81029c28999831a70cb055dda1a3bc60be759a04d2556f60ec960" + sha256: "043878e907b7a1b460b54fb7b3b27b101cf70d4ac28b32a2db87ae67dbaed611" url: "https://pub.dev" source: hosted - version: "0.4.7" + version: "0.4.8" path: dependency: transitive description: @@ -476,26 +492,26 @@ packages: dependency: transitive description: name: path_provider - sha256: a1aa8aaa2542a6bc57e381f132af822420216c80d4781f7aa085ca3229208aaa + sha256: "50c5dd5b6e1aaf6fb3a78b33f6aa3afca52bf903a8a5298f53101fdaee55bbcd" url: "https://pub.dev" source: hosted - version: "2.1.1" + version: "2.1.5" path_provider_android: dependency: transitive description: name: path_provider_android - sha256: e595b98692943b4881b219f0a9e3945118d3c16bd7e2813f98ec6e532d905f72 + sha256: c464428172cb986b758c6d1724c603097febb8fb855aa265aeecc9280c294d4a url: "https://pub.dev" source: hosted - version: "2.2.1" + version: "2.2.12" path_provider_foundation: dependency: transitive description: name: path_provider_foundation - sha256: "19314d595120f82aca0ba62787d58dde2cc6b5df7d2f0daf72489e38d1b57f2d" + sha256: f234384a3fdd67f989b4d54a5d73ca2a6c422fa55ae694381ae0f4375cd1ea16 url: "https://pub.dev" source: hosted - version: "2.3.1" + version: "2.4.0" path_provider_linux: dependency: transitive description: @@ -508,50 +524,50 @@ packages: dependency: transitive description: name: path_provider_platform_interface - sha256: "94b1e0dd80970c1ce43d5d4e050a9918fce4f4a775e6142424c30a29a363265c" + sha256: "88f5779f72ba699763fa3a3b06aa4bf6de76c8e5de842cf6f29e2e06476c2334" url: "https://pub.dev" source: hosted - version: "2.1.1" + version: "2.1.2" path_provider_windows: dependency: transitive description: name: path_provider_windows - sha256: "8bc9f22eee8690981c22aa7fc602f5c85b497a6fb2ceb35ee5a5e5ed85ad8170" + sha256: bd6f00dbd873bfb70d0761682da2b3a2c2fccc2b9e84c495821639601d81afe7 url: "https://pub.dev" source: hosted - version: "2.2.1" + version: "2.3.0" petitparser: dependency: transitive description: name: petitparser - sha256: cb3798bef7fc021ac45b308f4b51208a152792445cce0448c9a4ba5879dd8750 + sha256: c15605cd28af66339f8eb6fbe0e541bfe2d1b72d5825efc6598f3e0a31b9ad27 url: "https://pub.dev" source: hosted - version: "5.4.0" + version: "6.0.2" platform: dependency: transitive description: name: platform - sha256: ae68c7bfcd7383af3629daafb32fb4e8681c7154428da4febcff06200585f102 + sha256: "5d6b1b0036a5f331ebc77c850ebc8506cbc1e9416c27e59b439f917a902a4984" url: "https://pub.dev" source: hosted - version: "3.1.2" + version: "3.1.6" plugin_platform_interface: dependency: transitive description: name: plugin_platform_interface - sha256: da3fdfeccc4d4ff2da8f8c556704c08f912542c5fb3cf2233ed75372384a034d + sha256: "4820fbfdb9478b1ebae27888254d445073732dae3d6ea81f0b7e06d5dedc3f02" url: "https://pub.dev" source: hosted - version: "2.1.6" + version: "2.1.8" pointycastle: dependency: transitive description: name: pointycastle - sha256: "7c1e5f0d23c9016c5bbd8b1473d0d3fb3fc851b876046039509e18e0c7485f2c" + sha256: "4be0097fcf3fd3e8449e53730c631200ebc7b88016acecab2b0da2f0149222fe" url: "https://pub.dev" source: hosted - version: "3.7.3" + version: "3.9.1" protobuf: dependency: transitive description: @@ -564,74 +580,74 @@ packages: dependency: "direct main" description: name: provider - sha256: cdbe7530b12ecd9eb455bdaa2fcb8d4dad22e80b8afb4798b41479d5ce26847f + sha256: c8a055ee5ce3fd98d6fc872478b03823ffdb448699c6ebdbbc71d59b596fd48c url: "https://pub.dev" source: hosted - version: "6.0.5" + version: "6.1.2" quiver: dependency: transitive description: name: quiver - sha256: b1c1ac5ce6688d77f65f3375a9abb9319b3cb32486bdc7a1e0fdf004d7ba4e47 + sha256: ea0b925899e64ecdfbf9c7becb60d5b50e706ade44a85b2363be2a22d88117d2 url: "https://pub.dev" source: hosted - version: "3.2.1" + version: "3.2.2" shared_preferences: dependency: "direct main" description: name: shared_preferences - sha256: b7f41bad7e521d205998772545de63ff4e6c97714775902c199353f8bf1511ac + sha256: "95f9997ca1fb9799d494d0cb2a780fd7be075818d59f00c43832ed112b158a82" url: "https://pub.dev" source: hosted - version: "2.2.1" + version: "2.3.3" shared_preferences_android: dependency: transitive description: name: shared_preferences_android - sha256: "8568a389334b6e83415b6aae55378e158fbc2314e074983362d20c562780fb06" + sha256: "3b9febd815c9ca29c9e3520d50ec32f49157711e143b7a4ca039eb87e8ade5ab" url: "https://pub.dev" source: hosted - version: "2.2.1" + version: "2.3.3" shared_preferences_foundation: dependency: transitive description: name: shared_preferences_foundation - sha256: "7bf53a9f2d007329ee6f3df7268fd498f8373602f943c975598bbb34649b62a7" + sha256: "07e050c7cd39bad516f8d64c455f04508d09df104be326d8c02551590a0d513d" url: "https://pub.dev" source: hosted - version: "2.3.4" + version: "2.5.3" shared_preferences_linux: dependency: transitive description: name: shared_preferences_linux - sha256: c2eb5bf57a2fe9ad6988121609e47d3e07bb3bdca5b6f8444e4cf302428a128a + sha256: "580abfd40f415611503cae30adf626e6656dfb2f0cee8f465ece7b6defb40f2f" url: "https://pub.dev" source: hosted - version: "2.3.1" + version: "2.4.1" shared_preferences_platform_interface: dependency: transitive description: name: shared_preferences_platform_interface - sha256: d4ec5fc9ebb2f2e056c617112aa75dcf92fc2e4faaf2ae999caa297473f75d8a + sha256: "57cbf196c486bc2cf1f02b85784932c6094376284b3ad5779d1b1c6c6a816b80" url: "https://pub.dev" source: hosted - version: "2.3.1" + version: "2.4.1" shared_preferences_web: dependency: transitive description: name: shared_preferences_web - sha256: d762709c2bbe80626ecc819143013cc820fa49ca5e363620ee20a8b15a3e3daf + sha256: d2ca4132d3946fec2184261726b355836a82c33d7d5b67af32692aff18a4684e url: "https://pub.dev" source: hosted - version: "2.2.1" + version: "2.4.2" shared_preferences_windows: dependency: transitive description: name: shared_preferences_windows - sha256: f763a101313bd3be87edffe0560037500967de9c394a714cd598d945517f694f + sha256: "94ef0f72b2d71bc3e700e025db3710911bd51a71cefb65cc609dd0d9a982e3c1" url: "https://pub.dev" source: hosted - version: "2.3.1" + version: "2.4.1" sky_engine: dependency: transitive description: flutter @@ -681,18 +697,18 @@ packages: dependency: transitive description: name: test_api - sha256: "5c2f730018264d276c20e4f1503fd1308dfbbae39ec8ee63c5236311ac06954b" + sha256: "5b8a98dafc4d5c4c9c72d8b31ab2b23fc13422348d2997120294d3bac86b4ddb" url: "https://pub.dev" source: hosted - version: "0.6.1" + version: "0.7.2" typed_data: dependency: transitive description: name: typed_data - sha256: facc8d6582f16042dd49f2463ff1bd6e2c9ef9f3d5da3d9b087e244a7b564b3c + sha256: f9049c039ebfeb4cf7a7104a675823cd72dba8297f264b6637062516699fa006 url: "https://pub.dev" source: hosted - version: "1.3.2" + version: "1.4.0" universal_io: dependency: transitive description: @@ -705,66 +721,66 @@ packages: dependency: transitive description: name: url_launcher - sha256: b1c9e98774adf8820c96fbc7ae3601231d324a7d5ebd8babe27b6dfac91357ba + sha256: "9d06212b1362abc2f0f0d78e6f09f726608c74e3b9462e8368bb03314aa8d603" url: "https://pub.dev" source: hosted - version: "6.2.1" + version: "6.3.1" url_launcher_android: dependency: transitive description: name: url_launcher_android - sha256: "31222ffb0063171b526d3e569079cf1f8b294075ba323443fdc690842bfd4def" + sha256: "6fc2f56536ee873eeb867ad176ae15f304ccccc357848b351f6f0d8d4a40d193" url: "https://pub.dev" source: hosted - version: "6.2.0" + version: "6.3.14" url_launcher_ios: dependency: transitive description: name: url_launcher_ios - sha256: "4ac97281cf60e2e8c5cc703b2b28528f9b50c8f7cebc71df6bdf0845f647268a" + sha256: e43b677296fadce447e987a2f519dcf5f6d1e527dc35d01ffab4fff5b8a7063e url: "https://pub.dev" source: hosted - version: "6.2.0" + version: "6.3.1" url_launcher_linux: dependency: transitive description: name: url_launcher_linux - sha256: "9f2d390e096fdbe1e6e6256f97851e51afc2d9c423d3432f1d6a02a8a9a8b9fd" + sha256: "4e9ba368772369e3e08f231d2301b4ef72b9ff87c31192ef471b380ef29a4935" url: "https://pub.dev" source: hosted - version: "3.1.0" + version: "3.2.1" url_launcher_macos: dependency: transitive description: name: url_launcher_macos - sha256: b7244901ea3cf489c5335bdacda07264a6e960b1c1b1a9f91e4bc371d9e68234 + sha256: "769549c999acdb42b8bcfa7c43d72bf79a382ca7441ab18a808e101149daf672" url: "https://pub.dev" source: hosted - version: "3.1.0" + version: "3.2.1" url_launcher_platform_interface: dependency: transitive description: name: url_launcher_platform_interface - sha256: "980e8d9af422f477be6948bdfb68df8433be71f5743a188968b0c1b887807e50" + sha256: "552f8a1e663569be95a8190206a38187b531910283c3e982193e4f2733f01029" url: "https://pub.dev" source: hosted - version: "2.2.0" + version: "2.3.2" url_launcher_web: dependency: transitive description: name: url_launcher_web - sha256: "7fd2f55fe86cea2897b963e864dc01a7eb0719ecc65fcef4c1cc3d686d718bb2" + sha256: "772638d3b34c779ede05ba3d38af34657a05ac55b06279ea6edd409e323dca8e" url: "https://pub.dev" source: hosted - version: "2.2.0" + version: "2.3.3" url_launcher_windows: dependency: transitive description: name: url_launcher_windows - sha256: "7754a1ad30ee896b265f8d14078b0513a4dba28d358eabb9d5f339886f4a1adc" + sha256: "44cf3aabcedde30f2dba119a9dea3b0f2672fbe6fa96e85536251d678216b3c4" url: "https://pub.dev" source: hosted - version: "3.1.0" + version: "3.1.3" vector_math: dependency: transitive description: @@ -777,42 +793,50 @@ packages: dependency: transitive description: name: vm_service - sha256: b3d56ff4341b8f182b96aceb2fa20e3dcb336b9f867bc0eafc0de10f1048e957 + sha256: "5c5f338a667b4c644744b661f309fb8080bb94b18a7e91ef1dbd343bed00ed6d" url: "https://pub.dev" source: hosted - version: "13.0.0" + version: "14.2.5" + web: + dependency: transitive + description: + name: web + sha256: cd3543bd5798f6ad290ea73d210f423502e71900302dde696f8bff84bf89a1cb + url: "https://pub.dev" + source: hosted + version: "1.1.0" win32: dependency: transitive description: name: win32 - sha256: "9e82a402b7f3d518fb9c02d0e9ae45952df31b9bf34d77baf19da2de03fc2aaa" + sha256: "84ba388638ed7a8cb3445a320c8273136ab2631cd5f2c57888335504ddab1bc2" url: "https://pub.dev" source: hosted - version: "5.0.7" + version: "5.8.0" x509: dependency: transitive description: name: x509 - sha256: "6db77b0baecf54584f886607247e9dedd9fd63f1e2d0ee0a00b5bb353fd7885f" + sha256: cbd1a63846884afd273cda247b0365284c8d85a365ca98e110413f93d105b935 url: "https://pub.dev" source: hosted - version: "0.2.3" + version: "0.2.4+3" xdg_directories: dependency: transitive description: name: xdg_directories - sha256: "589ada45ba9e39405c198fe34eb0f607cddb2108527e658136120892beac46d2" + sha256: "7a3f37b05d989967cdddcbb571f1ea834867ae2faa29725fd085180e0883aa15" url: "https://pub.dev" source: hosted - version: "1.0.3" + version: "1.1.0" xml: dependency: transitive description: name: xml - sha256: "5bc72e1e45e941d825fd7468b9b4cc3b9327942649aeb6fc5cdbf135f0a86e84" + sha256: b015a8ad1c488f66851d762d3090a21c600e479dc75e68328c52774040cf9226 url: "https://pub.dev" source: hosted - version: "6.3.0" + version: "6.5.0" yaml: dependency: transitive description: @@ -822,5 +846,5 @@ packages: source: hosted version: "3.1.2" sdks: - dart: ">=3.2.0-0 <4.0.0" - flutter: ">=3.16.0" + dart: ">=3.5.0 <4.0.0" + flutter: ">=3.24.0" diff --git a/apps/tasks/pubspec.yaml b/apps/tasks/pubspec.yaml index 068c883b..0a2e82c2 100644 --- a/apps/tasks/pubspec.yaml +++ b/apps/tasks/pubspec.yaml @@ -18,7 +18,7 @@ publish_to: 'none' version: 1.0.0+2 environment: - sdk: '>=3.1.0' + sdk: '>=3.5.0' flutter: ">=3.16.0" # Dependencies specify other packages that your package needs in order to work. @@ -59,7 +59,7 @@ dev_dependencies: # activated in the `analysis_options.yaml` file located at the root of your # package. See that file for information about deactivating specific lint # rules and activating additional ones. - flutter_lints: ^2.0.0 + flutter_lints: ^5.0.0 # For information on the generic Dart part of this file, see the # following page: https://dart.dev/tools/pub/pubspec diff --git a/packages/helpwave_localization/pubspec.yaml b/packages/helpwave_localization/pubspec.yaml index 7e3e6156..dca88fba 100644 --- a/packages/helpwave_localization/pubspec.yaml +++ b/packages/helpwave_localization/pubspec.yaml @@ -4,7 +4,7 @@ version: 0.0.1 homepage: https://github.com/helpwave/mobile-app environment: - sdk: '>=3.1.0' + sdk: '>=3.5.0' flutter: ">=3.16.0" dependencies: @@ -12,13 +12,13 @@ dependencies: sdk: flutter flutter_localizations: sdk: flutter - intl: ^0.18.1 + intl: ^0.19.0 shared_preferences: ^2.0.15 dev_dependencies: flutter_test: sdk: flutter - flutter_lints: ^2.0.0 + flutter_lints: ^5.0.0 flutter: generate: true diff --git a/packages/helpwave_service/pubspec.yaml b/packages/helpwave_service/pubspec.yaml index 1639ab95..029cf896 100644 --- a/packages/helpwave_service/pubspec.yaml +++ b/packages/helpwave_service/pubspec.yaml @@ -4,7 +4,7 @@ version: 0.0.1 homepage: https://github.com/helpwave/mobile-app environment: - sdk: '>=3.1.0' + sdk: '>=3.5.0' flutter: ">=3.16.0" dependencies: @@ -20,8 +20,5 @@ dependencies: dev_dependencies: flutter_test: sdk: flutter - flutter_lints: ^2.0.0 - - -flutter: + flutter_lints: ^5.0.0 diff --git a/packages/helpwave_theme/pubspec.yaml b/packages/helpwave_theme/pubspec.yaml index 8f0e6bff..8b11ef56 100644 --- a/packages/helpwave_theme/pubspec.yaml +++ b/packages/helpwave_theme/pubspec.yaml @@ -6,7 +6,7 @@ homepage: https://github.com/helpwave/mobile-app publish_to: none environment: - sdk: '>=3.1.0' + sdk: '>=3.5.0' flutter: ">=3.16.0" dependencies: @@ -19,7 +19,7 @@ dependencies: dev_dependencies: flutter_test: sdk: flutter - flutter_lints: ^2.0.0 + flutter_lints: ^5.0.0 flutter: fonts: diff --git a/packages/helpwave_util/pubspec.yaml b/packages/helpwave_util/pubspec.yaml index 5511abea..1ba695a6 100644 --- a/packages/helpwave_util/pubspec.yaml +++ b/packages/helpwave_util/pubspec.yaml @@ -6,7 +6,7 @@ homepage: https://github.com/helpwave/mobile-app publish_to: none environment: - sdk: '>=3.1.0' + sdk: '>=3.5.0' flutter: ">=3.16.0" dependencies: @@ -16,6 +16,4 @@ dependencies: dev_dependencies: flutter_test: sdk: flutter - flutter_lints: ^2.0.0 - -flutter: + flutter_lints: ^5.0.0 diff --git a/packages/helpwave_widget/pubspec.yaml b/packages/helpwave_widget/pubspec.yaml index c0e91e36..81a07c5e 100644 --- a/packages/helpwave_widget/pubspec.yaml +++ b/packages/helpwave_widget/pubspec.yaml @@ -6,7 +6,7 @@ homepage: https://github.com/helpwave/mobile-app publish_to: none environment: - sdk: '>=3.1.0' + sdk: '>=3.5.0' flutter: ">=3.16.0" dependencies: @@ -22,6 +22,4 @@ dependencies: dev_dependencies: flutter_test: sdk: flutter - flutter_lints: ^2.0.0 - -flutter: + flutter_lints: ^5.0.0 From 3c1c519f74cb17293518c04778cbf01462866db8 Mon Sep 17 00:00:00 2001 From: Felix Thape Date: Wed, 20 Nov 2024 23:55:42 +0100 Subject: [PATCH 30/36] chore: make compatible with flutter 3.24.1 --- apps/tasks/pubspec.lock | 320 +++++++++++++++++++++++----------------- 1 file changed, 184 insertions(+), 136 deletions(-) diff --git a/apps/tasks/pubspec.lock b/apps/tasks/pubspec.lock index 54e89c51..724aad2b 100644 --- a/apps/tasks/pubspec.lock +++ b/apps/tasks/pubspec.lock @@ -1,30 +1,38 @@ # Generated by pub # See https://dart.dev/tools/pub/glossary#lockfile packages: + ansicolor: + dependency: transitive + description: + name: ansicolor + sha256: "50e982d500bc863e1d703448afdbf9e5a72eb48840a4f766fa361ffd6877055f" + url: "https://pub.dev" + source: hosted + version: "2.0.3" archive: dependency: transitive description: name: archive - sha256: "49b1fad315e57ab0bbc15bcbb874e83116a1d78f77ebd500a4af6c9407d6b28e" + sha256: cb6a278ef2dbb298455e1a713bda08524a175630ec643a242c399c932a0a1f7d url: "https://pub.dev" source: hosted - version: "3.3.8" + version: "3.6.1" args: dependency: transitive description: name: args - sha256: eef6c46b622e0494a36c5a12d10d77fb4e855501a91c1b9ef9339326e58f0596 + sha256: bf9f5caeea8d8fe6721a9c358dd8a5c1947b27f1cfaa18b39c301273594919e6 url: "https://pub.dev" source: hosted - version: "2.4.2" + version: "2.6.0" asn1lib: dependency: transitive description: name: asn1lib - sha256: "21afe4333076c02877d14f4a89df111e658a6d466cbfc802eb705eb91bd5adfd" + sha256: "4bae5ae63e6d6dd17c4aac8086f3dec26c0236f6a0f03416c6c19d830c367cf5" url: "https://pub.dev" source: hosted - version: "1.5.0" + version: "1.5.8" async: dependency: transitive description: @@ -61,10 +69,10 @@ packages: dependency: transitive description: name: cli_util - sha256: b8db3080e59b2503ca9e7922c3df2072cf13992354d5e944074ffa836fba43b7 + sha256: ff6785f7e9e3c38ac98b2fb035701789de90154024a75b6cb926445e83197d1c url: "https://pub.dev" source: hosted - version: "0.4.0" + version: "0.4.2" clock: dependency: transitive description: @@ -85,18 +93,18 @@ packages: dependency: transitive description: name: convert - sha256: "0f08b14755d163f6e2134cb58222dd25ea2a2ee8a195e53983d57c075324d592" + sha256: b30acd5944035672bc15c6b7a8b47d773e41e2f17de064350988c5d02adb1c68 url: "https://pub.dev" source: hosted - version: "3.1.1" + version: "3.1.2" crypto: dependency: transitive description: name: crypto - sha256: ff625774173754681d66daaf4a448684fb04b78f902da9cb3d308c19cc5e8bab + sha256: "1e445881f28f22d6140f181e07737b22f1e099a5e1ff94b0af2f9e4a463f4855" url: "https://pub.dev" source: hosted - version: "3.0.3" + version: "3.0.6" crypto_keys: dependency: transitive description: @@ -109,18 +117,18 @@ packages: dependency: transitive description: name: csslib - sha256: "706b5707578e0c1b4b7550f64078f0a0f19dec3f50a178ffae7006b0a9ca58fb" + sha256: "09bad715f418841f976c77db72d5398dc1253c21fb9c0c7f0b0b985860b2d58e" url: "https://pub.dev" source: hosted - version: "1.0.0" + version: "1.0.2" cupertino_icons: dependency: "direct main" description: name: cupertino_icons - sha256: d57953e10f9f8327ce64a508a355f0b1ec902193f66288e8cb5070e7c47eeb2d + sha256: ba631d1c7f7bef6b729a622b7b752645a2d076dba9976925b8f25725a30e1ee6 url: "https://pub.dev" source: hosted - version: "1.0.6" + version: "1.0.8" fake_async: dependency: transitive description: @@ -133,26 +141,26 @@ packages: dependency: transitive description: name: ffi - sha256: "7bf0adc28a23d395f19f3f1eb21dd7cfd1dd9f8e1c50051c069122e6853bc878" + sha256: "16ed7b077ef01ad6170a3d0c57caa4a112a38d7a2ed5602e0aca9ca6f3d98da6" url: "https://pub.dev" source: hosted - version: "2.1.0" + version: "2.1.3" file: dependency: transitive description: name: file - sha256: "1b92bec4fc2a72f59a8e15af5f52cd441e4a7860b49499d69dfa817af20e925d" + sha256: a3b4f84adafef897088c160faf7dfffb7696046cb13ae90b508c2cbc95d3b8d4 url: "https://pub.dev" source: hosted - version: "6.1.4" + version: "7.0.1" fixnum: dependency: transitive description: name: fixnum - sha256: "25517a4deb0c03aa0f32fd12db525856438902d9c16536311e76cdc57b31d7d1" + sha256: b6dc7065e46c974bc7c5f143080a6764ec7a4be6da1285ececdc37be96de53be url: "https://pub.dev" source: hosted - version: "1.1.0" + version: "1.1.1" flutter: dependency: "direct main" description: flutter @@ -170,10 +178,10 @@ packages: dependency: "direct dev" description: name: flutter_lints - sha256: a25a15ebbdfc33ab1cd26c63a6ee519df92338a9c10f122adda92938253bef04 + sha256: "5398f14efa795ffb7a33e9b6a08798b26a180edac4ad7db3f231e40f82ce11e1" url: "https://pub.dev" source: hosted - version: "2.0.3" + version: "5.0.0" flutter_localizations: dependency: transitive description: flutter @@ -183,10 +191,10 @@ packages: dependency: "direct main" description: name: flutter_native_splash - sha256: ecff62b3b893f2f665de7e4ad3de89f738941fcfcaaba8ee601e749efafa4698 + sha256: "1152ab0067ca5a2ebeb862fe0a762057202cceb22b7e62692dcbabf6483891bb" url: "https://pub.dev" source: hosted - version: "2.3.2" + version: "2.4.3" flutter_secure_storage: dependency: transitive description: @@ -199,42 +207,42 @@ packages: dependency: transitive description: name: flutter_secure_storage_linux - sha256: "3d5032e314774ee0e1a7d0a9f5e2793486f0dff2dd9ef5a23f4e3fb2a0ae6a9e" + sha256: "4d91bfc23047422cbcd73ac684bc169859ee766482517c22172c86596bf1464b" url: "https://pub.dev" source: hosted - version: "1.2.0" + version: "1.2.1" flutter_secure_storage_macos: dependency: transitive description: name: flutter_secure_storage_macos - sha256: bd33935b4b628abd0b86c8ca20655c5b36275c3a3f5194769a7b3f37c905369c + sha256: "1693ab11121a5f925bbea0be725abfcfbbcf36c1e29e571f84a0c0f436147a81" url: "https://pub.dev" source: hosted - version: "3.0.1" + version: "3.1.2" flutter_secure_storage_platform_interface: dependency: transitive description: name: flutter_secure_storage_platform_interface - sha256: "0d4d3a5dd4db28c96ae414d7ba3b8422fd735a8255642774803b2532c9a61d7e" + sha256: cf91ad32ce5adef6fba4d736a542baca9daf3beac4db2d04be350b87f69ac4a8 url: "https://pub.dev" source: hosted - version: "1.0.2" + version: "1.1.2" flutter_secure_storage_web: dependency: transitive description: name: flutter_secure_storage_web - sha256: "30f84f102df9dcdaa2241866a958c2ec976902ebdaa8883fbfe525f1f2f3cf20" + sha256: f4ebff989b4f07b2656fb16b47852c0aab9fed9b4ec1c70103368337bc1886a9 url: "https://pub.dev" source: hosted - version: "1.1.2" + version: "1.2.1" flutter_secure_storage_windows: dependency: transitive description: name: flutter_secure_storage_windows - sha256: "5809c66f9dd3b4b93b0a6e2e8561539405322ee767ac2f64d084e2ab5429d108" + sha256: b20b07cb5ed4ed74fc567b78a72936203f587eba460af1df11281c9326cd3709 url: "https://pub.dev" source: hosted - version: "3.0.0" + version: "3.1.2" flutter_test: dependency: "direct dev" description: flutter @@ -245,14 +253,22 @@ packages: description: flutter source: sdk version: "0.0.0" + google_identity_services_web: + dependency: transitive + description: + name: google_identity_services_web + sha256: "55580f436822d64c8ff9a77e37d61f5fb1e6c7ec9d632a43ee324e2a05c3c6c9" + url: "https://pub.dev" + source: hosted + version: "0.3.3" googleapis_auth: dependency: transitive description: name: googleapis_auth - sha256: af7c3a3edf9d0de2e1e0a77e994fae0a581c525fa7012af4fa0d4a52ed9484da + sha256: befd71383a955535060acde8792e7efc11d2fccd03dd1d3ec434e85b68775938 url: "https://pub.dev" source: hosted - version: "1.4.1" + version: "1.6.0" grpc: dependency: transitive description: @@ -272,10 +288,10 @@ packages: dependency: transitive description: name: helpwave_proto_dart - sha256: "354e0677387867f3900add1e7a6b899d6c6725e8516ee07023d0c145414bc2d5" + sha256: eeff6a1a3645e412ad2a8024afd113c1d10779ddb039d5824f8a8bab203ff990 url: "https://pub.dev" source: hosted - version: "0.58.0-1191f09" + version: "0.58.1-688aac0" helpwave_service: dependency: "direct main" description: @@ -308,26 +324,26 @@ packages: dependency: transitive description: name: html - sha256: "3a7812d5bcd2894edf53dfaf8cd640876cf6cef50a8f238745c8b8120ea74d3a" + sha256: "1fc58edeaec4307368c60d59b7e15b9d658b57d7f3125098b6294153c75337ec" url: "https://pub.dev" source: hosted - version: "0.15.4" + version: "0.15.5" http: dependency: transitive description: name: http - sha256: "5895291c13fa8a3bd82e76d5627f69e0d85ca6a30dcac95c4ea19a5d555879c2" + sha256: b9c29a161230ee03d3ccf545097fccd9b87a5264228c5d348202e0f0c28f9010 url: "https://pub.dev" source: hosted - version: "0.13.6" + version: "1.2.2" http2: dependency: transitive description: name: http2 - sha256: "38db0c4aa9f1cd238a5d2e86aa0cc7cc91c77e0c6c94ba64bbe85e4ff732a952" + sha256: "9ced024a160b77aba8fb8674e38f70875e321d319e6f303ec18e87bd5a4b0c1d" url: "https://pub.dev" source: hosted - version: "2.2.0" + version: "2.3.0" http_parser: dependency: transitive description: @@ -340,18 +356,18 @@ packages: dependency: transitive description: name: image - sha256: a72242c9a0ffb65d03de1b7113bc4e189686fc07c7147b8b41811d0dd0e0d9bf + sha256: f31d52537dc417fdcde36088fdf11d191026fd5e4fae742491ebd40e5a8bea7d url: "https://pub.dev" source: hosted - version: "4.0.17" + version: "4.3.0" intl: dependency: transitive description: name: intl - sha256: "3bc132a9dbce73a7e4a21a17d06e1878839ffbf975568bc875c60537824b0c4d" + sha256: d6f56758b7d3014a48af9701c085700aac781a92a87a62b1333b46d8879661cf url: "https://pub.dev" source: hosted - version: "0.18.1" + version: "0.19.0" jose: dependency: transitive description: @@ -372,58 +388,82 @@ packages: dependency: transitive description: name: json_annotation - sha256: b10a7b2ff83d83c777edba3c6a0f97045ddadd56c944e1a23a3fdf43a1bf4467 + sha256: "1ce844379ca14835a50d2f019a3099f419082cfdd231cd86a142af94dd5c6bb1" + url: "https://pub.dev" + source: hosted + version: "4.9.0" + leak_tracker: + dependency: transitive + description: + name: leak_tracker + sha256: "3f87a60e8c63aecc975dda1ceedbc8f24de75f09e4856ea27daf8958f2f0ce05" + url: "https://pub.dev" + source: hosted + version: "10.0.5" + leak_tracker_flutter_testing: + dependency: transitive + description: + name: leak_tracker_flutter_testing + sha256: "932549fb305594d82d7183ecd9fa93463e9914e1b67cacc34bc40906594a1806" + url: "https://pub.dev" + source: hosted + version: "3.0.5" + leak_tracker_testing: + dependency: transitive + description: + name: leak_tracker_testing + sha256: "6ba465d5d76e67ddf503e1161d1f4a6bc42306f9d66ca1e8f079a47290fb06d3" url: "https://pub.dev" source: hosted - version: "4.8.1" + version: "3.0.1" lints: dependency: transitive description: name: lints - sha256: "0a217c6c989d21039f1498c3ed9f3ed71b354e69873f13a8dfc3c9fe76f1b452" + sha256: "3315600f3fb3b135be672bf4a178c55f274bebe368325ae18462c89ac1e3b413" url: "https://pub.dev" source: hosted - version: "2.1.1" + version: "5.0.0" logger: dependency: "direct main" description: name: logger - sha256: "6bbb9d6f7056729537a4309bda2e74e18e5d9f14302489cc1e93f33b3fe32cac" + sha256: be4b23575aac7ebf01f225a241eb7f6b5641eeaf43c6a8613510fc2f8cf187d1 url: "https://pub.dev" source: hosted - version: "2.0.2+1" + version: "2.5.0" logging: dependency: transitive description: name: logging - sha256: "623a88c9594aa774443aa3eb2d41807a48486b5613e67599fb4c41c0ad47c340" + sha256: c8245ada5f1717ed44271ed1c26b8ce85ca3228fd2ffdb75468ab01979309d61 url: "https://pub.dev" source: hosted - version: "1.2.0" + version: "1.3.0" matcher: dependency: transitive description: name: matcher - sha256: "1803e76e6653768d64ed8ff2e1e67bea3ad4b923eb5c56a295c3e634bad5960e" + sha256: d2323aa2060500f906aa31a895b4030b6da3ebdcc5619d14ce1aada65cd161cb url: "https://pub.dev" source: hosted - version: "0.12.16" + version: "0.12.16+1" material_color_utilities: dependency: transitive description: name: material_color_utilities - sha256: "9528f2f296073ff54cb9fee677df673ace1218163c3bc7628093e7eed5203d41" + sha256: f7142bb1154231d7ea5f96bc7bde4bda2a0945d2806bb11670e30b850d56bdec url: "https://pub.dev" source: hosted - version: "0.5.0" + version: "0.11.1" meta: dependency: transitive description: name: meta - sha256: a6e590c838b18133bb482a2745ad77c5bb7715fb0451209e1a7567d416678b8e + sha256: bdb68674043280c3428e9ec998512fb681678676b3c54e773629ffe74419f8c7 url: "https://pub.dev" source: hosted - version: "1.10.0" + version: "1.15.0" nested: dependency: transitive description: @@ -436,42 +476,42 @@ packages: dependency: transitive description: name: openid_client - sha256: "514c0ba645b81029c28999831a70cb055dda1a3bc60be759a04d2556f60ec960" + sha256: "043878e907b7a1b460b54fb7b3b27b101cf70d4ac28b32a2db87ae67dbaed611" url: "https://pub.dev" source: hosted - version: "0.4.7" + version: "0.4.8" path: dependency: transitive description: name: path - sha256: "8829d8a55c13fc0e37127c29fedf290c102f4e40ae94ada574091fe0ff96c917" + sha256: "087ce49c3f0dc39180befefc60fdb4acd8f8620e5682fe2476afd0b3688bb4af" url: "https://pub.dev" source: hosted - version: "1.8.3" + version: "1.9.0" path_provider: dependency: transitive description: name: path_provider - sha256: a1aa8aaa2542a6bc57e381f132af822420216c80d4781f7aa085ca3229208aaa + sha256: "50c5dd5b6e1aaf6fb3a78b33f6aa3afca52bf903a8a5298f53101fdaee55bbcd" url: "https://pub.dev" source: hosted - version: "2.1.1" + version: "2.1.5" path_provider_android: dependency: transitive description: name: path_provider_android - sha256: e595b98692943b4881b219f0a9e3945118d3c16bd7e2813f98ec6e532d905f72 + sha256: c464428172cb986b758c6d1724c603097febb8fb855aa265aeecc9280c294d4a url: "https://pub.dev" source: hosted - version: "2.2.1" + version: "2.2.12" path_provider_foundation: dependency: transitive description: name: path_provider_foundation - sha256: "19314d595120f82aca0ba62787d58dde2cc6b5df7d2f0daf72489e38d1b57f2d" + sha256: f234384a3fdd67f989b4d54a5d73ca2a6c422fa55ae694381ae0f4375cd1ea16 url: "https://pub.dev" source: hosted - version: "2.3.1" + version: "2.4.0" path_provider_linux: dependency: transitive description: @@ -484,50 +524,50 @@ packages: dependency: transitive description: name: path_provider_platform_interface - sha256: "94b1e0dd80970c1ce43d5d4e050a9918fce4f4a775e6142424c30a29a363265c" + sha256: "88f5779f72ba699763fa3a3b06aa4bf6de76c8e5de842cf6f29e2e06476c2334" url: "https://pub.dev" source: hosted - version: "2.1.1" + version: "2.1.2" path_provider_windows: dependency: transitive description: name: path_provider_windows - sha256: "8bc9f22eee8690981c22aa7fc602f5c85b497a6fb2ceb35ee5a5e5ed85ad8170" + sha256: bd6f00dbd873bfb70d0761682da2b3a2c2fccc2b9e84c495821639601d81afe7 url: "https://pub.dev" source: hosted - version: "2.2.1" + version: "2.3.0" petitparser: dependency: transitive description: name: petitparser - sha256: cb3798bef7fc021ac45b308f4b51208a152792445cce0448c9a4ba5879dd8750 + sha256: c15605cd28af66339f8eb6fbe0e541bfe2d1b72d5825efc6598f3e0a31b9ad27 url: "https://pub.dev" source: hosted - version: "5.4.0" + version: "6.0.2" platform: dependency: transitive description: name: platform - sha256: ae68c7bfcd7383af3629daafb32fb4e8681c7154428da4febcff06200585f102 + sha256: "5d6b1b0036a5f331ebc77c850ebc8506cbc1e9416c27e59b439f917a902a4984" url: "https://pub.dev" source: hosted - version: "3.1.2" + version: "3.1.6" plugin_platform_interface: dependency: transitive description: name: plugin_platform_interface - sha256: da3fdfeccc4d4ff2da8f8c556704c08f912542c5fb3cf2233ed75372384a034d + sha256: "4820fbfdb9478b1ebae27888254d445073732dae3d6ea81f0b7e06d5dedc3f02" url: "https://pub.dev" source: hosted - version: "2.1.6" + version: "2.1.8" pointycastle: dependency: transitive description: name: pointycastle - sha256: "7c1e5f0d23c9016c5bbd8b1473d0d3fb3fc851b876046039509e18e0c7485f2c" + sha256: "4be0097fcf3fd3e8449e53730c631200ebc7b88016acecab2b0da2f0149222fe" url: "https://pub.dev" source: hosted - version: "3.7.3" + version: "3.9.1" protobuf: dependency: transitive description: @@ -540,74 +580,74 @@ packages: dependency: "direct main" description: name: provider - sha256: cdbe7530b12ecd9eb455bdaa2fcb8d4dad22e80b8afb4798b41479d5ce26847f + sha256: c8a055ee5ce3fd98d6fc872478b03823ffdb448699c6ebdbbc71d59b596fd48c url: "https://pub.dev" source: hosted - version: "6.0.5" + version: "6.1.2" quiver: dependency: transitive description: name: quiver - sha256: b1c1ac5ce6688d77f65f3375a9abb9319b3cb32486bdc7a1e0fdf004d7ba4e47 + sha256: ea0b925899e64ecdfbf9c7becb60d5b50e706ade44a85b2363be2a22d88117d2 url: "https://pub.dev" source: hosted - version: "3.2.1" + version: "3.2.2" shared_preferences: dependency: "direct main" description: name: shared_preferences - sha256: b7f41bad7e521d205998772545de63ff4e6c97714775902c199353f8bf1511ac + sha256: "95f9997ca1fb9799d494d0cb2a780fd7be075818d59f00c43832ed112b158a82" url: "https://pub.dev" source: hosted - version: "2.2.1" + version: "2.3.3" shared_preferences_android: dependency: transitive description: name: shared_preferences_android - sha256: "8568a389334b6e83415b6aae55378e158fbc2314e074983362d20c562780fb06" + sha256: "3b9febd815c9ca29c9e3520d50ec32f49157711e143b7a4ca039eb87e8ade5ab" url: "https://pub.dev" source: hosted - version: "2.2.1" + version: "2.3.3" shared_preferences_foundation: dependency: transitive description: name: shared_preferences_foundation - sha256: "7bf53a9f2d007329ee6f3df7268fd498f8373602f943c975598bbb34649b62a7" + sha256: "07e050c7cd39bad516f8d64c455f04508d09df104be326d8c02551590a0d513d" url: "https://pub.dev" source: hosted - version: "2.3.4" + version: "2.5.3" shared_preferences_linux: dependency: transitive description: name: shared_preferences_linux - sha256: c2eb5bf57a2fe9ad6988121609e47d3e07bb3bdca5b6f8444e4cf302428a128a + sha256: "580abfd40f415611503cae30adf626e6656dfb2f0cee8f465ece7b6defb40f2f" url: "https://pub.dev" source: hosted - version: "2.3.1" + version: "2.4.1" shared_preferences_platform_interface: dependency: transitive description: name: shared_preferences_platform_interface - sha256: d4ec5fc9ebb2f2e056c617112aa75dcf92fc2e4faaf2ae999caa297473f75d8a + sha256: "57cbf196c486bc2cf1f02b85784932c6094376284b3ad5779d1b1c6c6a816b80" url: "https://pub.dev" source: hosted - version: "2.3.1" + version: "2.4.1" shared_preferences_web: dependency: transitive description: name: shared_preferences_web - sha256: d762709c2bbe80626ecc819143013cc820fa49ca5e363620ee20a8b15a3e3daf + sha256: d2ca4132d3946fec2184261726b355836a82c33d7d5b67af32692aff18a4684e url: "https://pub.dev" source: hosted - version: "2.2.1" + version: "2.4.2" shared_preferences_windows: dependency: transitive description: name: shared_preferences_windows - sha256: f763a101313bd3be87edffe0560037500967de9c394a714cd598d945517f694f + sha256: "94ef0f72b2d71bc3e700e025db3710911bd51a71cefb65cc609dd0d9a982e3c1" url: "https://pub.dev" source: hosted - version: "2.3.1" + version: "2.4.1" sky_engine: dependency: transitive description: flutter @@ -657,18 +697,18 @@ packages: dependency: transitive description: name: test_api - sha256: "5c2f730018264d276c20e4f1503fd1308dfbbae39ec8ee63c5236311ac06954b" + sha256: "5b8a98dafc4d5c4c9c72d8b31ab2b23fc13422348d2997120294d3bac86b4ddb" url: "https://pub.dev" source: hosted - version: "0.6.1" + version: "0.7.2" typed_data: dependency: transitive description: name: typed_data - sha256: facc8d6582f16042dd49f2463ff1bd6e2c9ef9f3d5da3d9b087e244a7b564b3c + sha256: f9049c039ebfeb4cf7a7104a675823cd72dba8297f264b6637062516699fa006 url: "https://pub.dev" source: hosted - version: "1.3.2" + version: "1.4.0" universal_io: dependency: transitive description: @@ -681,66 +721,66 @@ packages: dependency: transitive description: name: url_launcher - sha256: b1c9e98774adf8820c96fbc7ae3601231d324a7d5ebd8babe27b6dfac91357ba + sha256: "9d06212b1362abc2f0f0d78e6f09f726608c74e3b9462e8368bb03314aa8d603" url: "https://pub.dev" source: hosted - version: "6.2.1" + version: "6.3.1" url_launcher_android: dependency: transitive description: name: url_launcher_android - sha256: "31222ffb0063171b526d3e569079cf1f8b294075ba323443fdc690842bfd4def" + sha256: "6fc2f56536ee873eeb867ad176ae15f304ccccc357848b351f6f0d8d4a40d193" url: "https://pub.dev" source: hosted - version: "6.2.0" + version: "6.3.14" url_launcher_ios: dependency: transitive description: name: url_launcher_ios - sha256: "4ac97281cf60e2e8c5cc703b2b28528f9b50c8f7cebc71df6bdf0845f647268a" + sha256: e43b677296fadce447e987a2f519dcf5f6d1e527dc35d01ffab4fff5b8a7063e url: "https://pub.dev" source: hosted - version: "6.2.0" + version: "6.3.1" url_launcher_linux: dependency: transitive description: name: url_launcher_linux - sha256: "9f2d390e096fdbe1e6e6256f97851e51afc2d9c423d3432f1d6a02a8a9a8b9fd" + sha256: "4e9ba368772369e3e08f231d2301b4ef72b9ff87c31192ef471b380ef29a4935" url: "https://pub.dev" source: hosted - version: "3.1.0" + version: "3.2.1" url_launcher_macos: dependency: transitive description: name: url_launcher_macos - sha256: b7244901ea3cf489c5335bdacda07264a6e960b1c1b1a9f91e4bc371d9e68234 + sha256: "769549c999acdb42b8bcfa7c43d72bf79a382ca7441ab18a808e101149daf672" url: "https://pub.dev" source: hosted - version: "3.1.0" + version: "3.2.1" url_launcher_platform_interface: dependency: transitive description: name: url_launcher_platform_interface - sha256: "980e8d9af422f477be6948bdfb68df8433be71f5743a188968b0c1b887807e50" + sha256: "552f8a1e663569be95a8190206a38187b531910283c3e982193e4f2733f01029" url: "https://pub.dev" source: hosted - version: "2.2.0" + version: "2.3.2" url_launcher_web: dependency: transitive description: name: url_launcher_web - sha256: "7fd2f55fe86cea2897b963e864dc01a7eb0719ecc65fcef4c1cc3d686d718bb2" + sha256: "772638d3b34c779ede05ba3d38af34657a05ac55b06279ea6edd409e323dca8e" url: "https://pub.dev" source: hosted - version: "2.2.0" + version: "2.3.3" url_launcher_windows: dependency: transitive description: name: url_launcher_windows - sha256: "7754a1ad30ee896b265f8d14078b0513a4dba28d358eabb9d5f339886f4a1adc" + sha256: "44cf3aabcedde30f2dba119a9dea3b0f2672fbe6fa96e85536251d678216b3c4" url: "https://pub.dev" source: hosted - version: "3.1.0" + version: "3.1.3" vector_math: dependency: transitive description: @@ -749,46 +789,54 @@ packages: url: "https://pub.dev" source: hosted version: "2.1.4" + vm_service: + dependency: transitive + description: + name: vm_service + sha256: "5c5f338a667b4c644744b661f309fb8080bb94b18a7e91ef1dbd343bed00ed6d" + url: "https://pub.dev" + source: hosted + version: "14.2.5" web: dependency: transitive description: name: web - sha256: afe077240a270dcfd2aafe77602b4113645af95d0ad31128cc02bce5ac5d5152 + sha256: cd3543bd5798f6ad290ea73d210f423502e71900302dde696f8bff84bf89a1cb url: "https://pub.dev" source: hosted - version: "0.3.0" + version: "1.1.0" win32: dependency: transitive description: name: win32 - sha256: "9e82a402b7f3d518fb9c02d0e9ae45952df31b9bf34d77baf19da2de03fc2aaa" + sha256: "84ba388638ed7a8cb3445a320c8273136ab2631cd5f2c57888335504ddab1bc2" url: "https://pub.dev" source: hosted - version: "5.0.7" + version: "5.8.0" x509: dependency: transitive description: name: x509 - sha256: "6db77b0baecf54584f886607247e9dedd9fd63f1e2d0ee0a00b5bb353fd7885f" + sha256: cbd1a63846884afd273cda247b0365284c8d85a365ca98e110413f93d105b935 url: "https://pub.dev" source: hosted - version: "0.2.3" + version: "0.2.4+3" xdg_directories: dependency: transitive description: name: xdg_directories - sha256: "589ada45ba9e39405c198fe34eb0f607cddb2108527e658136120892beac46d2" + sha256: "7a3f37b05d989967cdddcbb571f1ea834867ae2faa29725fd085180e0883aa15" url: "https://pub.dev" source: hosted - version: "1.0.3" + version: "1.1.0" xml: dependency: transitive description: name: xml - sha256: "5bc72e1e45e941d825fd7468b9b4cc3b9327942649aeb6fc5cdbf135f0a86e84" + sha256: b015a8ad1c488f66851d762d3090a21c600e479dc75e68328c52774040cf9226 url: "https://pub.dev" source: hosted - version: "6.3.0" + version: "6.5.0" yaml: dependency: transitive description: @@ -798,5 +846,5 @@ packages: source: hosted version: "3.1.2" sdks: - dart: ">=3.2.0-194.0.dev <4.0.0" - flutter: ">=3.16.0" + dart: ">=3.5.0 <4.0.0" + flutter: ">=3.24.0" From 5a75284655786ea247ae6aed8522936b0bf3dc6e Mon Sep 17 00:00:00 2001 From: Felix <67233923+DasProffi@users.noreply.github.com> Date: Thu, 21 Nov 2024 00:27:26 +0100 Subject: [PATCH 31/36] chore: update flutter version in CI --- .github/workflows/tasks-android.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/tasks-android.yaml b/.github/workflows/tasks-android.yaml index d4d619d3..907e9216 100644 --- a/.github/workflows/tasks-android.yaml +++ b/.github/workflows/tasks-android.yaml @@ -2,7 +2,7 @@ name: Deploy tasks to Google Play env: RUBY_VERSION: '2.6.10' - FLUTTER_VERSION: '3.16.0' + FLUTTER_VERSION: '3.24.1' MELOS_VERSION: '3.0.0' on: From 10a532e72268b1ccd650a218db35c5ba2e1692ef Mon Sep 17 00:00:00 2001 From: Felix <67233923+DasProffi@users.noreply.github.com> Date: Thu, 21 Nov 2024 00:27:51 +0100 Subject: [PATCH 32/36] chore: update flutter version in CI --- .github/workflows/ci.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index e1a5586a..a2954fa4 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -1,7 +1,7 @@ name: CI env: - FLUTTER_VERSION: '3.16.0' + FLUTTER_VERSION: '3.24.1' on: push: From b22a912617cf33bd2fafb915fc1f6b38d2f8fdc5 Mon Sep 17 00:00:00 2001 From: Felix Thape Date: Thu, 21 Nov 2024 11:03:54 +0100 Subject: [PATCH 33/36] feat: add CRUD interface and use it for property and organization --- .../organization_bottom_sheet.dart | 16 +-- .../property_bottom_sheet.dart | 26 ++-- .../bottom_sheet_pages/task_bottom_sheet.dart | 2 +- .../wards_bottom_sheet_page.dart | 2 +- apps/tasks/lib/screens/settings_screen.dart | 2 +- .../src/api/offline/offline_client_store.dart | 17 +-- .../controllers/property_controller.dart | 83 +----------- .../data_types/attached_property.dart | 3 +- .../src/api/property/data_types/property.dart | 27 +++- .../api/property/data_types/select_data.dart | 3 +- .../property/services/property_service.dart | 21 ++-- .../tasks/controllers/beds_controller.dart | 4 +- .../tasks/controllers/patient_controller.dart | 1 + .../tasks/controllers/rooms_controller.dart | 4 +- .../controllers/subtask_list_controller.dart | 6 +- .../tasks/controllers/task_controller.dart | 4 +- .../controllers/task_template_controller.dart | 4 +- .../tasks/controllers/ward_controller.dart | 1 + .../lib/src/api/tasks/data_types/patient.dart | 2 +- .../lib/src/api/tasks/data_types/subtask.dart | 2 +- .../lib/src/api/tasks/data_types/task.dart | 2 +- .../lib/src/api/tasks/data_types/ward.dart | 2 +- .../lib/src/api/tasks/services/task_svc.dart | 2 + .../controllers/organization_controller.dart | 95 ++++---------- .../api/user/controllers/user_controller.dart | 1 + .../src/api/user/data_types/invitation.dart | 3 +- .../src/api/user/data_types/organization.dart | 119 ++++++++++-------- .../lib/src/api/user/data_types/user.dart | 5 +- .../organization_offline_client.dart | 117 ++++++++--------- .../offline_clients/user_offline_client.dart | 2 +- .../api/user/services/organization_svc.dart | 92 ++++++++------ .../lib/src/api/util/copy_with_interface.dart | 3 - .../lib/src/auth/current_ward_svc.dart | 14 +-- packages/helpwave_service/lib/src/config.dart | 1 + .../lib/src/util/copy_with_interface.dart | 3 + .../lib/src/util/crud_object_interface.dart | 9 ++ .../crud_service_interface.dart} | 8 +- .../src/{api => }/util/identified_object.dart | 7 +- .../helpwave_service/lib/src/util/index.dart | 5 + .../lib/src/util/load_controller.dart | 112 +++++++++++++++++ packages/helpwave_service/lib/util.dart | 1 + 41 files changed, 447 insertions(+), 386 deletions(-) delete mode 100644 packages/helpwave_service/lib/src/api/util/copy_with_interface.dart create mode 100644 packages/helpwave_service/lib/src/config.dart create mode 100644 packages/helpwave_service/lib/src/util/copy_with_interface.dart create mode 100644 packages/helpwave_service/lib/src/util/crud_object_interface.dart rename packages/helpwave_service/lib/src/{api/util/crud_interface.dart => util/crud_service_interface.dart} (53%) rename packages/helpwave_service/lib/src/{api => }/util/identified_object.dart (73%) create mode 100644 packages/helpwave_service/lib/src/util/index.dart create mode 100644 packages/helpwave_service/lib/src/util/load_controller.dart create mode 100644 packages/helpwave_service/lib/util.dart diff --git a/apps/tasks/lib/components/bottom_sheet_pages/organization_bottom_sheet.dart b/apps/tasks/lib/components/bottom_sheet_pages/organization_bottom_sheet.dart index 8ff061af..1eeef1a7 100644 --- a/apps/tasks/lib/components/bottom_sheet_pages/organization_bottom_sheet.dart +++ b/apps/tasks/lib/components/bottom_sheet_pages/organization_bottom_sheet.dart @@ -22,7 +22,7 @@ class OrganizationBottomSheetPage extends StatelessWidget { @override Widget build(BuildContext context) { return ChangeNotifierProvider( - create: (context) => OrganizationController(Organization.empty(organizationId)), + create: (context) => OrganizationController(id: organizationId), child: Consumer(builder: (context, controller, _) { return BottomSheetPage( header: BottomSheetHeader.navigation( @@ -30,7 +30,7 @@ class OrganizationBottomSheetPage extends StatelessWidget { title: LoadingAndErrorWidget.pulsing( state: controller.state, child: Text( - controller.organization.combinedName, + controller.data.combinedName, style: const TextStyle(fontWeight: FontWeight.w800, fontSize: 16), ), ), @@ -45,23 +45,23 @@ class OrganizationBottomSheetPage extends StatelessWidget { Text(context.localization.shortName, style: context.theme.textTheme.titleMedium), const SizedBox(height: distanceTiny), TextFormFieldWithTimer( - initialValue: controller.organization.shortName, - onUpdate: (value) => controller.update(shortName: value), + initialValue: controller.data.shortName, + onUpdate: (value) => controller.update(OrganizationUpdate(shortName: value)), ), const SizedBox(height: distanceMedium), Text(context.localization.longName, style: context.theme.textTheme.titleMedium), const SizedBox(height: distanceTiny), TextFormFieldWithTimer( - initialValue: controller.organization.longName, - onUpdate: (value) => controller.update(longName: value), + initialValue: controller.data.longName, + onUpdate: (value) => controller.update(OrganizationUpdate(longName: value)), ), const SizedBox(height: distanceMedium), Text(context.localization.contactEmail, style: context.theme.textTheme.titleMedium), const SizedBox(height: distanceTiny), TextFormFieldWithTimer( - initialValue: controller.organization.email, + initialValue: controller.data.details?.email, // TODO validation - onUpdate: (value) => controller.update(email: value), + onUpdate: (value) => controller.update(OrganizationUpdate(email: value)), ), const SizedBox(height: distanceMedium), Text(context.localization.settings, style: context.theme.textTheme.titleMedium), diff --git a/apps/tasks/lib/components/bottom_sheet_pages/property_bottom_sheet.dart b/apps/tasks/lib/components/bottom_sheet_pages/property_bottom_sheet.dart index 7d1b983d..b94e13be 100644 --- a/apps/tasks/lib/components/bottom_sheet_pages/property_bottom_sheet.dart +++ b/apps/tasks/lib/components/bottom_sheet_pages/property_bottom_sheet.dart @@ -9,7 +9,7 @@ import 'package:helpwave_widget/loading.dart'; import 'package:helpwave_widget/text_input.dart'; import 'package:provider/provider.dart'; import 'package:tasks/util/field_type_translations.dart'; - +import 'package:helpwave_service/util.dart'; import '../../util/subject_type_translations.dart'; class PropertyBottomSheetPage extends StatelessWidget { @@ -36,7 +36,7 @@ class PropertyBottomSheetPage extends StatelessWidget { builder: (context, controller, _) => LoadingAndErrorWidget( state: controller.state, loadingWidget: const PulsingContainer(width: 50), - child: Text(controller.property.isCreating ? createTitle : editTitle, + child: Text(controller.data.isCreating ? createTitle : editTitle, style: context.theme.textTheme.titleMedium), ), ), @@ -60,7 +60,7 @@ class PropertyBottomSheetPage extends StatelessWidget { ), const SizedBox(height: paddingTiny), TextFormFieldWithTimer( - initialValue: controller.property.name, + initialValue: controller.data.name, onUpdate: (value) => controller.update(PropertyUpdate(name: value)), ), const SizedBox(height: paddingSmall), @@ -71,7 +71,7 @@ class PropertyBottomSheetPage extends StatelessWidget { const SizedBox(height: paddingTiny), DropdownButtonFormField( decoration: inputDecoration, - value: controller.property.subjectType, + value: controller.data.subjectType, items: PropertySubjectType.values .map((value) => DropdownMenuItem( value: value, @@ -87,7 +87,7 @@ class PropertyBottomSheetPage extends StatelessWidget { ), const SizedBox(height: paddingTiny), TextFormFieldWithTimer( - initialValue: controller.property.description, + initialValue: controller.data.description, onUpdate: (value) => controller.update(PropertyUpdate(description: value)), maxLines: 5, ), @@ -107,7 +107,7 @@ class PropertyBottomSheetPage extends StatelessWidget { const SizedBox(height: paddingTiny), DropdownButtonFormField( decoration: inputDecoration, - value: controller.property.fieldType, + value: controller.data.fieldType, items: PropertyFieldType.values .map((value) => DropdownMenuItem( value: value, @@ -117,7 +117,7 @@ class PropertyBottomSheetPage extends StatelessWidget { onChanged: (value) => controller.update(PropertyUpdate(fieldType: value)), ), Visibility( - visible: controller.property.isSelectType, + visible: controller.data.isSelectType, child: Column( children: [ const SizedBox(height: paddingSmall), @@ -126,8 +126,8 @@ class PropertyBottomSheetPage extends StatelessWidget { ExpansionTile( shape: const Border(), title: Text( - "${controller.property.selectData?.options.length} ${context.localization.options}"), - children: (controller.property.selectData?.options ?? []) + "${controller.data.selectData?.options.length} ${context.localization.options}"), + children: (controller.data.selectData?.options ?? []) .map((selectOption) => ListTile(title: Text(selectOption.name))) .toList(), @@ -148,11 +148,11 @@ class PropertyBottomSheetPage extends StatelessWidget { style: TextStyle(color: context.theme.hintColor), ), trailing: Switch( - value: controller.property.selectData?.isAllowingFreeText ?? false, + value: controller.data.selectData?.isAllowingFreeText ?? false, onChanged: (value) => controller.update(PropertyUpdate( selectDataUpdate: ( isAllowingFreeText: value, - options: controller.property.selectData!.options, + options: controller.data.selectData!.options, removeOptions: null, upsert: null ), @@ -189,14 +189,14 @@ class PropertyBottomSheetPage extends StatelessWidget { style: TextStyle(color: context.theme.hintColor), ), trailing: Switch( - value: controller.property.alwaysIncludeForViewSource ?? false, + value: controller.data.alwaysIncludeForViewSource ?? false, onChanged: (value) => controller.update(PropertyUpdate(alwaysIncludeForViewSource: value)), ), ), ], ), Visibility( - visible: controller.property.isCreating, + visible: controller.data.isCreating, child: Column( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.end, diff --git a/apps/tasks/lib/components/bottom_sheet_pages/task_bottom_sheet.dart b/apps/tasks/lib/components/bottom_sheet_pages/task_bottom_sheet.dart index b009a7cf..4396995d 100644 --- a/apps/tasks/lib/components/bottom_sheet_pages/task_bottom_sheet.dart +++ b/apps/tasks/lib/components/bottom_sheet_pages/task_bottom_sheet.dart @@ -13,7 +13,7 @@ import 'package:tasks/components/bottom_sheet_pages/assignee_select.dart'; import 'package:tasks/components/subtask_list.dart'; import 'package:tasks/components/visibility_select.dart'; import 'package:helpwave_service/tasks.dart'; - +import 'package:helpwave_service/util.dart'; import '../patient_selector.dart'; /// A private [Widget] similar to a [ListTile] that has an icon and then to text diff --git a/apps/tasks/lib/components/bottom_sheet_pages/wards_bottom_sheet_page.dart b/apps/tasks/lib/components/bottom_sheet_pages/wards_bottom_sheet_page.dart index d6eb281e..a412d5c7 100644 --- a/apps/tasks/lib/components/bottom_sheet_pages/wards_bottom_sheet_page.dart +++ b/apps/tasks/lib/components/bottom_sheet_pages/wards_bottom_sheet_page.dart @@ -22,7 +22,7 @@ class WardsBottomSheetPage extends StatelessWidget { header: BottomSheetHeader.navigation( context, title: LoadingFutureBuilder( - future: OrganizationService().getOrganization(id: organizationId), + future: OrganizationService().get(organizationId), thenBuilder: (context, data) => Column( mainAxisSize: MainAxisSize.min, children: [ diff --git a/apps/tasks/lib/screens/settings_screen.dart b/apps/tasks/lib/screens/settings_screen.dart index 6619fc46..132fe73f 100644 --- a/apps/tasks/lib/screens/settings_screen.dart +++ b/apps/tasks/lib/screens/settings_screen.dart @@ -210,7 +210,7 @@ class SettingsBottomSheetPage extends StatelessWidget { title: organization.longName, onTap: () { NavigationStackController.of(context) - .push(OrganizationBottomSheetPage(organizationId: organization.id)); + .push(OrganizationBottomSheetPage(organizationId: organization.id!)); }, )) .toList(), diff --git a/packages/helpwave_service/lib/src/api/offline/offline_client_store.dart b/packages/helpwave_service/lib/src/api/offline/offline_client_store.dart index 204cd60b..20609cf2 100644 --- a/packages/helpwave_service/lib/src/api/offline/offline_client_store.dart +++ b/packages/helpwave_service/lib/src/api/offline/offline_client_store.dart @@ -15,13 +15,16 @@ const String profileUrl = "https://helpwave.de/favicon.ico"; final List initialOrganizations = [ Organization( - id: "organization1", - shortName: "MK", - longName: "Musterklinikum", + id: "organization1", + shortName: "MK", + longName: "Musterklinikum", + details: OrganizationDetails( avatarURL: profileUrl, email: "test@helpwave.de", isPersonal: false, - isVerified: true), + isVerified: true, + ), + ), ]; final List initialUsers = [ User( @@ -62,7 +65,7 @@ final List initialUsers = [ ]; final List initialWards = initialOrganizations .map((organization) => range(0, 3).map((index) => - Ward(id: "${organization.id}${index + 1}", name: "Ward ${index + 1}", organizationId: organization.id))) + Ward(id: "${organization.id}${index + 1}", name: "Ward ${index + 1}", organizationId: organization.id!))) .expand((element) => element) .toList(); final List initialRooms = initialWards @@ -71,8 +74,8 @@ final List initialRooms = initialWards .expand((element) => element) .toList(); final List initialBeds = initialRooms - .map((room) => range(0, 4) - .map((index) => Bed(id: "${room.id}${index + 1}", name: "Bed ${index + 1}", roomId: room.id))) + .map((room) => + range(0, 4).map((index) => Bed(id: "${room.id}${index + 1}", name: "Bed ${index + 1}", roomId: room.id))) .expand((element) => element) .toList(); final List initialPatients = initialBeds.indexed diff --git a/packages/helpwave_service/lib/src/api/property/controllers/property_controller.dart b/packages/helpwave_service/lib/src/api/property/controllers/property_controller.dart index 3bcf6960..559e91d5 100644 --- a/packages/helpwave_service/lib/src/api/property/controllers/property_controller.dart +++ b/packages/helpwave_service/lib/src/api/property/controllers/property_controller.dart @@ -1,82 +1,11 @@ import 'package:helpwave_service/property.dart'; -import 'package:helpwave_util/loading.dart'; +import 'package:helpwave_service/util.dart'; /// The Controller for managing [Property]s in a Ward -class PropertyController extends LoadingChangeNotifier { - /// The current [Property] - Property _property = Property( - name: "Property Name", - subjectType: PropertySubjectType.patient, - fieldType: PropertyFieldType.multiSelect, - selectData: PropertySelectData( - options: [ - PropertySelectOption(name: "Option 1"), - PropertySelectOption(name: "Option 2"), - PropertySelectOption(name: "Option 3"), - ], - isAllowingFreeText: true - ) - ); +class PropertyController extends LoadController { + PropertyController({String? id, Property? property}) + : super(id: id, initialData: property, service: PropertyService()); - /// The current [Property] - Property get property => _property; - - set property(Property value) { - _property = value; - changeState(LoadingState.loaded); - } - - /// Is the current [Property] already saved on the server or are we creating? - get isCreating => _property.isCreating; - - PropertyController({String? id, Property? property}) { - assert(id == null || property == null || id == property.id); - if (property != null) { - _property = property; - } else { - _property = _property.copyWith(PropertyUpdate(id: id)); - } - load(); - } - - /// A function to load the [Property] - Future load() async { - loadProperty() async { - if (isCreating) { - return; - } - _property = await PropertyService().get(property.id!); - } - - loadHandler( - future: loadProperty(), - ); - } - - /// Change the notes of the [Property] - Future update(PropertyUpdate update) async { - updateNotes() async { - if (isCreating) { - _property = property.copyWith(update); - return; - } - await PropertyService().update(property.id!, update).then((hasSuccess) { - _property = property.copyWith(update); - }); - } - - loadHandler(future: updateNotes()); - } - - /// Creates the [Property] - Future create() async { - assert(property.isCreating, "Only use create when the Property is creating"); - createProperty() async { - await PropertyService().create(property).then((newProperty) { - property = newProperty; - }); - } - - loadHandler(future: createProperty()); - } + @override + Property defaultData() => Property.empty(); } diff --git a/packages/helpwave_service/lib/src/api/property/data_types/attached_property.dart b/packages/helpwave_service/lib/src/api/property/data_types/attached_property.dart index a2f0a123..afe9b032 100644 --- a/packages/helpwave_service/lib/src/api/property/data_types/attached_property.dart +++ b/packages/helpwave_service/lib/src/api/property/data_types/attached_property.dart @@ -1,6 +1,5 @@ -import 'package:helpwave_service/src/api/util/copy_with_interface.dart'; -import 'package:helpwave_service/src/api/util/identified_object.dart'; import 'package:helpwave_util/lists.dart'; +import 'package:helpwave_service/util.dart'; import 'index.dart'; typedef MultiSelectUpdate = ({List upsert, List remove}); diff --git a/packages/helpwave_service/lib/src/api/property/data_types/property.dart b/packages/helpwave_service/lib/src/api/property/data_types/property.dart index cd415083..5cd3d9f0 100644 --- a/packages/helpwave_service/lib/src/api/property/data_types/property.dart +++ b/packages/helpwave_service/lib/src/api/property/data_types/property.dart @@ -1,8 +1,8 @@ import 'package:helpwave_service/src/api/property/data_types/field_type.dart'; import 'package:helpwave_service/src/api/property/data_types/select_data.dart'; import 'package:helpwave_service/src/api/property/data_types/subject_type.dart'; -import 'package:helpwave_service/src/api/util/copy_with_interface.dart'; -import 'package:helpwave_service/src/api/util/identified_object.dart'; +import 'package:helpwave_service/util.dart'; + class PropertyUpdate { String? id; @@ -32,7 +32,7 @@ class PropertyUpdate { }); } -class Property extends IdentifiedObject implements CopyWithInterface { +class Property extends CRUDObject { final String name; final String description; final PropertySubjectType subjectType; @@ -57,6 +57,22 @@ class Property extends IdentifiedObject implements CopyWithInterface implements CopyWithInterface { +class PropertyService implements CRUDInterface { /// The GRPC ServiceClient which handles GRPC PropertyServiceClient service = PropertyAPIServiceClients().propertyServiceClient; @@ -94,18 +93,18 @@ class PropertyService implements CRUDInterface update(String id, PropertyUpdate update) async { + Future update(String id, PropertyUpdate? update) async { UpdatePropertyRequest request = UpdatePropertyRequest( id: id, - name: update.name, - description: update.description, - isArchived: update.isArchived, + name: update?.name, + description: update?.description, + isArchived: update?.isArchived, subjectType: - update.subjectType != null ? PropertyGRPCTypeConverter.subjectTypeToGRPC(update.subjectType!) : null, - setId: update.setId, - selectData: update.selectDataUpdate != null + update?.subjectType != null ? PropertyGRPCTypeConverter.subjectTypeToGRPC(update!.subjectType!) : null, + setId: update?.setId, + selectData: update?.selectDataUpdate != null ? UpdatePropertyRequest_SelectData( - allowFreetext: update.selectDataUpdate!.isAllowingFreeText, + allowFreetext: update!.selectDataUpdate!.isAllowingFreeText, upsertOptions: update.selectDataUpdate?.upsert?.map((option) => UpdatePropertyRequest_SelectData_SelectOption( id: option.id, diff --git a/packages/helpwave_service/lib/src/api/tasks/controllers/beds_controller.dart b/packages/helpwave_service/lib/src/api/tasks/controllers/beds_controller.dart index d3f1454c..27bf25ab 100644 --- a/packages/helpwave_service/lib/src/api/tasks/controllers/beds_controller.dart +++ b/packages/helpwave_service/lib/src/api/tasks/controllers/beds_controller.dart @@ -40,7 +40,7 @@ class BedsController extends LoadingChangeNotifier { loadHandler(future: loadBeds()); } - /// Delete the [Bed] by its index in the list + /// Delete the [Bed] by its index.dart in the list Future deleteByIndex(int index) async { assert (index < 0 || index >= beds.length); if (isCreating) { @@ -88,7 +88,7 @@ class BedsController extends LoadingChangeNotifier { if (isCreating) { assert( index != null && index >= 0 && index < beds.length, - "When creating a bed list and updating a bed, a index for the bed must be provided", + "When creating a bed list and updating a bed, a index.dart for the bed must be provided", ); beds[index!] = bed; return; diff --git a/packages/helpwave_service/lib/src/api/tasks/controllers/patient_controller.dart b/packages/helpwave_service/lib/src/api/tasks/controllers/patient_controller.dart index 590a46da..1eb7b5f7 100644 --- a/packages/helpwave_service/lib/src/api/tasks/controllers/patient_controller.dart +++ b/packages/helpwave_service/lib/src/api/tasks/controllers/patient_controller.dart @@ -1,5 +1,6 @@ import 'package:helpwave_util/loading.dart'; import 'package:helpwave_service/src/api/tasks/index.dart'; +import 'package:helpwave_service/util.dart'; /// The Controller for managing [Patient]s in a Ward class PatientController extends LoadingChangeNotifier { diff --git a/packages/helpwave_service/lib/src/api/tasks/controllers/rooms_controller.dart b/packages/helpwave_service/lib/src/api/tasks/controllers/rooms_controller.dart index 17c63ff0..c836b894 100644 --- a/packages/helpwave_service/lib/src/api/tasks/controllers/rooms_controller.dart +++ b/packages/helpwave_service/lib/src/api/tasks/controllers/rooms_controller.dart @@ -40,7 +40,7 @@ class RoomsController extends LoadingChangeNotifier { loadHandler(future: loadOp()); } - /// Delete the [Room] by its index in the list + /// Delete the [Room] by its index.dart in the list Future deleteByIndex(int index) async { assert(index < 0 || index >= rooms.length); if (isCreating) { @@ -88,7 +88,7 @@ class RoomsController extends LoadingChangeNotifier { if (isCreating) { assert( index != null && index >= 0 && index < rooms.length, - "When creating a room list and updating a room, a index for the room must be provided", + "When creating a room list and updating a room, a index.dart for the room must be provided", ); rooms[index!] = room; return; diff --git a/packages/helpwave_service/lib/src/api/tasks/controllers/subtask_list_controller.dart b/packages/helpwave_service/lib/src/api/tasks/controllers/subtask_list_controller.dart index 6038a971..7e0eb529 100644 --- a/packages/helpwave_service/lib/src/api/tasks/controllers/subtask_list_controller.dart +++ b/packages/helpwave_service/lib/src/api/tasks/controllers/subtask_list_controller.dart @@ -2,6 +2,8 @@ import 'dart:async'; import 'package:helpwave_service/src/api/tasks/index.dart'; import 'package:helpwave_util/lists.dart'; import 'package:helpwave_util/loading.dart'; +import 'package:helpwave_service/util.dart'; + /// The Controller for managing [Subtask]s in a [Task] /// @@ -40,7 +42,7 @@ class SubtasksController extends LoadingChangeNotifier { loadHandler(future: loadTask()); } - /// Delete the [Subtask] by its index int the list + /// Delete the [Subtask] by its index.dart int the list Future deleteByIndex(int index) async { if (!subtasks.isIndexValid(index)) { return; @@ -90,7 +92,7 @@ class SubtasksController extends LoadingChangeNotifier { if (isCreating) { assert( index != null && index >= 0 && index < subtasks.length, - "When creating a subtask list and updating a subtask, a index for the subtask must be provided", + "When creating a subtask list and updating a subtask, a index.dart for the subtask must be provided", ); subtasks[index!] = subtask; return; diff --git a/packages/helpwave_service/lib/src/api/tasks/controllers/task_controller.dart b/packages/helpwave_service/lib/src/api/tasks/controllers/task_controller.dart index 06d4f7b9..8e02097f 100644 --- a/packages/helpwave_service/lib/src/api/tasks/controllers/task_controller.dart +++ b/packages/helpwave_service/lib/src/api/tasks/controllers/task_controller.dart @@ -1,7 +1,7 @@ import 'package:helpwave_util/loading.dart'; import 'package:helpwave_service/src/api/tasks/index.dart'; - -import '../../../../user.dart'; +import 'package:helpwave_service/util.dart'; +import 'package:helpwave_service/user.dart'; /// The Controller for managing a [TaskWithPatient] class TaskController extends LoadingChangeNotifier { diff --git a/packages/helpwave_service/lib/src/api/tasks/controllers/task_template_controller.dart b/packages/helpwave_service/lib/src/api/tasks/controllers/task_template_controller.dart index 69696a3a..ddb14982 100644 --- a/packages/helpwave_service/lib/src/api/tasks/controllers/task_template_controller.dart +++ b/packages/helpwave_service/lib/src/api/tasks/controllers/task_template_controller.dart @@ -105,7 +105,7 @@ class TaskTemplateController extends LoadingChangeNotifier { loadHandler(future: createOp()); } - /// Updates the [TaskTemplateSubtask] by its index in the [List] + /// Updates the [TaskTemplateSubtask] by its index.dart in the [List] /// /// **Only use** this when the [TaskTemplate] was **loaded or an initial [TaskTemplate]** is provided Future updateSubtaskByIndex({required int index, String? name}) async { @@ -144,7 +144,7 @@ class TaskTemplateController extends LoadingChangeNotifier { loadHandler(future: updateOp()); } - /// Delete the [TaskTemplate] by the index in the [List] + /// Delete the [TaskTemplate] by the index.dart in the [List] Future deleteSubtaskByIndex({required int index}) async { assert(_taskTemplate != null && _taskTemplate!.subtasks.isIndexValid(index)); deleteOp() async { diff --git a/packages/helpwave_service/lib/src/api/tasks/controllers/ward_controller.dart b/packages/helpwave_service/lib/src/api/tasks/controllers/ward_controller.dart index a716ea6c..306fb7f6 100644 --- a/packages/helpwave_service/lib/src/api/tasks/controllers/ward_controller.dart +++ b/packages/helpwave_service/lib/src/api/tasks/controllers/ward_controller.dart @@ -1,6 +1,7 @@ import 'dart:async'; import 'package:helpwave_service/src/api/tasks/index.dart'; import 'package:helpwave_util/loading.dart'; +import 'package:helpwave_service/util.dart'; /// The Controller for managing a [WardMinimal] /// diff --git a/packages/helpwave_service/lib/src/api/tasks/data_types/patient.dart b/packages/helpwave_service/lib/src/api/tasks/data_types/patient.dart index 3bc6eab2..45ce4409 100644 --- a/packages/helpwave_service/lib/src/api/tasks/data_types/patient.dart +++ b/packages/helpwave_service/lib/src/api/tasks/data_types/patient.dart @@ -1,5 +1,5 @@ import 'package:helpwave_service/src/api/tasks/index.dart'; -import 'package:helpwave_service/src/api/util/identified_object.dart'; +import 'package:helpwave_service/util.dart'; enum PatientAssignmentStatus { active, unassigned, discharged, all } diff --git a/packages/helpwave_service/lib/src/api/tasks/data_types/subtask.dart b/packages/helpwave_service/lib/src/api/tasks/data_types/subtask.dart index 0c706783..e7dbcdeb 100644 --- a/packages/helpwave_service/lib/src/api/tasks/data_types/subtask.dart +++ b/packages/helpwave_service/lib/src/api/tasks/data_types/subtask.dart @@ -1,4 +1,4 @@ -import 'package:helpwave_service/src/api/util/identified_object.dart'; +import 'package:helpwave_service/util.dart'; /// Data class for a [Subtask] class Subtask extends IdentifiedObject { diff --git a/packages/helpwave_service/lib/src/api/tasks/data_types/task.dart b/packages/helpwave_service/lib/src/api/tasks/data_types/task.dart index 04f6a63e..44498595 100644 --- a/packages/helpwave_service/lib/src/api/tasks/data_types/task.dart +++ b/packages/helpwave_service/lib/src/api/tasks/data_types/task.dart @@ -1,6 +1,6 @@ import 'package:helpwave_service/src/api/tasks/data_types/patient.dart'; import 'package:helpwave_service/src/api/tasks/data_types/subtask.dart'; -import 'package:helpwave_service/src/api/util/identified_object.dart'; +import 'package:helpwave_service/util.dart'; enum TaskStatus { unspecified, diff --git a/packages/helpwave_service/lib/src/api/tasks/data_types/ward.dart b/packages/helpwave_service/lib/src/api/tasks/data_types/ward.dart index 95bbf11c..77e421f5 100644 --- a/packages/helpwave_service/lib/src/api/tasks/data_types/ward.dart +++ b/packages/helpwave_service/lib/src/api/tasks/data_types/ward.dart @@ -1,4 +1,4 @@ -import 'package:helpwave_service/src/api/util/identified_object.dart'; +import 'package:helpwave_service/util.dart'; /// data class for [Ward] class WardMinimal extends IdentifiedObject { diff --git a/packages/helpwave_service/lib/src/api/tasks/services/task_svc.dart b/packages/helpwave_service/lib/src/api/tasks/services/task_svc.dart index 5f7665ea..97f48577 100644 --- a/packages/helpwave_service/lib/src/api/tasks/services/task_svc.dart +++ b/packages/helpwave_service/lib/src/api/tasks/services/task_svc.dart @@ -3,6 +3,8 @@ import 'package:helpwave_proto_dart/google/protobuf/timestamp.pb.dart'; import 'package:helpwave_service/src/api/tasks/index.dart'; import 'package:helpwave_proto_dart/services/tasks_svc/v1/task_svc.pbgrpc.dart'; import '../util/task_status_mapping.dart'; +import 'package:helpwave_service/util.dart'; + /// The GRPC Service for [Task]s /// diff --git a/packages/helpwave_service/lib/src/api/user/controllers/organization_controller.dart b/packages/helpwave_service/lib/src/api/user/controllers/organization_controller.dart index 0c1816fa..8a565d89 100644 --- a/packages/helpwave_service/lib/src/api/user/controllers/organization_controller.dart +++ b/packages/helpwave_service/lib/src/api/user/controllers/organization_controller.dart @@ -1,85 +1,40 @@ import 'package:helpwave_service/auth.dart'; -import 'package:helpwave_util/loading.dart'; +import 'package:helpwave_service/src/util/index.dart'; import '../../../../user.dart'; /// The Controller for managing a [Organization] -class OrganizationController extends LoadingChangeNotifier { - /// The current [Organization] - Organization _organization; - - /// The current [Organization] - Organization get organization => _organization; - - set organization(Organization value) { - _organization = value; - changeState(LoadingState.loaded); - } - - OrganizationController(this._organization) { - load(); - } - - /// A function to load the [Organization] - Future load() async { - loadOrganization() async { - organization = await OrganizationService().getOrganization(id: organization.id); - } - - loadHandler( - future: loadOrganization(), - ); - } - - Future update({ - String? shortName, - String? longName, - String? email, - bool? isPersonal, - String? avatarUrl, - }) async { - changeOrganization() async { - await OrganizationService().update( - id: organization.id, - longName: longName, - shortName: shortName, - isPersonal: isPersonal, - email: email, - avatarUrl: avatarUrl, - ); - organization = organization.copyWith( - longName: longName, - shortName: shortName, - isPersonal: isPersonal, - email: email, - avatarURL: avatarUrl, - ); - - bool affectsCurrentWardService = shortName != null || longName != null; +class OrganizationController + extends LoadController { + OrganizationController({String? id, Organization? initialData}) + : super(id: id, initialData: initialData, service: OrganizationService()); + + @override + Future update(OrganizationUpdate? update) async { + updateOp() async { + if (isCreating) { + changeData(data.copyWith(update)); + return; + } + await service.update(data.id!, update).then((value) { + changeData(data.copyWith(update)); + }); + bool affectsCurrentWardService = update?.shortName != null || update?.longName != null; - if (affectsCurrentWardService && CurrentWardService().currentWard?.organizationId == organization.id) { + if (affectsCurrentWardService && CurrentWardService().currentWard?.organizationId == data.id) { CurrentWardService().currentWard = CurrentWardService().currentWard!.copyWith( - organization: CurrentWardService().currentWard!.organization.copyWith( - shortName: shortName, - longName: longName, - ), + organization: CurrentWardService().currentWard!.organization.copyWith(OrganizationUpdate( + shortName: update?.shortName, + longName: update?.longName, + )), ); } } loadHandler( - future: changeOrganization(), + future: updateOp(), ); } - Future delete() async { - deleteOrganization() async { - await OrganizationService().delete(organization.id).then((_) { - // TODO do something here - }); - } - - loadHandler(future: deleteOrganization()); - } - -// TODO create method and isCreating handling + @override + Organization defaultData() => Organization.empty(); } diff --git a/packages/helpwave_service/lib/src/api/user/controllers/user_controller.dart b/packages/helpwave_service/lib/src/api/user/controllers/user_controller.dart index 6d975aa2..790fd5f1 100644 --- a/packages/helpwave_service/lib/src/api/user/controllers/user_controller.dart +++ b/packages/helpwave_service/lib/src/api/user/controllers/user_controller.dart @@ -1,6 +1,7 @@ import 'package:flutter/material.dart'; import 'package:helpwave_util/loading.dart'; import '../index.dart'; +import 'package:helpwave_service/util.dart'; /// The Controller for managing a [User] class UserController extends ChangeNotifier { diff --git a/packages/helpwave_service/lib/src/api/user/data_types/invitation.dart b/packages/helpwave_service/lib/src/api/user/data_types/invitation.dart index 6f690889..e6387a69 100644 --- a/packages/helpwave_service/lib/src/api/user/data_types/invitation.dart +++ b/packages/helpwave_service/lib/src/api/user/data_types/invitation.dart @@ -1,6 +1,5 @@ -import 'package:helpwave_service/src/api/util/identified_object.dart'; - import '../../../../user.dart'; +import 'package:helpwave_service/util.dart'; enum InvitationState { unspecified, diff --git a/packages/helpwave_service/lib/src/api/user/data_types/organization.dart b/packages/helpwave_service/lib/src/api/user/data_types/organization.dart index 8f977fbd..aa7d9bb3 100644 --- a/packages/helpwave_service/lib/src/api/user/data_types/organization.dart +++ b/packages/helpwave_service/lib/src/api/user/data_types/organization.dart @@ -1,75 +1,86 @@ -import 'package:helpwave_service/src/api/util/identified_object.dart'; +import 'package:helpwave_service/src/config.dart'; +import 'package:helpwave_service/util.dart'; -class OrganizationMinimal extends IdentifiedObject { - String shortName; - String longName; - - OrganizationMinimal({ - super.id, - required this.shortName, - required this.longName, - }); - - OrganizationMinimal copyWith({ - String? id, - String? shortName, - String? longName, - }) { - return OrganizationMinimal( - id: id ?? this.id, - shortName: shortName ?? this.shortName, - longName: longName ?? this.longName, - ); - } -} - -/// data class for [Organization] -class Organization extends OrganizationMinimal { +class OrganizationDetails implements CopyWithInterface { String avatarURL; String email; bool isPersonal; bool isVerified; - Organization({ - super.id, - required super.shortName, - required super.longName, - required this.avatarURL, + OrganizationDetails({ required this.email, + required this.avatarURL, required this.isPersonal, required this.isVerified, }); - factory Organization.empty(String? id) => Organization( - id: id ?? "", - shortName: "", - longName: "", - avatarURL: "", - email: "", + factory OrganizationDetails.empty() => OrganizationDetails( + email: "test@helpwave.de", + avatarURL: avatarFallBackURL, isPersonal: false, isVerified: false, ); @override - Organization copyWith({ - String? id, - String? shortName, - String? longName, - String? avatarURL, - String? email, - bool? isPersonal, - bool? isVerified, - }) { - return Organization( - id: id ?? this.id, - shortName: shortName ?? this.shortName, - longName: longName ?? this.longName, - avatarURL: avatarURL ?? this.avatarURL, - email: email ?? this.email, - isPersonal: isPersonal ?? this.isPersonal, - isVerified: isVerified ?? this.isVerified, + OrganizationDetails copyWith(OrganizationUpdate? update) { + return OrganizationDetails( + email: update?.email ?? email, + avatarURL: update?.avatarURL ?? avatarURL, + isPersonal: update?.isPersonal ?? isPersonal, + isVerified: update?.isVerified ?? isVerified, ); } +} + +class OrganizationUpdate { + String? id; + String? shortName; + String? longName; + bool? isPersonal; + bool? isVerified; + String? avatarURL; + String? email; + + OrganizationUpdate({ + this.id, + this.shortName, + this.longName, + this.isPersonal, + this.isVerified, + this.avatarURL, + this.email, + }); +} + +/// data class for [Organization] +class Organization extends IdentifiedObject + implements CRUDObject { + String shortName; + String longName; + OrganizationDetails? details; + + bool get hasDetails => details != null; + + Organization({super.id, required this.shortName, required this.longName, this.details}); + + factory Organization.empty({String? id, bool hasEmptyDetails = true}) => Organization( + id: id, + shortName: "ORG", + longName: "Organization Name", + details: hasEmptyDetails ? OrganizationDetails.empty() : null, + ); + + @override + Organization create(String id) => copyWith(OrganizationUpdate(id: id)); + + @override + Organization copyWith(OrganizationUpdate? update) { + return Organization( + id: update?.id ?? id, + shortName: update?.shortName ?? shortName, + longName: update?.longName ?? longName, + details: details?.copyWith(update)); + } String get combinedName => "$longName ($shortName)"; diff --git a/packages/helpwave_service/lib/src/api/user/data_types/user.dart b/packages/helpwave_service/lib/src/api/user/data_types/user.dart index e27b7cdf..85a6c635 100644 --- a/packages/helpwave_service/lib/src/api/user/data_types/user.dart +++ b/packages/helpwave_service/lib/src/api/user/data_types/user.dart @@ -1,4 +1,5 @@ -import 'package:helpwave_service/src/api/util/identified_object.dart'; +import 'package:helpwave_service/src/config.dart'; +import 'package:helpwave_service/util.dart'; /// data class for [User] class User extends IdentifiedObject { @@ -20,7 +21,7 @@ class User extends IdentifiedObject { name: "User", nickName: "", email: "", - profileUrl: Uri.parse("https://cdn.helpwave.de/boringavatar.svg"), + profileUrl: Uri.parse(avatarFallBackURL), ); User copyWith({String? id, String? name, String? nickName, Uri? profileUrl, String? email}) => User( diff --git a/packages/helpwave_service/lib/src/api/user/offline_clients/organization_offline_client.dart b/packages/helpwave_service/lib/src/api/user/offline_clients/organization_offline_client.dart index e3b39978..b9ac1a02 100644 --- a/packages/helpwave_service/lib/src/api/user/offline_clients/organization_offline_client.dart +++ b/packages/helpwave_service/lib/src/api/user/offline_clients/organization_offline_client.dart @@ -6,24 +6,6 @@ import 'package:helpwave_service/src/api/user/util/type_converter.dart'; import 'package:helpwave_service/user.dart'; import '../data_types/invitation.dart' as invite_types; -class OrganizationUpdate { - String id; - String? shortName; - String? longName; - String? email; - bool? isPersonal; - String? avatarURL; - - OrganizationUpdate({ - required this.id, - this.shortName, - this.longName, - this.email, - this.isPersonal, - this.avatarURL, - }); -} - class OrganizationOfflineService { List organizations = []; @@ -48,12 +30,13 @@ class OrganizationOfflineService { organizations = organizations.map((org) { if (org.id == organizationUpdate.id) { found = true; - return org.copyWith( - shortName: organizationUpdate.shortName, - longName: organizationUpdate.longName, - avatarURL: organizationUpdate.avatarURL, - email: organizationUpdate.email, - isPersonal: organizationUpdate.isPersonal); + return org.copyWith(OrganizationUpdate( + shortName: organizationUpdate.shortName, + longName: organizationUpdate.longName, + avatarURL: organizationUpdate.avatarURL, + email: organizationUpdate.email, + isPersonal: organizationUpdate.isPersonal, + )); } return org; }).toList(); @@ -80,15 +63,17 @@ class OrganizationOfflineClient extends OrganizationServiceClient { id: DateTime.now().millisecondsSinceEpoch.toString(), shortName: request.shortName, longName: request.longName, - avatarURL: 'https://helpwave.de/favicon.ico', - email: request.contactEmail, - isPersonal: request.isPersonal, - isVerified: true, + details: OrganizationDetails( + avatarURL: 'https://helpwave.de/favicon.ico', + email: request.contactEmail, + isPersonal: request.isPersonal, + isVerified: true, + ), ); OfflineClientStore().organizationStore.create(newOrganization); - return MockResponseFuture.value(CreateOrganizationResponse()..id = newOrganization.id); + return MockResponseFuture.value(CreateOrganizationResponse(id: newOrganization.id)); } @override @@ -102,14 +87,15 @@ class OrganizationOfflineClient extends OrganizationServiceClient { final members = OfflineClientStore().userStore.findUsers().map((user) => GetOrganizationMember()..userId = user.id).toList(); - return MockResponseFuture.value(GetOrganizationResponse() - ..id = organization.id - ..shortName = organization.shortName - ..longName = organization.longName - ..avatarUrl = organization.avatarURL - ..contactEmail = organization.email - ..isPersonal = organization.isPersonal - ..members.addAll(members)); + return MockResponseFuture.value(GetOrganizationResponse( + id: organization.id, + shortName: organization.shortName, + longName: organization.longName, + avatarUrl: organization.details?.avatarURL, + contactEmail: organization.details?.email, + isPersonal: organization.details?.isPersonal, + members: members, + )); } @override @@ -122,14 +108,15 @@ class OrganizationOfflineClient extends OrganizationServiceClient { .map((user) => GetOrganizationsByUserResponse_Organization_Member()..userId = user.id) .toList(); - return GetOrganizationsByUserResponse_Organization() - ..id = org.id - ..shortName = org.shortName - ..longName = org.longName - ..contactEmail = org.email - ..avatarUrl = org.avatarURL - ..members.addAll(members) - ..isPersonal = org.isPersonal; + return GetOrganizationsByUserResponse_Organization( + id: org.id, + shortName: org.shortName, + longName: org.longName, + contactEmail: org.details?.email, + avatarUrl: org.details?.avatarURL, + members: members, + isPersonal: org.details?.isPersonal, + ); }).toList(); return MockResponseFuture.value(GetOrganizationsByUserResponse()..organizations.addAll(organizations)); @@ -145,14 +132,15 @@ class OrganizationOfflineClient extends OrganizationServiceClient { .map((user) => GetOrganizationsForUserResponse_Organization_Member()..userId = user.id) .toList(); - return GetOrganizationsForUserResponse_Organization() - ..id = org.id - ..shortName = org.shortName - ..longName = org.longName - ..contactEmail = org.email - ..avatarUrl = org.avatarURL - ..members.addAll(members) - ..isPersonal = org.isPersonal; + return GetOrganizationsForUserResponse_Organization( + id: org.id, + shortName: org.shortName, + longName: org.longName, + contactEmail: org.details?.email, + avatarUrl: org.details?.avatarURL, + members: members, + isPersonal: org.details?.isPersonal, + ); }).toList(); return MockResponseFuture.value(GetOrganizationsForUserResponse()..organizations.addAll(organizations)); @@ -259,7 +247,7 @@ class OrganizationOfflineClient extends OrganizationServiceClient { invite.organization = GetInvitationsByUserResponse_Invitation_Organization( id: organization.id, longName: organization.longName, - avatarUrl: organization.avatarURL, + avatarUrl: organization.details?.avatarURL, ); return invite; })); @@ -271,7 +259,8 @@ class OrganizationOfflineClient extends OrganizationServiceClient { ResponseFuture inviteMember(InviteMemberRequest request, {CallOptions? options}) { assert(OfflineClientStore().userStore.users.indexWhere((element) => element.email == request.email) != -1); assert(OfflineClientStore().invitationStore.invitations.indexWhere( - (element) => element.organizationId == request.organizationId && element.email == request.email) == -1); + (element) => element.organizationId == request.organizationId && element.email == request.email) == + -1); final invite = invite_types.Invitation( id: DateTime.now().toString(), @@ -285,12 +274,12 @@ class OrganizationOfflineClient extends OrganizationServiceClient { @override ResponseFuture revokeInvitation(RevokeInvitationRequest request, {CallOptions? options}) { - final invite = OfflineClientStore().invitationStore.find(request.invitationId); + final invite = OfflineClientStore().invitationStore.find(request.invitationId); - if(invite == null){ + if (invite == null) { throw "Invitation with id ${request.invitationId} not found"; } - if(invite_types.InvitationState.pending != invite.state){ + if (invite_types.InvitationState.pending != invite.state) { throw "Only pending Invitations can be revoked"; } OfflineClientStore().invitationStore.changeState(request.invitationId, invite_types.InvitationState.accepted); @@ -300,12 +289,12 @@ class OrganizationOfflineClient extends OrganizationServiceClient { @override ResponseFuture acceptInvitation(AcceptInvitationRequest request, {CallOptions? options}) { - final invite = OfflineClientStore().invitationStore.find(request.invitationId); + final invite = OfflineClientStore().invitationStore.find(request.invitationId); - if(invite == null){ + if (invite == null) { throw "Invitation with id ${request.invitationId} not found"; } - if(invite_types.InvitationState.pending != invite.state){ + if (invite_types.InvitationState.pending != invite.state) { throw "Only pending Invitations can be accepted"; } OfflineClientStore().invitationStore.changeState(request.invitationId, invite_types.InvitationState.accepted); @@ -316,12 +305,12 @@ class OrganizationOfflineClient extends OrganizationServiceClient { @override ResponseFuture declineInvitation(DeclineInvitationRequest request, {CallOptions? options}) { - final invite = OfflineClientStore().invitationStore.find(request.invitationId); + final invite = OfflineClientStore().invitationStore.find(request.invitationId); - if(invite == null){ + if (invite == null) { throw "Invitation with id ${request.invitationId} not found"; } - if(invite_types.InvitationState.pending != invite.state){ + if (invite_types.InvitationState.pending != invite.state) { throw "Only pending Invitations can be de"; } OfflineClientStore().invitationStore.changeState(request.invitationId, invite_types.InvitationState.accepted); diff --git a/packages/helpwave_service/lib/src/api/user/offline_clients/user_offline_client.dart b/packages/helpwave_service/lib/src/api/user/offline_clients/user_offline_client.dart index d39c57c6..71bedbd3 100644 --- a/packages/helpwave_service/lib/src/api/user/offline_clients/user_offline_client.dart +++ b/packages/helpwave_service/lib/src/api/user/offline_clients/user_offline_client.dart @@ -75,7 +75,7 @@ class UserOfflineClient extends UserServiceClient { final organizations = OfflineClientStore() .organizationStore .findOrganizations() - .map((org) => ReadSelfOrganization()..id = org.id) + .map((org) => ReadSelfOrganization(id: org.id)) .toList(); final response = ReadSelfResponse() diff --git a/packages/helpwave_service/lib/src/api/user/services/organization_svc.dart b/packages/helpwave_service/lib/src/api/user/services/organization_svc.dart index 7e0956eb..5bcc2b4b 100644 --- a/packages/helpwave_service/lib/src/api/user/services/organization_svc.dart +++ b/packages/helpwave_service/lib/src/api/user/services/organization_svc.dart @@ -4,18 +4,20 @@ import 'package:helpwave_service/auth.dart'; import 'package:helpwave_service/src/api/user/data_types/invitation.dart' as invitation; import 'package:helpwave_service/src/api/user/user_api_service_clients.dart'; import 'package:helpwave_service/src/api/user/util/type_converter.dart'; +import 'package:helpwave_service/src/util/crud_service_interface.dart'; import '../data_types/index.dart'; /// The GRPC Service for [Organization]s /// /// Provides queries and requests that load or alter [Organization] objects on the server /// The server is defined in the underlying [UserAPIServiceClients] -class OrganizationService { +class OrganizationService implements CRUDInterface { /// The GRPC ServiceClient which handles GRPC OrganizationServiceClient organizationService = UserAPIServiceClients().organizationServiceClient; /// Load a Organization by its identifier - Future getOrganization({required String id}) async { + @override + Future get(String id) async { GetOrganizationRequest request = GetOrganizationRequest(id: id); GetOrganizationResponse response = await organizationService.getOrganization( request, @@ -23,13 +25,16 @@ class OrganizationService { ); Organization organization = Organization( - id: response.id, - longName: response.longName, - shortName: response.shortName, + id: response.id, + longName: response.longName, + shortName: response.shortName, + details: OrganizationDetails( avatarURL: response.avatarUrl, email: response.contactEmail, isVerified: true, - isPersonal: response.isPersonal); + isPersonal: response.isPersonal, + ), + ); return organization; } @@ -46,33 +51,39 @@ class OrganizationService { ); List organizations = response.organizations - .map((organization) => Organization( + .map( + (organization) => Organization( id: organization.id, longName: organization.longName, shortName: organization.shortName, - avatarURL: organization.avatarUrl, - email: organization.contactEmail, - isVerified: true, - isPersonal: organization.isPersonal)) + details: OrganizationDetails( + avatarURL: organization.avatarUrl, + email: organization.contactEmail, + isVerified: true, + isPersonal: organization.isPersonal, + ), + ), + ) .toList(); return organizations; } - Future update({ - required String id, - String? shortName, - String? longName, - String? email, - bool? isPersonal, - String? avatarUrl, - }) async { + @override + @Deprecated("The Creation of Organizations was disallowed") + Future create(Organization value) { + // TODO: implement create + throw UnimplementedError("The Backend does not allow the creation of Organizations through the API"); + } + + @override + Future update(String id, OrganizationUpdate? update) async { UpdateOrganizationRequest request = UpdateOrganizationRequest( id: id, - longName: longName, - shortName: shortName, - isPersonal: isPersonal, - contactEmail: email, - avatarUrl: avatarUrl, + longName: update?.longName, + shortName: update?.shortName, + isPersonal: update?.isPersonal, + contactEmail: update?.email, + avatarUrl: update?.avatarURL, ); await organizationService.updateOrganization( request, @@ -80,9 +91,11 @@ class OrganizationService { metadata: UserAPIServiceClients().getMetaData(organizationId: id), ), ); + return true; } - Future delete(String id) async { + @override + Future delete(String id) async { DeleteOrganizationRequest request = DeleteOrganizationRequest(id: id); await organizationService.deleteOrganization( request, @@ -90,6 +103,7 @@ class OrganizationService { metadata: UserAPIServiceClients().getMetaData(organizationId: id), ), ); + return true; } /// Loads the members of an [Organization] as [User]s @@ -168,12 +182,14 @@ class OrganizationService { ), ); - return response.invitations.map((invite) => invitation.Invitation( - id: invite.id, - organizationId: organizationId, - email: invite.email, - state: UserGRPCTypeConverter.invitationStateFromGRPC(invite.state), - )).toList(); + return response.invitations + .map((invite) => invitation.Invitation( + id: invite.id, + organizationId: organizationId, + email: invite.email, + state: UserGRPCTypeConverter.invitationStateFromGRPC(invite.state), + )) + .toList(); } Future> getInvitationsByUser({ @@ -191,12 +207,14 @@ class OrganizationService { ), ); - return response.invitations.map((invite) => invitation.Invitation( - id: invite.id, - organizationId: organizationId, - email: invite.email, - state: UserGRPCTypeConverter.invitationStateFromGRPC(invite.state), - )).toList(); + return response.invitations + .map((invite) => invitation.Invitation( + id: invite.id, + organizationId: organizationId, + email: invite.email, + state: UserGRPCTypeConverter.invitationStateFromGRPC(invite.state), + )) + .toList(); } Future acceptInvitation({required String invitationId}) async { diff --git a/packages/helpwave_service/lib/src/api/util/copy_with_interface.dart b/packages/helpwave_service/lib/src/api/util/copy_with_interface.dart deleted file mode 100644 index 9514e667..00000000 --- a/packages/helpwave_service/lib/src/api/util/copy_with_interface.dart +++ /dev/null @@ -1,3 +0,0 @@ -abstract class CopyWithInterface { - T copyWith(U? update); -} diff --git a/packages/helpwave_service/lib/src/auth/current_ward_svc.dart b/packages/helpwave_service/lib/src/auth/current_ward_svc.dart index f6cb447f..245b4ab5 100644 --- a/packages/helpwave_service/lib/src/auth/current_ward_svc.dart +++ b/packages/helpwave_service/lib/src/auth/current_ward_svc.dart @@ -10,17 +10,17 @@ class CurrentWardInformation { final WardMinimal ward; /// The identifier of the organization - final OrganizationMinimal organization; + final Organization organization; - String get wardId => ward.id; + String get wardId => ward.id; // Id can be null String get wardName => ward.name; - String get organizationId => organization.id; + String get organizationId => organization.id ?? ""; // Id can be null String get organizationName => "${organization.longName} (${organization.shortName})"; - bool get isEmpty => wardId == "" || organizationId == ""; + bool get isEmpty => wardId == "" || organizationId == ""; // TODO the ids are null and would throw an error bool get hasFullInformation => ward.name != "" && organization.longName != ""; @@ -28,7 +28,7 @@ class CurrentWardInformation { CurrentWardInformation copyWith({ WardMinimal? ward, - OrganizationMinimal? organization, + Organization? organization, }) { return CurrentWardInformation( ward ?? this.ward, @@ -73,7 +73,7 @@ class _CurrentWardPreferences { if (wardId != null && organizationId != null) { return CurrentWardInformation( WardMinimal(id: wardId, name: ""), - OrganizationMinimal(id: organizationId, longName: "", shortName: ""), + Organization.empty(id: organizationId), ); } return null; @@ -167,7 +167,7 @@ class CurrentWardService extends Listenable { if (!isInitialized) { return; } - Organization organization = await OrganizationService().getOrganization(id: currentWard!.organizationId); + Organization organization = await OrganizationService().get(currentWard!.organizationId); WardMinimal ward = await WardService().get(id: currentWard!.wardId); _currentWard = CurrentWardInformation(ward, organization); notifyListeners(); diff --git a/packages/helpwave_service/lib/src/config.dart b/packages/helpwave_service/lib/src/config.dart new file mode 100644 index 00000000..a12f4107 --- /dev/null +++ b/packages/helpwave_service/lib/src/config.dart @@ -0,0 +1 @@ +const avatarFallBackURL = "https://cdn.helpwave.de/boringavatar.svg"; diff --git a/packages/helpwave_service/lib/src/util/copy_with_interface.dart b/packages/helpwave_service/lib/src/util/copy_with_interface.dart new file mode 100644 index 00000000..e9ddb88e --- /dev/null +++ b/packages/helpwave_service/lib/src/util/copy_with_interface.dart @@ -0,0 +1,3 @@ +abstract class CopyWithInterface { + DataType copyWith(UpdateType? update); +} diff --git a/packages/helpwave_service/lib/src/util/crud_object_interface.dart b/packages/helpwave_service/lib/src/util/crud_object_interface.dart new file mode 100644 index 00000000..1b321d53 --- /dev/null +++ b/packages/helpwave_service/lib/src/util/crud_object_interface.dart @@ -0,0 +1,9 @@ +import 'copy_with_interface.dart'; +import 'identified_object.dart'; + +abstract class CRUDObject extends IdentifiedObject + implements CopyWithInterface { + CRUDObject({super.id}); + + DataType create(IdentifierType id); +} diff --git a/packages/helpwave_service/lib/src/api/util/crud_interface.dart b/packages/helpwave_service/lib/src/util/crud_service_interface.dart similarity index 53% rename from packages/helpwave_service/lib/src/api/util/crud_interface.dart rename to packages/helpwave_service/lib/src/util/crud_service_interface.dart index 78ec707d..5fe952ee 100644 --- a/packages/helpwave_service/lib/src/api/util/crud_interface.dart +++ b/packages/helpwave_service/lib/src/util/crud_service_interface.dart @@ -1,13 +1,13 @@ -abstract class CRUDInterface { +abstract class CRUDInterface { /// A [Function] for loading the object - Future get(String id); + Future get(IdentifierType id); /// A [Function] for creating the object Future create(Create value); /// A [Function] for updating the object with an update object - Future update(String id, Update update); + Future update(IdentifierType id, Update? update); /// A [Function] for deleting the object - Future delete(String id); + Future delete(IdentifierType id); } diff --git a/packages/helpwave_service/lib/src/api/util/identified_object.dart b/packages/helpwave_service/lib/src/util/identified_object.dart similarity index 73% rename from packages/helpwave_service/lib/src/api/util/identified_object.dart rename to packages/helpwave_service/lib/src/util/identified_object.dart index 5752a1ff..bb8e6e53 100644 --- a/packages/helpwave_service/lib/src/api/util/identified_object.dart +++ b/packages/helpwave_service/lib/src/util/identified_object.dart @@ -2,8 +2,6 @@ class IdentifiedObject { final T? id; - bool get isCreating => id == null; - IdentifiedObject({this.id}); bool isReferencingSame(IdentifiedObject other) { @@ -15,3 +13,8 @@ class IdentifiedObject { return "$runtimeType{id: $id}"; } } + +/// A Extension to check the creation status of an [IdentifiedObject] +extension CreationExtension on IdentifiedObject { + bool get isCreating => id == null; +} diff --git a/packages/helpwave_service/lib/src/util/index.dart b/packages/helpwave_service/lib/src/util/index.dart new file mode 100644 index 00000000..10ed2f74 --- /dev/null +++ b/packages/helpwave_service/lib/src/util/index.dart @@ -0,0 +1,5 @@ +export 'identified_object.dart'; +export 'crud_object_interface.dart'; +export 'crud_service_interface.dart'; +export 'load_controller.dart'; +export 'copy_with_interface.dart'; diff --git a/packages/helpwave_service/lib/src/util/load_controller.dart b/packages/helpwave_service/lib/src/util/load_controller.dart new file mode 100644 index 00000000..f39a8273 --- /dev/null +++ b/packages/helpwave_service/lib/src/util/load_controller.dart @@ -0,0 +1,112 @@ +import 'package:helpwave_service/src/util/crud_service_interface.dart'; +import 'package:helpwave_service/src/util/identified_object.dart'; +import 'package:helpwave_util/loading.dart'; +import 'crud_object_interface.dart'; + +abstract class LoadController< + IdentifierType, + DataType extends CRUDObject, + CreateType, + UpdateType, + ServiceType extends CRUDInterface> extends LoadingChangeNotifier { + late final ServiceType service; + late DataType _data; // Default value + + DataType get data => _data; + + set data(DataType value) => changeData(value); + + bool get isCreating => data.isCreating; + + /// A handler for changing the data that allows not notifying observers + /// + /// This is useful in case the change is handled inside the [loadHandler] as it automatically + void changeData(DataType value, {isNotifying = true}) { + _data = value; + if (isNotifying) { + changeState(LoadingState.loaded); + } + } + + LoadController({IdentifierType? id, DataType? initialData, required this.service}) { + assert(initialData == null || id == initialData.id, "The id and initial data id must be the same or not provided"); + if (initialData != null) { + _data = data; + } else if (id != null) { + _data = defaultData().create(id); + } + load(); + } + + /// The default value for the DataType + DataType defaultData(); // This cannot be abstracted as we cannot know a constructor for the DataType + + /// Creates the [DataType] + Future create() async { + assert(data.isCreating, "Only call $runtimeType.create when managing a new object."); + + createOp() async { + await service.create(data).then((value) { + _data = value; + }); + } + + loadHandler(future: createOp()); + } + + /// A function to load the [DataType] + Future load() async { + loadOp() async { + if (isCreating) { + return; + } + _data = await service.get(data.id as IdentifierType); + } + + loadHandler( + future: loadOp(), + ); + } + + /// Updates the [DataType] + Future update(UpdateType? update) async { + updateOp() async { + if (isCreating) { + _data = data.copyWith(update); + return; + } + await service.update(data.id as IdentifierType, update).then((value) { + if(value){ + _data = data.copyWith(update); + } + }); + } + + loadHandler(future: updateOp()); + } + + /// Deletes the [DataType] and makes this controller unusable until restore is called + Future delete() async { + assert(!data.isCreating, "Only call $runtimeType.delete when managing an existing object."); + + deleteOp() async { + // Due to the genericity a cast is better + await service.delete(data.id as IdentifierType).then((_) {}); + } + + loadHandler(future: deleteOp()); + } + +/* + /// Restores the [DataType] after deletion + Future restore() async { + assert(!data.isCreating, "Only call $runtimeType.restore when managing an existing object."); + + restoreOp() async { + await Service().restore(data.id).then((bool) {}); + } + + loadHandler(future: restoreOp()); + } + */ +} diff --git a/packages/helpwave_service/lib/util.dart b/packages/helpwave_service/lib/util.dart new file mode 100644 index 00000000..78005b82 --- /dev/null +++ b/packages/helpwave_service/lib/util.dart @@ -0,0 +1 @@ +export 'src/util/index.dart'; From a2650c929d731aba063702c8a9027e608aadf15f Mon Sep 17 00:00:00 2001 From: Felix Thape Date: Tue, 26 Nov 2024 22:49:41 +0100 Subject: [PATCH 34/36] fix: fix deprecation warnings --- .../patient_bottom_sheet.dart | 119 +++++----- .../bottom_sheet_pages/task_bottom_sheet.dart | 2 +- .../task_template_bottom_sheet.dart | 4 +- .../lib/components/navigation_drawer.dart | 39 ++-- .../lib/components/task_expansion_tile.dart | 3 + apps/tasks/lib/components/user_header.dart | 4 +- apps/tasks/lib/screens/landing_screen.dart | 4 +- .../patient_screen.dart | 2 +- apps/tasks/pubspec.lock | 4 +- .../controllers/property_controller.dart | 4 +- .../services/property_value_service.dart | 1 - .../services/property_views_service.dart | 1 - .../controllers/organization_controller.dart | 4 +- packages/helpwave_service/pubspec.yaml | 2 +- .../helpwave_theme/lib/src/constants.dart | 26 +-- .../helpwave_theme/lib/src/theme/theme.dart | 21 +- .../util/material_state_color_resolver.dart | 8 +- .../src/loading/loading_change_notifier.dart | 1 - .../material_state/material_state_helper.dart | 22 +- .../src/bottom_sheets/bottom_sheet_base.dart | 2 +- .../src/content_selection/list_search.dart | 215 +++++++++--------- .../lib/src/loading/pulsing_container.dart | 2 +- .../lib/src/navigation/navigation_outlet.dart | 2 +- .../password_text_editing_field.dart | 4 +- .../src/widgets/forward_navigation_tile.dart | 2 +- 25 files changed, 251 insertions(+), 247 deletions(-) diff --git a/apps/tasks/lib/components/bottom_sheet_pages/patient_bottom_sheet.dart b/apps/tasks/lib/components/bottom_sheet_pages/patient_bottom_sheet.dart index c68f1ea1..6cf3a1e6 100644 --- a/apps/tasks/lib/components/bottom_sheet_pages/patient_bottom_sheet.dart +++ b/apps/tasks/lib/components/bottom_sheet_pages/patient_bottom_sheet.dart @@ -19,7 +19,7 @@ class PatientBottomSheet extends StatefulWidget { /// The identifier of the [Patient] final String? patentId; - const PatientBottomSheet({Key? key, this.patentId}) : super(key: key); + const PatientBottomSheet({super.key, this.patentId}); @override State createState() => _PatientBottomSheetState(); @@ -76,71 +76,74 @@ class _PatientBottomSheetState extends State { padding: const EdgeInsets.symmetric(vertical: paddingSmall), child: Consumer(builder: (context, patientController, _) { return LoadingAndErrorWidget( - state: patientController.state, - child: Row( - mainAxisAlignment: - patientController.isCreating ? MainAxisAlignment.end : MainAxisAlignment.spaceBetween, - children: patientController.isCreating - ? [ - FilledButton( - onPressed: patientController.create, - child: Text(context.localization.create), - ) - ] - : [ - SizedBox( - width: width * 0.4, - // TODO make this state checking easier and more readable - child: FilledButton( - onPressed: patientController.patient.isNotAssignedToBed - ? null - : () { - patientController.unassign(); - }, - style: buttonStyleMedium.copyWith( - backgroundColor: resolveByStatesAndContextBackground( - context: context, - defaultValue: inProgressColor, - ), - foregroundColor: resolveByStatesAndContextForeground( - context: context, - ), + state: patientController.state, + child: Row( + mainAxisAlignment: + patientController.isCreating ? MainAxisAlignment.end : MainAxisAlignment.spaceBetween, + children: patientController.isCreating + ? [ + FilledButton( + onPressed: patientController.create, + child: Text(context.localization.create), + ) + ] + : [ + SizedBox( + width: width * 0.4, + // TODO make this state checking easier and more readable + child: FilledButton( + onPressed: patientController.patient.isNotAssignedToBed + ? null + : () { + patientController.unassign(); + }, + style: buttonStyleMedium.copyWith( + backgroundColor: resolveByStatesAndContextBackground( + context: context, + defaultValue: inProgressColor, + ), + foregroundColor: resolveByStatesAndContextForeground( + context: context, ), - child: Text(context.localization.unassigne), ), + child: Text(context.localization.unassigne), ), - SizedBox( - width: width * 0.4, - child: FilledButton( - // TODO check whether the patient is active - onPressed: patientController.patient.isDischarged - ? null - : () { - showDialog( - context: context, - builder: (context) => - AcceptDialog(titleText: context.localization.dischargePatient), - ).then((value) { - if (value) { - patientController.discharge(); + ), + SizedBox( + width: width * 0.4, + child: FilledButton( + // TODO check whether the patient is active + onPressed: patientController.patient.isDischarged + ? null + : () { + showDialog( + context: context, + builder: (context) => + AcceptDialog(titleText: context.localization.dischargePatient), + ).then((value) { + if (value) { + patientController.discharge(); + if (context.mounted) { Navigator.of(context).pop(); } - }); - }, - style: buttonStyleMedium.copyWith( - backgroundColor: resolveByStatesAndContextBackground( - context: context, - defaultValue: negativeColor, - ), - foregroundColor: resolveByStatesAndContextForeground( - context: context, - ), + } + }); + }, + style: buttonStyleMedium.copyWith( + backgroundColor: resolveByStatesAndContextBackground( + context: context, + defaultValue: negativeColor, + ), + foregroundColor: resolveByStatesAndContextForeground( + context: context, ), - child: Text(context.localization.discharge), ), + child: Text(context.localization.discharge), ), - ], - )); + ), + ], + ), + ); }), ), child: Flexible( diff --git a/apps/tasks/lib/components/bottom_sheet_pages/task_bottom_sheet.dart b/apps/tasks/lib/components/bottom_sheet_pages/task_bottom_sheet.dart index 4396995d..8f9bcd30 100644 --- a/apps/tasks/lib/components/bottom_sheet_pages/task_bottom_sheet.dart +++ b/apps/tasks/lib/components/bottom_sheet_pages/task_bottom_sheet.dart @@ -152,7 +152,7 @@ class _TaskBottomSheetState extends State { onPressed: taskController.isReadyForCreate ? () { taskController.create().then((value) { - if (value) { + if (value && context.mounted) { Navigator.pop(context); } }); diff --git a/apps/tasks/lib/components/bottom_sheet_pages/task_template_bottom_sheet.dart b/apps/tasks/lib/components/bottom_sheet_pages/task_template_bottom_sheet.dart index 2d7c2068..8a79f742 100644 --- a/apps/tasks/lib/components/bottom_sheet_pages/task_template_bottom_sheet.dart +++ b/apps/tasks/lib/components/bottom_sheet_pages/task_template_bottom_sheet.dart @@ -66,7 +66,9 @@ class TaskTemplateBottomSheetPage extends StatelessWidget { icon: Icons.check, onPressed: () { controller.create().then((_) { - NavigationStackController.of(context).pop(); + if(context.mounted){ + NavigationStackController.of(context).pop(); + } }); }, ) diff --git a/apps/tasks/lib/components/navigation_drawer.dart b/apps/tasks/lib/components/navigation_drawer.dart index 71e9ae1b..bea4b426 100644 --- a/apps/tasks/lib/components/navigation_drawer.dart +++ b/apps/tasks/lib/components/navigation_drawer.dart @@ -25,9 +25,7 @@ class TasksNavigationDrawer extends StatelessWidget { /// determines the [Color] of the ListTile for the given page Color tileColorForPage(NavigationOptions page) { - return currentPage == page - ? Colors.black.withOpacity(0.2) - : Colors.transparent; + return currentPage == page ? Colors.black.withOpacity(0.2) : Colors.transparent; } /// Replaces the current Page with the given one with an Animation @@ -35,16 +33,21 @@ class TasksNavigationDrawer extends StatelessWidget { // TODO fine tune here to see what kind of animation transition looks the best Navigator.pop(context); await Future.delayed(const Duration(milliseconds: 100)).then( - (_) => Navigator.pushReplacement( - context, - PageRouteBuilder( - pageBuilder: (_, __, ___) => newPage, - transitionsBuilder: (_, __, ___, child) { - return child; - }, - transitionDuration: Duration.zero, - ), - ), + (_) { + if (!context.mounted) { + return; + } + Navigator.pushReplacement( + context, + PageRouteBuilder( + pageBuilder: (_, __, ___) => newPage, + transitionsBuilder: (_, __, ___, child) { + return child; + }, + transitionDuration: Duration.zero, + ), + ); + }, ); } @@ -66,17 +69,15 @@ class TasksNavigationDrawer extends StatelessWidget { ListTile( leading: const Icon(Icons.task_alt), tileColor: tileColorForPage(NavigationOptions.myTasks), - onTap: currentPage == NavigationOptions.myTasks - ? null - : () => pushReplace(context, const MyTasksScreen()), + onTap: + currentPage == NavigationOptions.myTasks ? null : () => pushReplace(context, const MyTasksScreen()), title: Text(context.localization.myTasks), ), ListTile( leading: const Icon(Icons.settings), tileColor: tileColorForPage(NavigationOptions.settings), - onTap: currentPage == NavigationOptions.settings - ? null - : () => pushReplace(context, const SettingsScreen()), + onTap: + currentPage == NavigationOptions.settings ? null : () => pushReplace(context, const SettingsScreen()), title: Text(context.localization.settings), ), Flexible(child: Container()), diff --git a/apps/tasks/lib/components/task_expansion_tile.dart b/apps/tasks/lib/components/task_expansion_tile.dart index 2f7c0bf6..4b374a76 100644 --- a/apps/tasks/lib/components/task_expansion_tile.dart +++ b/apps/tasks/lib/components/task_expansion_tile.dart @@ -60,6 +60,9 @@ class TaskExpansionTile extends StatelessWidget { builder: (context) => TaskBottomSheet(task: task, patient: task.patient), ).then((_) { try { + if(!context.mounted) { + return; + } AssignedTasksController controller = Provider.of(context, listen: false); controller.load(); } catch (e) { diff --git a/apps/tasks/lib/components/user_header.dart b/apps/tasks/lib/components/user_header.dart index 2ffe6f6f..5db1083f 100644 --- a/apps/tasks/lib/components/user_header.dart +++ b/apps/tasks/lib/components/user_header.dart @@ -13,7 +13,7 @@ import 'package:tasks/components/bottom_sheet_pages/user_bottom_sheet.dart'; /// A [AppBar] for displaying the current [User], [Organization] and [Ward] class UserHeader extends StatelessWidget implements PreferredSizeWidget { - const UserHeader({Key? key}) : super(key: key); + const UserHeader({super.key}); @override Widget build(BuildContext context) { @@ -59,7 +59,7 @@ class UserHeader extends StatelessWidget implements PreferredSizeWidget { Text( "${currentWardController.currentWard?.organization.shortName ?? ""} - ", style: TextStyle( - color: context.theme.colorScheme.onBackground.withOpacity(0.7), + color: context.theme.colorScheme.onSurface.withOpacity(0.7), fontSize: 14, ), ), diff --git a/apps/tasks/lib/screens/landing_screen.dart b/apps/tasks/lib/screens/landing_screen.dart index 6e2184cc..79d7a4ca 100644 --- a/apps/tasks/lib/screens/landing_screen.dart +++ b/apps/tasks/lib/screens/landing_screen.dart @@ -25,8 +25,8 @@ class LandingScreen extends StatelessWidget { } return OutlinedButton( - style: buttonStyleBig.copyWith(side: MaterialStatePropertyAll(buttonBorderSideBig.copyWith(color: context - .theme.colorScheme.onBackground))), + style: buttonStyleBig.copyWith(side: WidgetStatePropertyAll(buttonBorderSideBig.copyWith(color: context + .theme.colorScheme.onSurface))), child: Text( context.localization.loginSlogan, style: context.theme.textTheme.labelLarge, diff --git a/apps/tasks/lib/screens/main_screen_subscreens/patient_screen.dart b/apps/tasks/lib/screens/main_screen_subscreens/patient_screen.dart index f6d7daac..1813292a 100644 --- a/apps/tasks/lib/screens/main_screen_subscreens/patient_screen.dart +++ b/apps/tasks/lib/screens/main_screen_subscreens/patient_screen.dart @@ -42,7 +42,7 @@ class _PatientScreenState extends State { icon: Icon( Icons.search, size: iconSizeTiny, - color: context.theme.searchBarTheme.textStyle!.resolve({MaterialState.selected})!.color, + color: context.theme.searchBarTheme.textStyle!.resolve({WidgetState.selected})!.color, ), ), ], diff --git a/apps/tasks/pubspec.lock b/apps/tasks/pubspec.lock index 724aad2b..51cc22fb 100644 --- a/apps/tasks/pubspec.lock +++ b/apps/tasks/pubspec.lock @@ -288,10 +288,10 @@ packages: dependency: transitive description: name: helpwave_proto_dart - sha256: eeff6a1a3645e412ad2a8024afd113c1d10779ddb039d5824f8a8bab203ff990 + sha256: "5a8e82177ed18240b5506d514e7e9af9b512d7f2363d9278a2d34f79291d37c2" url: "https://pub.dev" source: hosted - version: "0.58.1-688aac0" + version: "0.59.0-e1d7c0c" helpwave_service: dependency: "direct main" description: diff --git a/packages/helpwave_service/lib/src/api/property/controllers/property_controller.dart b/packages/helpwave_service/lib/src/api/property/controllers/property_controller.dart index 559e91d5..2f3c8bce 100644 --- a/packages/helpwave_service/lib/src/api/property/controllers/property_controller.dart +++ b/packages/helpwave_service/lib/src/api/property/controllers/property_controller.dart @@ -3,8 +3,8 @@ import 'package:helpwave_service/util.dart'; /// The Controller for managing [Property]s in a Ward class PropertyController extends LoadController { - PropertyController({String? id, Property? property}) - : super(id: id, initialData: property, service: PropertyService()); + PropertyController({super.id, Property? property}) + : super(initialData: property, service: PropertyService()); @override Property defaultData() => Property.empty(); diff --git a/packages/helpwave_service/lib/src/api/property/services/property_value_service.dart b/packages/helpwave_service/lib/src/api/property/services/property_value_service.dart index d52c2764..01743326 100644 --- a/packages/helpwave_service/lib/src/api/property/services/property_value_service.dart +++ b/packages/helpwave_service/lib/src/api/property/services/property_value_service.dart @@ -2,7 +2,6 @@ import 'package:grpc/grpc.dart'; import 'package:helpwave_proto_dart/google/protobuf/timestamp.pb.dart'; import 'package:helpwave_proto_dart/services/property_svc/v1/property_value_svc.pbgrpc.dart'; import 'package:helpwave_proto_dart/services/property_svc/v1/types.pb.dart'; -import 'package:helpwave_service/src/api/property/property_api_service_clients.dart'; import 'package:helpwave_service/src/api/property/util/type_converter.dart'; import '../../../../property.dart'; diff --git a/packages/helpwave_service/lib/src/api/property/services/property_views_service.dart b/packages/helpwave_service/lib/src/api/property/services/property_views_service.dart index df4c76db..93c0374a 100644 --- a/packages/helpwave_service/lib/src/api/property/services/property_views_service.dart +++ b/packages/helpwave_service/lib/src/api/property/services/property_views_service.dart @@ -1,7 +1,6 @@ import 'package:grpc/grpc.dart'; import 'package:helpwave_proto_dart/services/property_svc/v1/property_value_svc.pb.dart'; import 'package:helpwave_proto_dart/services/property_svc/v1/property_views_svc.pbgrpc.dart'; -import 'package:helpwave_service/src/api/property/property_api_service_clients.dart'; import '../../../../property.dart'; /// The GRPC Service for [PropertyViewRules]s diff --git a/packages/helpwave_service/lib/src/api/user/controllers/organization_controller.dart b/packages/helpwave_service/lib/src/api/user/controllers/organization_controller.dart index 8a565d89..64b85857 100644 --- a/packages/helpwave_service/lib/src/api/user/controllers/organization_controller.dart +++ b/packages/helpwave_service/lib/src/api/user/controllers/organization_controller.dart @@ -5,8 +5,8 @@ import '../../../../user.dart'; /// The Controller for managing a [Organization] class OrganizationController extends LoadController { - OrganizationController({String? id, Organization? initialData}) - : super(id: id, initialData: initialData, service: OrganizationService()); + OrganizationController({super.id, super.initialData}) + : super(service: OrganizationService()); @override Future update(OrganizationUpdate? update) async { diff --git a/packages/helpwave_service/pubspec.yaml b/packages/helpwave_service/pubspec.yaml index 0c56fe9c..f1d4f4c1 100644 --- a/packages/helpwave_service/pubspec.yaml +++ b/packages/helpwave_service/pubspec.yaml @@ -17,7 +17,7 @@ dependencies: flutter_secure_storage: 9.0.0 jose: ^0.3.4 logger: ^2.0.2+1 - helpwave_proto_dart: ^0.58.0-1191f09 + helpwave_proto_dart: ^0.59.0-e1d7c0c grpc: ^3.2.4 helpwave_util: path: "../helpwave_util" diff --git a/packages/helpwave_theme/lib/src/constants.dart b/packages/helpwave_theme/lib/src/constants.dart index 214faf3b..b984e706 100644 --- a/packages/helpwave_theme/lib/src/constants.dart +++ b/packages/helpwave_theme/lib/src/constants.dart @@ -8,39 +8,39 @@ const BorderSide buttonBorderSideMedium = BorderSide(width: 1.5); const BorderSide buttonBorderSideBig = BorderSide(width: 2); ButtonStyle buttonStyleSmall = ButtonStyle( - minimumSize: const MaterialStatePropertyAll(Size(50, 20)), - shape: MaterialStatePropertyAll( + minimumSize: const WidgetStatePropertyAll(Size(50, 20)), + shape: WidgetStatePropertyAll( RoundedRectangleBorder( borderRadius: BorderRadius.circular(borderRadiusSmall), ), ), - textStyle: const MaterialStatePropertyAll(TextStyle( + textStyle: const WidgetStatePropertyAll(TextStyle( fontSize: 15, fontWeight: FontWeight.w500, )), ); ButtonStyle buttonStyleMedium = ButtonStyle( - minimumSize: const MaterialStatePropertyAll(Size(120, 40)), - shape: MaterialStatePropertyAll( + minimumSize: const WidgetStatePropertyAll(Size(120, 40)), + shape: WidgetStatePropertyAll( RoundedRectangleBorder( borderRadius: BorderRadius.circular(7), ), ), - textStyle: const MaterialStatePropertyAll(TextStyle( + textStyle: const WidgetStatePropertyAll(TextStyle( fontSize: 17, fontWeight: FontWeight.w500, )), ); ButtonStyle buttonStyleBig = ButtonStyle( - minimumSize: const MaterialStatePropertyAll(Size(250, 50)), - shape: MaterialStatePropertyAll( + minimumSize: const WidgetStatePropertyAll(Size(250, 50)), + shape: WidgetStatePropertyAll( RoundedRectangleBorder( borderRadius: BorderRadius.circular(borderRadiusMedium), ), ), - textStyle: const MaterialStatePropertyAll(TextStyle( + textStyle: const WidgetStatePropertyAll(TextStyle( fontSize: 20, fontWeight: FontWeight.w500, )), @@ -121,10 +121,10 @@ const double defaultElevation = 4; /// Common Themes const searchBarTheme = SearchBarThemeData( constraints: BoxConstraints(maxHeight: 40, minHeight: 40), - backgroundColor: MaterialStatePropertyAll(Color.fromARGB(255, 223, 223, 223)), - elevation: MaterialStatePropertyAll(0), - shadowColor: MaterialStatePropertyAll(Colors.transparent), - textStyle: MaterialStatePropertyAll(TextStyle(color: Colors.black))); + backgroundColor: WidgetStatePropertyAll(Color.fromARGB(255, 223, 223, 223)), + elevation: WidgetStatePropertyAll(0), + shadowColor: WidgetStatePropertyAll(Colors.transparent), + textStyle: WidgetStatePropertyAll(TextStyle(color: Colors.black))); const chipTheme = ChipThemeData( elevation: 2, diff --git a/packages/helpwave_theme/lib/src/theme/theme.dart b/packages/helpwave_theme/lib/src/theme/theme.dart index d71ca522..68e2ff78 100644 --- a/packages/helpwave_theme/lib/src/theme/theme.dart +++ b/packages/helpwave_theme/lib/src/theme/theme.dart @@ -76,29 +76,29 @@ ThemeData makeTheme({ enabledBorder: defaultOutlineInputBorder.copyWith(borderSide: BorderSide.none), errorBorder: defaultOutlineInputBorder.copyWith(borderSide: BorderSide.none), focusedErrorBorder: defaultOutlineInputBorder.copyWith(borderSide: BorderSide.none), - iconColor: MaterialStateColor.resolveWith((states) { - if (states.contains(MaterialState.focused)) { + iconColor: WidgetStateColor.resolveWith((states) { + if (states.contains(WidgetState.focused)) { return focusedColor; } else { return defaultColor; } }), - suffixIconColor: MaterialStateColor.resolveWith((states) { - if (states.contains(MaterialState.focused)) { + suffixIconColor: WidgetStateColor.resolveWith((states) { + if (states.contains(WidgetState.focused)) { return focusedColor; } else { return defaultColor; } }), - labelStyle: MaterialStateTextStyle.resolveWith((states) { - if (states.contains(MaterialState.focused)) { + labelStyle: WidgetStateTextStyle.resolveWith((states) { + if (states.contains(WidgetState.focused)) { return TextStyle(color: focusedColor); } else { return TextStyle(color: defaultColor); } }), - floatingLabelStyle: MaterialStateTextStyle.resolveWith((states) { - if (states.contains(MaterialState.focused)) { + floatingLabelStyle: WidgetStateTextStyle.resolveWith((states) { + if (states.contains(WidgetState.focused)) { return TextStyle(color: focusedColor); } else { return TextStyle(color: defaultColor); @@ -163,7 +163,7 @@ ThemeData makeTheme({ popupMenuTheme: PopupMenuThemeData( color: surface, textStyle: TextStyle(color: onSurface), - labelTextStyle: MaterialStatePropertyAll(TextStyle(color: onSurface)), + labelTextStyle: WidgetStatePropertyAll(TextStyle(color: onSurface)), shape: const RoundedRectangleBorder(borderRadius: BorderRadius.all(Radius.circular(borderRadiusMedium))), ), searchBarTheme: searchBarTheme, @@ -187,8 +187,6 @@ ThemeData makeTheme({ primary: primaryColor, inversePrimary: inversePrimaryColor, onPrimary: onPrimaryColor, - background: backgroundColor, - onBackground: onBackgroundColor, secondary: secondaryColor, onSecondary: onSecondaryColor, tertiary: tertiary, @@ -198,7 +196,6 @@ ThemeData makeTheme({ // Surface surface: surface, onSurface: onSurface, - surfaceVariant: surfaceVariant, onSurfaceVariant: onSurfaceVariant, inverseSurface: inverseSurface, onInverseSurface: onInverseSurface, diff --git a/packages/helpwave_theme/lib/src/util/material_state_color_resolver.dart b/packages/helpwave_theme/lib/src/util/material_state_color_resolver.dart index f866a4d4..ac5c04bc 100644 --- a/packages/helpwave_theme/lib/src/util/material_state_color_resolver.dart +++ b/packages/helpwave_theme/lib/src/util/material_state_color_resolver.dart @@ -2,8 +2,8 @@ import 'package:flutter/material.dart'; import 'package:helpwave_theme/src/util/context_extension.dart'; import 'package:helpwave_util/material_state.dart'; -/// Resolves the background [Color] based on the [MaterialState] and the [ThemeData] -MaterialStateProperty resolveByStatesAndContextBackground({ +/// Resolves the background [Color] based on the [WidgetState] and the [ThemeData] +WidgetStateProperty resolveByStatesAndContextBackground({ required BuildContext context, Color? defaultValue, Color? disabled, @@ -30,8 +30,8 @@ MaterialStateProperty resolveByStatesAndContextBackground({ ); } -/// Resolves the foreground [Color] based on the [MaterialState] and the [ThemeData] -MaterialStateProperty resolveByStatesAndContextForeground({ +/// Resolves the foreground [Color] based on the [WidgetState] and the [ThemeData] +WidgetStateProperty resolveByStatesAndContextForeground({ required BuildContext context, Color? defaultValue, Color? disabled, diff --git a/packages/helpwave_util/lib/src/loading/loading_change_notifier.dart b/packages/helpwave_util/lib/src/loading/loading_change_notifier.dart index 0bcf2bea..b7437fbb 100644 --- a/packages/helpwave_util/lib/src/loading/loading_change_notifier.dart +++ b/packages/helpwave_util/lib/src/loading/loading_change_notifier.dart @@ -1,4 +1,3 @@ -import 'package:flutter/cupertino.dart'; import 'package:flutter/foundation.dart'; import '../../loading.dart'; diff --git a/packages/helpwave_util/lib/src/material_state/material_state_helper.dart b/packages/helpwave_util/lib/src/material_state/material_state_helper.dart index 8d6d86dc..46bc24e7 100644 --- a/packages/helpwave_util/lib/src/material_state/material_state_helper.dart +++ b/packages/helpwave_util/lib/src/material_state/material_state_helper.dart @@ -1,7 +1,7 @@ import 'package:flutter/material.dart'; -/// A function to create a [MaterialStateProperty] by assigning each state a value -MaterialStateProperty resolveByStates({ +/// A function to create a [WidgetStateProperty] by assigning each state a value +WidgetStateProperty resolveByStates({ /// Used when no value is given for the other states required T defaultValue, T? disabled, @@ -13,29 +13,29 @@ MaterialStateProperty resolveByStates({ T? hovered, T? scrolledUnder, }) { - return MaterialStateProperty.resolveWith((states) { - if (states.contains(MaterialState.disabled)) { + return WidgetStateProperty.resolveWith((states) { + if (states.contains(WidgetState.disabled)) { return disabled ?? defaultValue; } - if (states.contains(MaterialState.error)) { + if (states.contains(WidgetState.error)) { return error ?? defaultValue; } - if (states.contains(MaterialState.dragged)) { + if (states.contains(WidgetState.dragged)) { return dragged ?? defaultValue; } - if (states.contains(MaterialState.hovered)) { + if (states.contains(WidgetState.hovered)) { return hovered ?? defaultValue; } - if (states.contains(MaterialState.selected)) { + if (states.contains(WidgetState.selected)) { return selected ?? defaultValue; } - if (states.contains(MaterialState.focused)) { + if (states.contains(WidgetState.focused)) { return focused ?? defaultValue; } - if (states.contains(MaterialState.pressed)) { + if (states.contains(WidgetState.pressed)) { return pressed ?? defaultValue; } - if (states.contains(MaterialState.scrolledUnder)) { + if (states.contains(WidgetState.scrolledUnder)) { return scrolledUnder ?? defaultValue; } diff --git a/packages/helpwave_widget/lib/src/bottom_sheets/bottom_sheet_base.dart b/packages/helpwave_widget/lib/src/bottom_sheets/bottom_sheet_base.dart index 0e93fd56..a03f7132 100644 --- a/packages/helpwave_widget/lib/src/bottom_sheets/bottom_sheet_base.dart +++ b/packages/helpwave_widget/lib/src/bottom_sheets/bottom_sheet_base.dart @@ -250,7 +250,7 @@ class BottomSheetHeader extends StatelessWidget { height: 5, width: 30, decoration: BoxDecoration( - color: context.theme.colorScheme.onBackground.withOpacity(0.8), + color: context.theme.colorScheme.onSurface.withOpacity(0.8), borderRadius: BorderRadius.circular(5), ), ), diff --git a/packages/helpwave_widget/lib/src/content_selection/list_search.dart b/packages/helpwave_widget/lib/src/content_selection/list_search.dart index 6af4ab03..413f1063 100644 --- a/packages/helpwave_widget/lib/src/content_selection/list_search.dart +++ b/packages/helpwave_widget/lib/src/content_selection/list_search.dart @@ -36,8 +36,7 @@ class ListSearch extends StatefulWidget { /// IMPORTANT: This disables navigation pops on clicking a result, but can be implemented in the function /// /// IMPORTANT: Overwrites default display of search results - final Widget Function(BuildContext context, T item, - void Function(T item, BuildContext context) selectItem)? + final Widget Function(BuildContext context, T item, void Function(T item, BuildContext context) selectItem)? resultTileBuilder; /// Allow adding user input, if filtered search items are empty for a given search @@ -130,121 +129,123 @@ class _ListSearchState extends State> { @override Widget build(BuildContext context) { - return WillPopScope( - onWillPop: () async { - if (widget.isMultiSelect) { - Navigator.of(context).pop(selected); - return false; - } - return true; - }, - child: Scaffold( - appBar: AppBar( - title: Text(widget.title ?? context.localization.listSearch), - ), - body: SizedBox( - height: MediaQuery.of(context).size.height, - child: Column( - //mainAxisAlignment: MainAxisAlignment.start, - //crossAxisAlignment: CrossAxisAlignment.center, - children: [ - Padding( - padding: const EdgeInsets.symmetric( - horizontal: distanceSmall, - vertical: distanceDefault, - ), - child: TextFormField( - onChanged: (_) => {setState(() {})}, - controller: _searchController, - decoration: InputDecoration( - suffixIcon: IconButton( - onPressed: () => { - setState(() { - _searchController.clear(); - }) - }, - icon: const Icon(Icons.close), - ), - hintText: widget.searchHintText ?? "${context.localization.search}...", - border: const OutlineInputBorder(), + return Scaffold( + appBar: AppBar( + title: Text(widget.title ?? context.localization.listSearch), + actions: [ + Visibility( + visible: widget.isMultiSelect, + child: IconButton( + onPressed: () { + Navigator.pop(context, selected); + }, + icon: Icon(Icons.check), + ), + ) + ], + ), + body: SizedBox( + height: MediaQuery.of(context).size.height, + child: Column( + //mainAxisAlignment: MainAxisAlignment.start, + //crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Padding( + padding: const EdgeInsets.symmetric( + horizontal: distanceSmall, + vertical: distanceDefault, + ), + child: TextFormField( + onChanged: (_) => {setState(() {})}, + controller: _searchController, + decoration: InputDecoration( + suffixIcon: IconButton( + onPressed: () => { + setState(() { + _searchController.clear(); + }) + }, + icon: const Icon(Icons.close), ), + hintText: widget.searchHintText ?? "${context.localization.search}...", + border: const OutlineInputBorder(), ), ), - FutureBuilder( - future: getSearchResults(_searchController.text), - builder: (context, snapshot) { - List children = []; - if (snapshot.hasData) { - if (snapshot.data!.isNotEmpty) { - if (!widget.isMultiSelect && widget.resultTileBuilder != null) { - children = snapshot.data! - .map((element) => widget.resultTileBuilder!(context, element, selectItem)) - .toList(); - } else { - children = snapshot.data!.map((element) { - return ListTile( - title: Text(widget.elementToString(element)), - trailing: widget.isMultiSelect - ? Checkbox( - onChanged: (_) => selectItem(element, context), - value: selected - .contains(element), // TODO use equal check contains potentially always false - ) - : const SizedBox(), - onTap: () => selectItem(element, context), - ); - }).toList(); - } + ), + FutureBuilder( + future: getSearchResults(_searchController.text), + builder: (context, snapshot) { + List children = []; + if (snapshot.hasData) { + if (snapshot.data!.isNotEmpty) { + if (!widget.isMultiSelect && widget.resultTileBuilder != null) { + children = snapshot.data! + .map((element) => widget.resultTileBuilder!(context, element, selectItem)) + .toList(); } else { - children.add(Center( - child: Column( - children: [ - const SizedBox(height: distanceBig), - Text( - widget.elementNotFoundText != null - ? widget.elementNotFoundText!(_searchController.text) - : "${context.localization.noItem} ${_searchController.text} ${context.localization.found}", - style: context.theme.textTheme.titleLarge, - ), - const SizedBox(height: distanceDefault), - widget.allowSelectAnyway - ? TextButton( - onPressed: () => Navigator.pop(context, _searchController.text.trim()), - child: Text(widget.addAnywayText ?? "${context.localization.addAnyway}!"), - ) - : const SizedBox(), - ], - ), - )); + children = snapshot.data!.map((element) { + return ListTile( + title: Text(widget.elementToString(element)), + trailing: widget.isMultiSelect + ? Checkbox( + onChanged: (_) => selectItem(element, context), + value: selected + .contains(element), // TODO use equal check contains potentially always false + ) + : const SizedBox(), + onTap: () => selectItem(element, context), + ); + }).toList(); } - } else if (snapshot.hasError) { - children = [ - Icon( - Icons.error_outline, - color: context.theme.colorScheme.error, - size: iconSizeBig, + } else { + children.add(Center( + child: Column( + children: [ + const SizedBox(height: distanceBig), + Text( + widget.elementNotFoundText != null + ? widget.elementNotFoundText!(_searchController.text) + : "${context.localization.noItem} ${_searchController.text} ${context.localization.found}", + style: context.theme.textTheme.titleLarge, + ), + const SizedBox(height: distanceDefault), + widget.allowSelectAnyway + ? TextButton( + onPressed: () => Navigator.pop(context, _searchController.text.trim()), + child: Text(widget.addAnywayText ?? "${context.localization.addAnyway}!"), + ) + : const SizedBox(), + ], ), - const SizedBox(height: distanceBig), - Text('${context.localization.error}: ${snapshot.error}'), - ]; + )); } - return Expanded( - child: ListView( - children: children, + } else if (snapshot.hasError) { + children = [ + Icon( + Icons.error_outline, + color: context.theme.colorScheme.error, + size: iconSizeBig, ), - ); - }, - ) - ], - ), + const SizedBox(height: distanceBig), + Text('${context.localization.error}: ${snapshot.error}'), + ]; + } + return Expanded( + child: ListView( + children: children, + ), + ); + }, + ) + ], ), - floatingActionButton: widget.isMultiSelect - ? FloatingActionButton( - onPressed: () => Navigator.of(context).pop(selected), - backgroundColor: positiveColor, - child: const Icon(Icons.check)) - : const SizedBox(), ), + floatingActionButton: widget.isMultiSelect + ? FloatingActionButton( + onPressed: () => Navigator.of(context).pop(selected), + backgroundColor: positiveColor, + child: const Icon(Icons.check)) + : const SizedBox(), ); } } diff --git a/packages/helpwave_widget/lib/src/loading/pulsing_container.dart b/packages/helpwave_widget/lib/src/loading/pulsing_container.dart index c7ad29fd..5b8f26eb 100644 --- a/packages/helpwave_widget/lib/src/loading/pulsing_container.dart +++ b/packages/helpwave_widget/lib/src/loading/pulsing_container.dart @@ -72,7 +72,7 @@ class _PulsingContainerState extends State with TickerProvider @override Widget build(BuildContext context) { - Color baseColor = widget.color ?? Theme.of(context).colorScheme.onBackground; + Color baseColor = widget.color ?? Theme.of(context).colorScheme.onSurface; return AnimatedBuilder( animation: _opacityAnimation, diff --git a/packages/helpwave_widget/lib/src/navigation/navigation_outlet.dart b/packages/helpwave_widget/lib/src/navigation/navigation_outlet.dart index 0bd53d6f..68924f04 100644 --- a/packages/helpwave_widget/lib/src/navigation/navigation_outlet.dart +++ b/packages/helpwave_widget/lib/src/navigation/navigation_outlet.dart @@ -37,7 +37,7 @@ class NavigationStack extends StatelessWidget { return PopScope( // If the navigation controller cannot pop to another bottom sheet, a normal pop is correct canPop: !navigationController.canPop, - onPopInvoked: (didPop) { + onPopInvokedWithResult: (didPop, _) { if (navigationController.canPop) { navigationController.pop(); } diff --git a/packages/helpwave_widget/lib/src/text_input/password_text_editing_field.dart b/packages/helpwave_widget/lib/src/text_input/password_text_editing_field.dart index 91d15e91..e8e3b3c2 100644 --- a/packages/helpwave_widget/lib/src/text_input/password_text_editing_field.dart +++ b/packages/helpwave_widget/lib/src/text_input/password_text_editing_field.dart @@ -132,8 +132,8 @@ class ObscuringTextEditingController extends TextEditingController { ObscuringTextEditingController({ this.obscureSymbol = '•', - String? text, - }) : super(text: text); + super.text, + }); @override TextSpan buildTextSpan( diff --git a/packages/helpwave_widget/lib/src/widgets/forward_navigation_tile.dart b/packages/helpwave_widget/lib/src/widgets/forward_navigation_tile.dart index 2682ade3..fc2ecb59 100644 --- a/packages/helpwave_widget/lib/src/widgets/forward_navigation_tile.dart +++ b/packages/helpwave_widget/lib/src/widgets/forward_navigation_tile.dart @@ -42,7 +42,7 @@ class ForwardNavigationTile extends StatelessWidget { : const SizedBox(), Icon( Icons.chevron_right_rounded, - color: context.theme.colorScheme.onBackground.withOpacity(0.7), + color: context.theme.colorScheme.onSurface.withOpacity(0.7), ), ], ), From 12e441904b6e8493b2e476bd1372345e6d6e0fd8 Mon Sep 17 00:00:00 2001 From: Felix Thape Date: Wed, 27 Nov 2024 08:48:23 +0100 Subject: [PATCH 35/36] fix: fix bug when loading assigned tasks --- .../bottom_sheet_pages/patient_bottom_sheet.dart | 2 ++ apps/tasks/lib/components/task_expansion_tile.dart | 1 + .../api/tasks/controllers/my_tasks_controller.dart | 12 ++++++++++++ .../api/tasks/controllers/patient_controller.dart | 2 +- .../src/api/tasks/controllers/task_controller.dart | 2 +- .../lib/src/api/tasks/services/patient_svc.dart | 1 + .../lib/src/api/tasks/services/task_svc.dart | 12 +++++++++--- 7 files changed, 27 insertions(+), 5 deletions(-) diff --git a/apps/tasks/lib/components/bottom_sheet_pages/patient_bottom_sheet.dart b/apps/tasks/lib/components/bottom_sheet_pages/patient_bottom_sheet.dart index 6cf3a1e6..014c9021 100644 --- a/apps/tasks/lib/components/bottom_sheet_pages/patient_bottom_sheet.dart +++ b/apps/tasks/lib/components/bottom_sheet_pages/patient_bottom_sheet.dart @@ -44,6 +44,7 @@ class _PatientBottomSheetState extends State { @override Widget build(BuildContext context) { + print(widget.patentId); double width = MediaQuery.of(context).size.width; return MultiProvider( providers: [ @@ -220,6 +221,7 @@ class _PatientBottomSheetState extends State { ...patient.doneTasks, ], itemBuilder: (_, index, taskList) { + // TODO after return from navigation reload if (index == 0) { return TaskExpansionTile( tasks: patient.unscheduledTasks diff --git a/apps/tasks/lib/components/task_expansion_tile.dart b/apps/tasks/lib/components/task_expansion_tile.dart index 4b374a76..7247fbfd 100644 --- a/apps/tasks/lib/components/task_expansion_tile.dart +++ b/apps/tasks/lib/components/task_expansion_tile.dart @@ -63,6 +63,7 @@ class TaskExpansionTile extends StatelessWidget { if(!context.mounted) { return; } + // TODO This widget is used in contexts without the AssignedTasksController leading to issues AssignedTasksController controller = Provider.of(context, listen: false); controller.load(); } catch (e) { diff --git a/packages/helpwave_service/lib/src/api/tasks/controllers/my_tasks_controller.dart b/packages/helpwave_service/lib/src/api/tasks/controllers/my_tasks_controller.dart index 70b89070..564b55db 100644 --- a/packages/helpwave_service/lib/src/api/tasks/controllers/my_tasks_controller.dart +++ b/packages/helpwave_service/lib/src/api/tasks/controllers/my_tasks_controller.dart @@ -1,3 +1,4 @@ +import 'package:helpwave_service/src/util/identified_object.dart'; import 'package:helpwave_service/tasks.dart'; import 'package:helpwave_util/loading.dart'; @@ -30,4 +31,15 @@ class AssignedTasksController extends LoadingChangeNotifier { loadHandler(future: loadTasksFuture()); } + + // TODO allow for more complex updates + Future updateTask(Task task) async { + assert(!task.isCreating); + loadTasksFuture() async { + await TaskService().updateTask(taskId: task.id!, status: task.status); + _tasks = await TaskService().getAssignedTasks(); + } + + loadHandler(future: loadTasksFuture()); + } } diff --git a/packages/helpwave_service/lib/src/api/tasks/controllers/patient_controller.dart b/packages/helpwave_service/lib/src/api/tasks/controllers/patient_controller.dart index 1eb7b5f7..41e76a5c 100644 --- a/packages/helpwave_service/lib/src/api/tasks/controllers/patient_controller.dart +++ b/packages/helpwave_service/lib/src/api/tasks/controllers/patient_controller.dart @@ -23,7 +23,7 @@ class PatientController extends LoadingChangeNotifier { if(patient != null) { _patient = patient; } else if(id != null) { - _patient.copyWith(id: id); + _patient = _patient.copyWith(id: id); } load(); } diff --git a/packages/helpwave_service/lib/src/api/tasks/controllers/task_controller.dart b/packages/helpwave_service/lib/src/api/tasks/controllers/task_controller.dart index 8e02097f..e1404071 100644 --- a/packages/helpwave_service/lib/src/api/tasks/controllers/task_controller.dart +++ b/packages/helpwave_service/lib/src/api/tasks/controllers/task_controller.dart @@ -41,7 +41,7 @@ class TaskController extends LoadingChangeNotifier { await TaskService().getTask(id: task.id).then((value) async { task = value; if (task.hasAssignee) { - UserService().getUser(id: task.assigneeId!).then((value) => _assignee = value); + await UserService().getUser(id: task.assigneeId!).then((value) => _assignee = value); } }); } diff --git a/packages/helpwave_service/lib/src/api/tasks/services/patient_svc.dart b/packages/helpwave_service/lib/src/api/tasks/services/patient_svc.dart index fe58f0e9..c0e09e08 100644 --- a/packages/helpwave_service/lib/src/api/tasks/services/patient_svc.dart +++ b/packages/helpwave_service/lib/src/api/tasks/services/patient_svc.dart @@ -91,6 +91,7 @@ class PatientService { metadata: TasksAPIServiceClients().getMetaData(), ), ); + print(response.tasks.length); return Patient( id: response.id, diff --git a/packages/helpwave_service/lib/src/api/tasks/services/task_svc.dart b/packages/helpwave_service/lib/src/api/tasks/services/task_svc.dart index 97f48577..3ace4cbc 100644 --- a/packages/helpwave_service/lib/src/api/tasks/services/task_svc.dart +++ b/packages/helpwave_service/lib/src/api/tasks/services/task_svc.dart @@ -5,7 +5,6 @@ import 'package:helpwave_proto_dart/services/tasks_svc/v1/task_svc.pbgrpc.dart'; import '../util/task_status_mapping.dart'; import 'package:helpwave_service/util.dart'; - /// The GRPC Service for [Task]s /// /// Provides queries and requests that load or alter [Task] objects on the server @@ -77,7 +76,7 @@ class TaskService { } /// Loads the [Task]s assigned to the current [User] - Future> getAssignedTasks({String? id}) async { + Future> getAssignedTasks() async { GetAssignedTasksRequest request = GetAssignedTasksRequest(); GetAssignedTasksResponse response = await taskService.getAssignedTasks( request, @@ -117,6 +116,13 @@ class TaskService { dueAt: task.dueDate != null ? Timestamp.fromDateTime(task.dueDate!) : null, patientId: !task.patient.isCreating ? task.patient.id : null, public: task.isPublicVisible, + assignedUserId: task.assigneeId, + subtasks: task.subtasks + .map((subtask) => CreateTaskRequest_SubTask( + name: subtask.name, + done: subtask.isDone, + )) + .toList(), ); CreateTaskResponse response = await taskService.createTask( request, @@ -128,7 +134,7 @@ class TaskService { /// Assign a [Task] to a [User] or unassign the [User] Future changeAssignee({required String taskId, required String? userId}) async { - if(userId != null){ + if (userId != null) { AssignTaskRequest request = AssignTaskRequest(taskId: taskId, userId: userId); await taskService.assignTask( request, From b2ef6823be9f2925b0578b12fa612460c587631c Mon Sep 17 00:00:00 2001 From: Felix Thape Date: Thu, 28 Nov 2024 09:41:06 +0100 Subject: [PATCH 36/36] fix: update task and patient sheets --- .../patient_bottom_sheet.dart | 23 ++- .../bottom_sheet_pages/task_bottom_sheet.dart | 50 ++++-- apps/tasks/lib/components/subtask_list.dart | 3 - apps/tasks/lib/components/task_card.dart | 169 ++++++++++-------- .../lib/components/task_expansion_tile.dart | 83 ++++++--- apps/tasks/lib/debug/theme_visualizer.dart | 4 +- .../my_tasks_screen.dart | 17 ++ .../patient_screen.dart | 4 +- .../helpwave_localization/lib/l10n/app_de.arb | 5 +- .../helpwave_localization/lib/l10n/app_en.arb | 5 +- .../tasks/controllers/patient_controller.dart | 7 + .../tasks/controllers/task_controller.dart | 14 ++ .../src/api/tasks/services/patient_svc.dart | 1 - .../lib/src/lists/add_list.dart | 12 +- 14 files changed, 264 insertions(+), 133 deletions(-) diff --git a/apps/tasks/lib/components/bottom_sheet_pages/patient_bottom_sheet.dart b/apps/tasks/lib/components/bottom_sheet_pages/patient_bottom_sheet.dart index 014c9021..a16520b7 100644 --- a/apps/tasks/lib/components/bottom_sheet_pages/patient_bottom_sheet.dart +++ b/apps/tasks/lib/components/bottom_sheet_pages/patient_bottom_sheet.dart @@ -7,6 +7,7 @@ import 'package:helpwave_widget/bottom_sheets.dart'; import 'package:helpwave_widget/dialog.dart'; import 'package:helpwave_widget/lists.dart'; import 'package:helpwave_widget/loading.dart'; +import 'package:helpwave_widget/navigation.dart'; import 'package:helpwave_widget/text_input.dart'; import 'package:provider/provider.dart'; import 'package:tasks/components/bottom_sheet_pages/task_bottom_sheet.dart'; @@ -44,7 +45,6 @@ class _PatientBottomSheetState extends State { @override Widget build(BuildContext context) { - print(widget.patentId); double width = MediaQuery.of(context).size.width; return MultiProvider( providers: [ @@ -214,14 +214,21 @@ class _PatientBottomSheetState extends State { child: Consumer(builder: (context, patientController, _) { Patient patient = patientController.patient; return AddList( - maxHeight: width * 0.5, items: [ - ...patient.unscheduledTasks, - ...patient.inProgressTasks, - ...patient.doneTasks, + patient.unscheduledTasks, + patient.inProgressTasks, + patient.doneTasks, ], itemBuilder: (_, index, taskList) { // TODO after return from navigation reload + complete(Task task, PatientController controller) { + controller.updateTaskStatus(task.copyWith(status: TaskStatus.done)); + } + + openEdit(TaskWithPatient task) { + NavigationStackController.of(context).push(TaskBottomSheet(task: task, patient: task.patient)); + } + if (index == 0) { return TaskExpansionTile( tasks: patient.unscheduledTasks @@ -232,6 +239,8 @@ class _PatientBottomSheetState extends State { .toList(), title: context.localization.upcoming, color: upcomingColor, + onOpenEdit: (task) => openEdit(task), + onComplete: (task) => complete(task, patientController), ); } if (index == 2) { @@ -244,6 +253,8 @@ class _PatientBottomSheetState extends State { .toList(), title: context.localization.inProgress, color: inProgressColor, + onOpenEdit: (task) => openEdit(task), + onComplete: (task) => complete(task, patientController), ); } return TaskExpansionTile( @@ -255,6 +266,8 @@ class _PatientBottomSheetState extends State { .toList(), title: context.localization.done, color: doneColor, + onOpenEdit: (task) => openEdit(task), + onComplete: (task) => complete(task, patientController), ); }, title: Text( diff --git a/apps/tasks/lib/components/bottom_sheet_pages/task_bottom_sheet.dart b/apps/tasks/lib/components/bottom_sheet_pages/task_bottom_sheet.dart index 8f9bcd30..5cd5d118 100644 --- a/apps/tasks/lib/components/bottom_sheet_pages/task_bottom_sheet.dart +++ b/apps/tasks/lib/components/bottom_sheet_pages/task_bottom_sheet.dart @@ -116,6 +116,12 @@ class TaskBottomSheet extends StatefulWidget { class _TaskBottomSheetState extends State { @override Widget build(BuildContext context) { + Map taskStatusTranslationMap = { + TaskStatus.done: context.localization.done, + TaskStatus.inProgress: context.localization.inProgress, + TaskStatus.todo: context.localization.upcoming, + }; + return ChangeNotifierProvider( create: (context) => TaskController(TaskWithPatient.fromTaskAndPatient(task: widget.task, patient: widget.patient)), @@ -285,20 +291,40 @@ class _TaskBottomSheetState extends State { ], ), const SizedBox(height: distanceSmall), - Consumer( - builder: (_, taskController, __) => LoadingAndErrorWidget.pulsing( - state: taskController.state, - child: _SheetListTile( - icon: Icons.lock, - label: context.localization.visibility, - valueWidget: VisibilitySelect( - isPublicVisible: taskController.task.isPublicVisible, - onChanged: taskController.changeIsPublic, - isCreating: taskController.isCreating, - textStyle: editableValueTextStyle(context), + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Consumer( + builder: (_, taskController, __) => LoadingAndErrorWidget.pulsing( + state: taskController.state, + child: _SheetListTile( + icon: Icons.lock, + label: context.localization.visibility, + valueWidget: VisibilitySelect( + isPublicVisible: taskController.task.isPublicVisible, + onChanged: taskController.changeIsPublic, + isCreating: taskController.isCreating, + textStyle: editableValueTextStyle(context), + ), + ), ), ), - ), + Consumer( + builder: (_, taskController, __) => LoadingAndErrorWidget.pulsing( + state: taskController.state, + child: _SheetListTile( + icon: Icons.check, + label: context.localization.status, + valueWidget: Text( + taskStatusTranslationMap[taskController.task.status] ?? "", + style: editableValueTextStyle(context), + ), + // TODO show modal here + onTap: () => taskController.changeStatus(TaskStatus.done), + ), + ), + ), + ], ), const SizedBox(height: distanceMedium), Text( diff --git a/apps/tasks/lib/components/subtask_list.dart b/apps/tasks/lib/components/subtask_list.dart index ec3fccf0..2a6d574c 100644 --- a/apps/tasks/lib/components/subtask_list.dart +++ b/apps/tasks/lib/components/subtask_list.dart @@ -28,13 +28,10 @@ class SubtaskList extends StatelessWidget { @override Widget build(BuildContext context) { - const double sizeForSubtasks = 200; - return ChangeNotifierProvider( create: (context) => SubtasksController(taskId: taskId, subtasks: subtasks), child: Consumer(builder: (context, subtasksController, __) { return AddList( - maxHeight: sizeForSubtasks, items: subtasksController.subtasks, title: Text( context.localization.subtasks, diff --git a/apps/tasks/lib/components/task_card.dart b/apps/tasks/lib/components/task_card.dart index 18c78b7a..c2d563b1 100644 --- a/apps/tasks/lib/components/task_card.dart +++ b/apps/tasks/lib/components/task_card.dart @@ -16,7 +16,20 @@ class TaskCard extends StatelessWidget { /// The [borderRadius] of the [Card] final double borderRadius; - const TaskCard({super.key, required this.task, this.margin, this.borderRadius = borderRadiusMedium}); + /// The [Function] called when the [Task] should be completed + final Function() onComplete; + + /// The [Function] called when the [Task] should be edited + final Function() onTap; + + const TaskCard({ + super.key, + required this.task, + this.margin, + this.borderRadius = borderRadiusMedium, + required this.onComplete, + required this.onTap, + }); /// Determines the text shown for indicating the remaining time for the [Task] String getDueText(BuildContext context) { @@ -72,86 +85,90 @@ class TaskCard extends StatelessWidget { @override Widget build(BuildContext context) { - return Card( - margin: margin, - child: Padding( - padding: const EdgeInsets.symmetric(vertical: paddingSmall, horizontal: paddingMedium), - child: Column( - children: [ - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - StaticProgressIndicator( - progress: task.progress, - color: context.theme.colorScheme.primary, - backgroundColor: context.theme.colorScheme.onSurface.withOpacity(0.3), - isClockwise: true, - angle: 0, - ), - Chip( - side: BorderSide.none, - backgroundColor: getBackgroundColor(), - label: Text( - getDueText(context), - style: TextStyle(color: getTextColor()), + return InkWell( + borderRadius: BorderRadius.circular(borderRadius), + onTap: onTap, + child: Card( + margin: margin, + child: Padding( + padding: const EdgeInsets.symmetric(vertical: paddingSmall, horizontal: paddingMedium), + child: Column( + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + StaticProgressIndicator( + progress: task.progress, + color: context.theme.colorScheme.primary, + backgroundColor: context.theme.colorScheme.onSurface.withOpacity(0.3), + isClockwise: true, + angle: 0, + ), + Chip( + side: BorderSide.none, + backgroundColor: getBackgroundColor(), + label: Text( + getDueText(context), + style: TextStyle(color: getTextColor()), + ), + elevation: 0, ), - elevation: 0, - ), - ], - ), - const SizedBox( - height: distanceTiny, - ), - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Flexible( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - task.patient.name, - style: TextStyle(color: context.theme.colorScheme.primary), - ), - Text( - task.name, - style: const TextStyle( - fontSize: 20, - fontWeight: FontWeight.bold, - fontFamily: "SpaceGrotesk", + ], + ), + const SizedBox( + height: distanceTiny, + ), + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Flexible( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + task.patient.name, + style: TextStyle(color: context.theme.colorScheme.primary), ), - ), - task.notes.isNotEmpty ? Text( - task.notes, - style: TextStyle( - fontSize: 14, - fontFamily: "SpaceGrotesk", - overflow: TextOverflow.ellipsis, - color: context.theme.colorScheme.onSurface.withOpacity(0.6), + Text( + task.name, + style: const TextStyle( + fontSize: 20, + fontWeight: FontWeight.bold, + fontFamily: "SpaceGrotesk", + ), ), - ) : const SizedBox(), - ], + task.notes.isNotEmpty + ? Text( + task.notes, + style: TextStyle( + fontSize: 14, + fontFamily: "SpaceGrotesk", + overflow: TextOverflow.ellipsis, + color: context.theme.colorScheme.onSurface.withOpacity(0.6), + ), + ) + : const SizedBox(), + ], + ), ), - ), - const SizedBox(width: distanceTiny), - IconButton( - padding: EdgeInsets.zero, - constraints: const BoxConstraints(maxHeight: iconSizeSmall, maxWidth: iconSizeSmall), - onPressed: () { - // TODO change task status - }, - icon: Icon( - size: iconSizeTiny, - Icons.check_circle_outline_rounded, - // TODO change colors later - color: task.status == TaskStatus.done ? context.theme.colorScheme.onSurface.withOpacity(0.4) : context.theme - .colorScheme - .primary, + const SizedBox(width: distanceTiny), + IconButton( + padding: EdgeInsets.zero, + constraints: const BoxConstraints(maxHeight: iconSizeSmall, maxWidth: iconSizeSmall), + onPressed: onComplete, + icon: Icon( + size: iconSizeTiny, + Icons.check_circle_outline_rounded, + // TODO change colors later + color: task.status != TaskStatus.done + ? context.theme.colorScheme.onSurface.withOpacity(0.4) + : context.theme.colorScheme.primary, + ), ), - ), - ], - ) - ], + ], + ) + ], + ), ), ), ); diff --git a/apps/tasks/lib/components/task_expansion_tile.dart b/apps/tasks/lib/components/task_expansion_tile.dart index 7247fbfd..1bac8b08 100644 --- a/apps/tasks/lib/components/task_expansion_tile.dart +++ b/apps/tasks/lib/components/task_expansion_tile.dart @@ -1,11 +1,8 @@ -import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; +import 'package:helpwave_localization/localization.dart'; import 'package:helpwave_theme/constants.dart'; import 'package:helpwave_theme/util.dart'; -import 'package:helpwave_widget/bottom_sheets.dart'; import 'package:helpwave_widget/shapes.dart'; -import 'package:provider/provider.dart'; -import 'package:tasks/components/bottom_sheet_pages/task_bottom_sheet.dart'; import 'package:tasks/components/task_card.dart'; import 'package:helpwave_service/tasks.dart'; @@ -26,7 +23,20 @@ class TaskExpansionTile extends StatelessWidget { /// The [title] of the Tile final String title; - const TaskExpansionTile({super.key, required this.tasks, required this.color, required this.title}); + /// The [Function] called when the [TaskWithPatient] should be completed + final Function(TaskWithPatient task) onComplete; + + /// The [Function] called when the [TaskWithPatient] should be edited + final Function(TaskWithPatient task) onOpenEdit; + + const TaskExpansionTile({ + super.key, + required this.tasks, + required this.color, + required this.title, + required this.onComplete, + required this.onOpenEdit, + }); @override Widget build(BuildContext context) { @@ -53,32 +63,57 @@ class TaskExpansionTile extends StatelessWidget { title: Text("$title (${tasks.length})"), children: tasks .map( - (task) => GestureDetector( - onTap: () { - context.pushModal( - context: context, - builder: (context) => TaskBottomSheet(task: task, patient: task.patient), - ).then((_) { - try { - if(!context.mounted) { - return; - } - // TODO This widget is used in contexts without the AssignedTasksController leading to issues - AssignedTasksController controller = Provider.of(context, listen: false); - controller.load(); - } catch (e) { - if (kDebugMode) { - print(e); - } - } - }); + (task) => Dismissible( + key: Key(task.id ?? "undefined"), + confirmDismiss: (direction) async { + if (direction == DismissDirection.endToStart) { + onComplete(task); + } else { + onOpenEdit(task); + } + return false; }, + background: Padding( + padding: const EdgeInsets.all(paddingTiny), + child: Container( + decoration: BoxDecoration( + color: context.theme.colorScheme.primary, + borderRadius: BorderRadius.circular(borderRadiusMedium), + ), + padding: const EdgeInsets.symmetric(horizontal: paddingMedium), + child: Align( + alignment: Alignment.centerLeft, + child: Text( + context.localization.edit, + ), + ), + ), + ), + secondaryBackground: Padding( + padding: const EdgeInsets.all(paddingTiny), + child: Container( + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(borderRadiusSmall), + color: positiveColor, + ), + child: Align( + alignment: Alignment.centerRight, + child: Padding( + padding: const EdgeInsets.only(right: paddingMedium), + child: Text( + context.localization.completed, + )), + ), + ), + ), child: TaskCard( task: task, margin: const EdgeInsets.symmetric( horizontal: paddingSmall, vertical: paddingTiny, ), + onComplete: () => onComplete(task), + onTap: () => onOpenEdit(task), ), ), ) diff --git a/apps/tasks/lib/debug/theme_visualizer.dart b/apps/tasks/lib/debug/theme_visualizer.dart index 2460446d..0b04ac05 100644 --- a/apps/tasks/lib/debug/theme_visualizer.dart +++ b/apps/tasks/lib/debug/theme_visualizer.dart @@ -34,7 +34,7 @@ class ThemeVisualizer extends StatelessWidget { //SizedBox(width: 300, height: 400, child: SubtaskList(onChange: (_) {})), const Text("Text"), const Icon(Icons.ac_unit), - SubtaskList(onChange: (_){}), + SubtaskList(onChange: (_) {}), TextButton(onPressed: () {}, child: const Text("TextButton")), ElevatedButton(onPressed: () {}, child: const Text("ElevatedButton")), OutlinedButton(onPressed: () {}, child: const Text("OutlinedButton")), @@ -51,6 +51,8 @@ class ThemeVisualizer extends StatelessWidget { name: "Patient", ), ), + onComplete: () {}, + onTap: () {}, ) ], ), diff --git a/apps/tasks/lib/screens/main_screen_subscreens/my_tasks_screen.dart b/apps/tasks/lib/screens/main_screen_subscreens/my_tasks_screen.dart index 1e1942a2..755fa094 100644 --- a/apps/tasks/lib/screens/main_screen_subscreens/my_tasks_screen.dart +++ b/apps/tasks/lib/screens/main_screen_subscreens/my_tasks_screen.dart @@ -2,8 +2,11 @@ import 'package:flutter/material.dart'; import 'package:helpwave_service/tasks.dart'; import 'package:helpwave_theme/constants.dart'; import 'package:helpwave_theme/util.dart'; +import 'package:helpwave_widget/bottom_sheets.dart'; +import 'package:helpwave_widget/navigation.dart'; import 'package:provider/provider.dart'; import 'package:helpwave_localization/localization.dart'; +import 'package:tasks/components/bottom_sheet_pages/task_bottom_sheet.dart'; import 'package:tasks/components/task_expansion_tile.dart'; import 'package:helpwave_widget/loading.dart'; @@ -22,6 +25,14 @@ class _MyTasksScreenState extends State { create: (_) => AssignedTasksController(), child: Consumer( builder: (BuildContext context, AssignedTasksController tasksController, Widget? child) { + complete(Task task, AssignedTasksController controller) { + controller.updateTask(task.copyWith(status: TaskStatus.done)); + } + + openEdit(TaskWithPatient task) { + context.pushModal(context: context, builder: (context) => NavigationOutlet(initialValue: TaskBottomSheet(task: task, patient: task.patient))); + } + return LoadingAndErrorWidget( state: tasksController.state, child: ListView(children: [ @@ -37,16 +48,22 @@ class _MyTasksScreenState extends State { tasks: tasksController.todo, color: upcomingColor, title: context.localization.upcoming, + onComplete: (task) => complete(task,tasksController), + onOpenEdit: (task) => openEdit(task), ), TaskExpansionTile( tasks: tasksController.inProgress, color: inProgressColor, title: context.localization.inProgress, + onComplete: (task) => complete(task,tasksController), + onOpenEdit: (task) => openEdit(task), ), TaskExpansionTile( tasks: tasksController.done, color: doneColor, title: context.localization.done, + onComplete: (task) => complete(task,tasksController), + onOpenEdit: (task) => openEdit(task), ), ], ), diff --git a/apps/tasks/lib/screens/main_screen_subscreens/patient_screen.dart b/apps/tasks/lib/screens/main_screen_subscreens/patient_screen.dart index 1813292a..c517196f 100644 --- a/apps/tasks/lib/screens/main_screen_subscreens/patient_screen.dart +++ b/apps/tasks/lib/screens/main_screen_subscreens/patient_screen.dart @@ -4,6 +4,7 @@ import 'package:helpwave_theme/constants.dart'; import 'package:helpwave_theme/util.dart'; import 'package:helpwave_widget/bottom_sheets.dart'; import 'package:helpwave_widget/loading.dart'; +import 'package:helpwave_widget/navigation.dart'; import 'package:provider/provider.dart'; import 'package:tasks/components/bottom_sheet_pages/patient_bottom_sheet.dart'; import 'package:tasks/components/patient_card.dart'; @@ -133,7 +134,8 @@ class _PatientScreenState extends State { onClick: () => context .pushModal( context: context, - builder: (context) => PatientBottomSheet(patentId: patient.id!), + builder: (context) => + NavigationOutlet(initialValue: PatientBottomSheet(patentId: patient.id!)), ) .then((_) => patientController.load()), patient: patient, diff --git a/packages/helpwave_localization/lib/l10n/app_de.arb b/packages/helpwave_localization/lib/l10n/app_de.arb index 0eca992f..ed93da36 100644 --- a/packages/helpwave_localization/lib/l10n/app_de.arb +++ b/packages/helpwave_localization/lib/l10n/app_de.arb @@ -215,5 +215,8 @@ "description": "Beschreibung", "options": "Optionen", "editProperty": "Property ändern", - "createWard": "Station erstellen" + "createWard": "Station erstellen", + "edit": "Ändern", + "completed": "Fertig", + "status": "Status" } diff --git a/packages/helpwave_localization/lib/l10n/app_en.arb b/packages/helpwave_localization/lib/l10n/app_en.arb index d30f2dd4..c28fc1f5 100644 --- a/packages/helpwave_localization/lib/l10n/app_en.arb +++ b/packages/helpwave_localization/lib/l10n/app_en.arb @@ -215,5 +215,8 @@ "description": "Description", "options": "Options", "editProperty": "Edit Property", - "createWard": "Create Ward" + "createWard": "Create Ward", + "edit": "Edit", + "completed": "Completed", + "status": "Status" } diff --git a/packages/helpwave_service/lib/src/api/tasks/controllers/patient_controller.dart b/packages/helpwave_service/lib/src/api/tasks/controllers/patient_controller.dart index 41e76a5c..522f7910 100644 --- a/packages/helpwave_service/lib/src/api/tasks/controllers/patient_controller.dart +++ b/packages/helpwave_service/lib/src/api/tasks/controllers/patient_controller.dart @@ -123,6 +123,13 @@ class PatientController extends LoadingChangeNotifier { loadHandler(future: updateNotes()); } + Future updateTaskStatus(Task task) async { + // TODO handle errors better + await TaskService().updateTask(taskId: task.id!, status: task.status).then((id) { + load(); + }); + } + /// Creates the [Patient] Future create() async { createPatient() async { diff --git a/packages/helpwave_service/lib/src/api/tasks/controllers/task_controller.dart b/packages/helpwave_service/lib/src/api/tasks/controllers/task_controller.dart index e1404071..b4f5bf21 100644 --- a/packages/helpwave_service/lib/src/api/tasks/controllers/task_controller.dart +++ b/packages/helpwave_service/lib/src/api/tasks/controllers/task_controller.dart @@ -81,6 +81,20 @@ class TaskController extends LoadingChangeNotifier { loadHandler(future: updateName()); } + Future changeStatus(TaskStatus status) async { + if (isCreating) { + task = TaskWithPatient.fromTaskAndPatient(task: task.copyWith(status: status), patient: task.patient); + notifyListeners(); + return; + } + updateName() async { + await TaskService().updateTask(taskId: task.id!, status: status).then( + (_) => task = TaskWithPatient.fromTaskAndPatient(task: task.copyWith(status: status), patient: task.patient)); + } + + loadHandler(future: updateName()); + } + Future changeIsPublic(bool isPublic) async { if (isCreating) { task = TaskWithPatient.fromTaskAndPatient(task: task.copyWith(isPublicVisible: isPublic), patient: task.patient); diff --git a/packages/helpwave_service/lib/src/api/tasks/services/patient_svc.dart b/packages/helpwave_service/lib/src/api/tasks/services/patient_svc.dart index c0e09e08..fe58f0e9 100644 --- a/packages/helpwave_service/lib/src/api/tasks/services/patient_svc.dart +++ b/packages/helpwave_service/lib/src/api/tasks/services/patient_svc.dart @@ -91,7 +91,6 @@ class PatientService { metadata: TasksAPIServiceClients().getMetaData(), ), ); - print(response.tasks.length); return Patient( id: response.id, diff --git a/packages/helpwave_widget/lib/src/lists/add_list.dart b/packages/helpwave_widget/lib/src/lists/add_list.dart index 59251908..fc58212b 100644 --- a/packages/helpwave_widget/lib/src/lists/add_list.dart +++ b/packages/helpwave_widget/lib/src/lists/add_list.dart @@ -34,7 +34,7 @@ class AddList extends StatelessWidget { final List items; /// The builder function for the items of the list - final Widget? Function(BuildContext, int, T) itemBuilder; + final Widget Function(BuildContext, int, T) itemBuilder; /// The [title] of the the list final Widget title; @@ -54,7 +54,7 @@ class AddList extends StatelessWidget { required this.itemBuilder, required this.title, required this.onAdd, - this.maxHeight = 300, + this.maxHeight = double.infinity, this.addButtonBuilder, }); @@ -74,12 +74,8 @@ class AddList extends StatelessWidget { const SizedBox(height: distanceSmall), ConstrainedBox( constraints: BoxConstraints(maxHeight: maxHeight), - child: ListView.builder( - itemCount: items.length, - itemBuilder: (context, index) { - T item = items[index]; - return itemBuilder(context, index, item); - }, + child: Column( + children: items.indexed.map((e) => itemBuilder(context, e.$1, e.$2)).toList(), ), ), ],