From 816e60c52db4db22b24b482c63e3a755e92dfb30 Mon Sep 17 00:00:00 2001 From: Felix Thape Date: Sat, 4 Jan 2025 18:59:12 +0100 Subject: [PATCH 1/3] feat: use crud objects and adapt datatypes --- .../bottom_sheet_pages/assignee_select.dart | 2 +- .../organization_members_bottom_sheet.dart | 2 +- .../patient_bottom_sheet.dart | 153 +++++--- .../room_overview_bottom_sheet.dart | 10 +- .../rooms_bottom_sheet.dart | 6 +- .../bottom_sheet_pages/task_bottom_sheet.dart | 107 ++++-- apps/tasks/lib/components/patient_card.dart | 4 +- .../lib/components/patient_selector.dart | 2 +- apps/tasks/lib/components/subtask_list.dart | 2 +- apps/tasks/lib/components/task_card.dart | 9 +- .../lib/components/task_expansion_tile.dart | 10 +- apps/tasks/lib/debug/theme_visualizer.dart | 9 +- .../my_tasks_screen.dart | 6 +- .../src/api/offline/offline_client_store.dart | 25 +- .../controllers/property_controller.dart | 3 +- .../src/api/property/data_types/property.dart | 4 +- .../tasks/controllers/beds_controller.dart | 9 +- .../controllers/my_tasks_controller.dart | 12 +- .../tasks/controllers/patient_controller.dart | 165 ++++----- .../tasks/controllers/room_controller.dart | 85 +---- .../tasks/controllers/rooms_controller.dart | 27 +- .../controllers/subtask_list_controller.dart | 4 +- .../tasks/controllers/task_controller.dart | 194 +++------- .../controllers/ward_patients_controller.dart | 8 +- .../lib/src/api/tasks/data_types/bed.dart | 46 ++- .../lib/src/api/tasks/data_types/patient.dart | 190 +++++----- .../lib/src/api/tasks/data_types/room.dart | 67 ++-- .../lib/src/api/tasks/data_types/subtask.dart | 36 +- .../lib/src/api/tasks/data_types/task.dart | 195 +++++----- .../offline_clients/bed_offline_client.dart | 19 +- .../patient_offline_client.dart | 72 ++-- .../offline_clients/room_offline_client.dart | 117 +++--- .../offline_clients/task_offline_client.dart | 336 ++++++++---------- .../template_offline_client.dart | 2 +- .../offline_clients/ward_offline_client.dart | 19 +- .../lib/src/api/tasks/services/bed_svc.dart | 33 +- .../src/api/tasks/services/patient_svc.dart | 125 ++++--- .../lib/src/api/tasks/services/room_svc.dart | 49 +-- .../lib/src/api/tasks/services/task_svc.dart | 122 ++++--- .../controllers/organization_controller.dart | 26 +- .../api/user/controllers/user_controller.dart | 2 +- .../src/api/user/data_types/organization.dart | 5 +- .../lib/src/api/user/data_types/user.dart | 38 +- .../organization_offline_client.dart | 177 ++++++--- .../offline_clients/user_offline_client.dart | 51 +-- .../api/user/services/organization_svc.dart | 2 +- .../src/api/user/services/user_service.dart | 6 +- .../helpwave_service/lib/src/util/README.md | 31 ++ .../lib/src/util/crud_extension.dart | 69 ++++ .../lib/src/util/crud_object_interface.dart | 9 +- .../lib/src/util/identified_object.dart | 10 +- .../helpwave_service/lib/src/util/index.dart | 3 + .../lib/src/util/list_update.dart | 51 +++ .../lib/src/util/load_controller.dart | 58 +-- .../lib/src/util/loadable_crud_object.dart | 131 +++++++ 55 files changed, 1666 insertions(+), 1289 deletions(-) create mode 100644 packages/helpwave_service/lib/src/util/README.md create mode 100644 packages/helpwave_service/lib/src/util/crud_extension.dart create mode 100644 packages/helpwave_service/lib/src/util/list_update.dart create mode 100644 packages/helpwave_service/lib/src/util/loadable_crud_object.dart 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 447e3ed0..c2801a3c 100644 --- a/apps/tasks/lib/components/bottom_sheet_pages/assignee_select.dart +++ b/apps/tasks/lib/components/bottom_sheet_pages/assignee_select.dart @@ -40,7 +40,7 @@ class AssigneeSelectBottomSheet extends StatelessWidget { onTap: select, leading: CircleAvatar( foregroundColor: Colors.blue, backgroundImage: NetworkImage(user.profileUrl.toString())), - title: Text(user.nickName, + title: Text(user.nickname, style: TextStyle(decoration: user.id == selectedId ? TextDecoration.underline : null)), ), ), 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 fbe5d59a..172facfb 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 @@ -54,7 +54,7 @@ class OrganizationMembersBottomSheetPage extends StatelessWidget { subtitle: Text(member.email, style: TextStyle(color: context.theme.hintColor),), trailing: IconButton( onPressed: () { - controller.removeMember(organizationId: organizationId, userId: member.id); + controller.removeMember(organizationId: organizationId, userId: member.id!); }, icon: const Icon( Icons.remove, 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 a16520b7..23eb60a6 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 @@ -1,6 +1,7 @@ import 'package:flutter/material.dart'; import 'package:helpwave_localization/localization.dart'; import 'package:helpwave_service/auth.dart'; +import 'package:helpwave_service/util.dart'; import 'package:helpwave_theme/constants.dart'; import 'package:helpwave_theme/util.dart'; import 'package:helpwave_widget/bottom_sheets.dart'; @@ -27,16 +28,16 @@ class PatientBottomSheet extends StatefulWidget { } class _PatientBottomSheetState extends State { - Future> loadRoomsWithBeds({String? patientId}) async { - List rooms = - await RoomService().getRoomOverviews(wardId: CurrentWardService().currentWard!.wardId); + Future> loadRoomsWithBeds({String? patientId}) async { + List rooms = await RoomService() + .getRoomOverviews(wardId: CurrentWardService().currentWard!.wardId); - List flattenedRooms = []; - for (RoomWithBedWithMinimalPatient room in rooms) { - for (Bed bed in room.beds) { + List flattenedRooms = []; + for (Room room in rooms) { + 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)); + flattenedRooms.add(RoomAndBed(room: room, bed: bed)); } } } @@ -54,11 +55,14 @@ class _PatientBottomSheetState extends State { ], child: BottomSheetPage( header: BottomSheetHeader( - title: Consumer(builder: (context, patientController, _) { - if (patientController.state == LoadingState.loaded || patientController.isCreating) { + title: Consumer( + builder: (context, patientController, _) { + if (patientController.state == LoadingState.loaded || + patientController.isCreating) { return ClickableTextEdit( - initialValue: patientController.patient.name, - onUpdated: patientController.changeName, + initialValue: patientController.data.name, + onUpdated: (value) => + patientController.update(PatientUpdate(name: value)), textAlign: TextAlign.center, textStyle: TextStyle( color: context.theme.colorScheme.primary, @@ -75,12 +79,14 @@ class _PatientBottomSheetState extends State { ), bottom: Padding( padding: const EdgeInsets.symmetric(vertical: paddingSmall), - child: Consumer(builder: (context, patientController, _) { + child: Consumer( + builder: (context, patientController, _) { return LoadingAndErrorWidget( state: patientController.state, child: Row( - mainAxisAlignment: - patientController.isCreating ? MainAxisAlignment.end : MainAxisAlignment.spaceBetween, + mainAxisAlignment: patientController.isCreating + ? MainAxisAlignment.end + : MainAxisAlignment.spaceBetween, children: patientController.isCreating ? [ FilledButton( @@ -93,17 +99,19 @@ class _PatientBottomSheetState extends State { width: width * 0.4, // TODO make this state checking easier and more readable child: FilledButton( - onPressed: patientController.patient.isNotAssignedToBed + onPressed: patientController.data.isNotAssignedToBed ? null : () { patientController.unassign(); }, style: buttonStyleMedium.copyWith( - backgroundColor: resolveByStatesAndContextBackground( + backgroundColor: + resolveByStatesAndContextBackground( context: context, defaultValue: inProgressColor, ), - foregroundColor: resolveByStatesAndContextForeground( + foregroundColor: + resolveByStatesAndContextForeground( context: context, ), ), @@ -114,13 +122,14 @@ class _PatientBottomSheetState extends State { width: width * 0.4, child: FilledButton( // TODO check whether the patient is active - onPressed: patientController.patient.isDischarged + onPressed: patientController.data.isDischarged ? null : () { showDialog( context: context, - builder: (context) => - AcceptDialog(titleText: context.localization.dischargePatient), + builder: (context) => AcceptDialog( + titleText: context + .localization.dischargePatient), ).then((value) { if (value) { patientController.discharge(); @@ -131,11 +140,13 @@ class _PatientBottomSheetState extends State { }); }, style: buttonStyleMedium.copyWith( - backgroundColor: resolveByStatesAndContextBackground( + backgroundColor: + resolveByStatesAndContextBackground( context: context, defaultValue: negativeColor, ), - foregroundColor: resolveByStatesAndContextForeground( + foregroundColor: + resolveByStatesAndContextForeground( context: context, ), ), @@ -151,42 +162,57 @@ class _PatientBottomSheetState extends State { child: ListView( children: [ Center( - child: Consumer(builder: (context, patientController, _) { + child: Consumer( + builder: (context, patientController, _) { return LoadingFutureBuilder( - future: loadRoomsWithBeds(patientId: patientController.patient.id), - loadingWidget: const PulsingContainer(width: 80, height: 20), + future: + loadRoomsWithBeds(patientId: patientController.data.id), + loadingWidget: + const PulsingContainer(width: 80, height: 20), thenBuilder: (context, beds) { if (beds.isEmpty) { return Text( context.localization.noFreeBeds, - style: TextStyle(color: context.theme.disabledColor, fontWeight: FontWeight.bold), + style: TextStyle( + color: context.theme.disabledColor, + fontWeight: FontWeight.bold), ); } return DropdownButtonHideUnderline( - child: DropdownButton( - iconEnabledColor: context.theme.colorScheme.primary.withOpacity(0.6), + child: DropdownButton( + iconEnabledColor: context.theme.colorScheme.primary + .withOpacity(0.6), padding: EdgeInsets.zero, isDense: true, hint: Text( context.localization.assignBed, - style: TextStyle(color: context.theme.colorScheme.primary.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, + value: beds + .where((beds) => + beds.bed.id == patientController.data.bed.id) + .firstOrNull, items: beds .map((roomWithBed) => DropdownMenuItem( value: roomWithBed, child: Text( "${roomWithBed.room.name} - ${roomWithBed.bed.name}", - style: TextStyle(color: context.theme.colorScheme.primary.withOpacity(0.6)), + style: TextStyle( + color: context + .theme.colorScheme.primary + .withOpacity(0.6)), ), )) .toList(), - onChanged: (RoomWithBedFlat? value) { + onChanged: (RoomAndBed? value) { // TODO later unassign here if (value == null) { return; } - patientController.assignToBed(value.room, value.bed); + patientController.assignToBed( + value.room, value.bed); }, ), ); @@ -196,23 +222,27 @@ class _PatientBottomSheetState extends State { ), Text( context.localization.notes, - style: const TextStyle(fontSize: fontSizeBig, fontWeight: FontWeight.bold), + style: const TextStyle( + fontSize: fontSizeBig, fontWeight: FontWeight.bold), ), const SizedBox(height: distanceSmall), Consumer( builder: (context, patientController, _) => - patientController.state == LoadingState.loaded || patientController.isCreating + patientController.state == LoadingState.loaded || + patientController.isCreating ? TextFormFieldWithTimer( - initialValue: patientController.patient.notes, + initialValue: patientController.data.notes, maxLines: 6, - onUpdate: patientController.changeNotes, + onUpdate: (value) => patientController + .update(PatientUpdate(notes: value)), ) : TextFormField(maxLines: 6), ), Padding( padding: const EdgeInsets.symmetric(vertical: paddingMedium), - child: Consumer(builder: (context, patientController, _) { - Patient patient = patientController.patient; + child: Consumer( + builder: (context, patientController, _) { + Patient patient = patientController.data; return AddList( items: [ patient.unscheduledTasks, @@ -222,47 +252,52 @@ class _PatientBottomSheetState extends State { itemBuilder: (_, index, taskList) { // TODO after return from navigation reload complete(Task task, PatientController controller) { - controller.updateTaskStatus(task.copyWith(status: TaskStatus.done)); + controller.updateTaskStatus( + task.copyWith(TaskCopyWithUpdate(status: TaskStatus.done))); } - openEdit(TaskWithPatient task) { - NavigationStackController.of(context).push(TaskBottomSheet(task: task, patient: task.patient)); + openEdit(Task task) { + NavigationStackController.of(context).push( + TaskBottomSheet( + task: task, patient: task.patient.data)); } if (index == 0) { return TaskExpansionTile( tasks: patient.unscheduledTasks - .map((task) => TaskWithPatient.fromTaskAndPatient( - task: task, - patient: patient, - )) + .map((task) => task.copyWith(TaskCopyWithUpdate( + patient: LoadableCRUDObjectUpdate( + overwrite: patient, + )))) .toList(), title: context.localization.upcoming, color: upcomingColor, onOpenEdit: (task) => openEdit(task), - onComplete: (task) => complete(task, patientController), + onComplete: (task) => + complete(task, patientController), ); } if (index == 2) { return TaskExpansionTile( tasks: patient.doneTasks - .map((task) => TaskWithPatient.fromTaskAndPatient( - task: task, - patient: patient, - )) + .map((task) => task.copyWith(TaskCopyWithUpdate( + patient: LoadableCRUDObjectUpdate( + overwrite: patient, + )))) .toList(), title: context.localization.inProgress, color: inProgressColor, onOpenEdit: (task) => openEdit(task), - onComplete: (task) => complete(task, patientController), + onComplete: (task) => + complete(task, patientController), ); } return TaskExpansionTile( tasks: patient.inProgressTasks - .map((task) => TaskWithPatient.fromTaskAndPatient( - task: task, - patient: patient, - )) + .map((task) => task.copyWith(TaskCopyWithUpdate( + patient: LoadableCRUDObjectUpdate( + overwrite: patient, + )))) .toList(), title: context.localization.done, color: doneColor, @@ -272,12 +307,14 @@ class _PatientBottomSheetState extends State { }, title: Text( context.localization.tasks, - style: const TextStyle(fontSize: fontSizeBig, fontWeight: FontWeight.bold), + 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), + builder: (context) => TaskBottomSheet( + task: Task.empty(patient.id), patient: patient), ), ); }), 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 9f259c25..60b14dd1 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 @@ -20,7 +20,7 @@ class RoomOverviewBottomSheetPage extends StatelessWidget { return MultiProvider( providers: [ ChangeNotifierProvider(create: (context) => BedsController(roomId: roomId)), - ChangeNotifierProvider(create: (context) => RoomController(roomId: roomId)), + ChangeNotifierProvider(create: (context) => RoomController(id: roomId)), ], child: BottomSheetPage( header: BottomSheetHeader.navigation( @@ -33,7 +33,7 @@ class RoomOverviewBottomSheetPage extends StatelessWidget { return LoadingAndErrorWidget( state: controller.state, loadingWidget: const PulsingContainer(width: 80), - child: Text(controller.room.name, style: TextStyle(color: context.theme.hintColor)), + child: Text(controller.data.name, style: TextStyle(color: context.theme.hintColor)), ); }), ], @@ -54,8 +54,8 @@ class RoomOverviewBottomSheetPage extends StatelessWidget { 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), + initialValue: controller.state == LoadingState.loaded ? controller.data.name : "", + onUpdate: (value) => controller.update(RoomUpdate(name: value)), ), ], ), @@ -74,7 +74,7 @@ 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(name: context.localization.newBed, roomId: roomId)) }, child: Text("+ ${context.localization.add} ${context.localization.bed}"), ), 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 2c3bec0e..3aae7592 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 @@ -41,7 +41,7 @@ class RoomsBottomSheetPage extends StatelessWidget { icon: Icons.add, onPressed: () { controller - .create(RoomWithBedWithMinimalPatient(id: "", name: context.localization.newRoom, beds: [])); + .create(Room(name: context.localization.newRoom, beds: [])); }, ), ), @@ -57,10 +57,10 @@ 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)); + .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 5cd5d118..d0faf84c 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 @@ -2,6 +2,7 @@ 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_service/util.dart'; import 'package:helpwave_theme/constants.dart'; import 'package:helpwave_theme/util.dart'; import 'package:helpwave_util/loading.dart'; @@ -13,7 +14,6 @@ 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 @@ -46,7 +46,8 @@ class _SheetListTile extends StatelessWidget { required this.icon, this.onTap, }) : assert( - (valueWidget == null && valueText != null) || (valueWidget != null && valueText == null), + (valueWidget == null && valueText != null) || + (valueWidget != null && valueText == null), "Exactly one of parameter1 or parameter2 should be provided.", ); @@ -105,7 +106,7 @@ class TaskBottomSheet extends StatefulWidget { /// /// Not providing a [patient] means creating a new task /// for which the [patient] must be chosen - final PatientMinimal? patient; + final Patient? patient; const TaskBottomSheet({super.key, required this.task, this.patient}); @@ -123,8 +124,13 @@ class _TaskBottomSheetState extends State { }; return ChangeNotifierProvider( - create: (context) => - TaskController(TaskWithPatient.fromTaskAndPatient(task: widget.task, patient: widget.patient)), + create: (context) => TaskController( + task: widget.task.copyWith( + TaskCopyWithUpdate( + patient: LoadableCRUDObjectUpdate(overwrite: widget.patient), + ), + ), + ), child: BottomSheetPage( header: BottomSheetHeader( title: Consumer( @@ -132,8 +138,9 @@ class _TaskBottomSheetState extends State { state: taskController.state, loadingWidget: const PulsingContainer(width: 60), child: ClickableTextEdit( - initialValue: taskController.task.name, - onUpdated: taskController.changeName, + initialValue: taskController.data.name, + onUpdated: (value) => + taskController.update(TaskCopyWithUpdate(name: value)), textAlign: TextAlign.center, textStyle: TextStyle( color: context.theme.colorScheme.primary, @@ -158,7 +165,7 @@ class _TaskBottomSheetState extends State { onPressed: taskController.isReadyForCreate ? () { taskController.create().then((value) { - if (value && context.mounted) { + if (context.mounted) { Navigator.pop(context); } }); @@ -197,26 +204,31 @@ class _TaskBottomSheetState extends State { Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ - Consumer(builder: (context, taskController, __) { + 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), + builder: (BuildContext context) => + AssigneeSelectBottomSheet( + users: OrganizationService().getMembersByOrganization( + CurrentWardService().currentWard!.organizationId), onChanged: (User? assignee) { taskController.changeAssignee(assignee); Navigator.pop(context); }, - selectedId: taskController.task.assigneeId, + selectedId: taskController.data.assignee.id, ), ), - valueWidget: taskController.task.hasAssignee + valueWidget: taskController.data.hasAssignee ? LoadingAndErrorWidget( - state: taskController.assignee != null ? LoadingState.loaded : LoadingState.loading, - loadingWidget: const PulsingContainer(width: 60, height: 24), + 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 @@ -231,15 +243,17 @@ class _TaskBottomSheetState extends State { ); }), Consumer( - builder: (context, taskController, __) => LoadingAndErrorWidget( + builder: (context, taskController, __) => + LoadingAndErrorWidget( state: taskController.state, - loadingWidget: const PulsingContainer(width: 60, height: 24), + loadingWidget: + const PulsingContainer(width: 60, height: 24), 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; + DateTime? dueDate = taskController.data.dueDate; if (dueDate != null) { String date = "${dueDate.day.toString().padLeft(2, "0")}.${dueDate.month.toString().padLeft(2, "0")}.${dueDate.year.toString().padLeft(4, "0")}"; @@ -249,7 +263,8 @@ class _TaskBottomSheetState extends State { mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, children: [ - Text(time, style: editableValueTextStyle(context)), + Text(time, + style: editableValueTextStyle(context)), Text(date), ], ); @@ -258,18 +273,24 @@ class _TaskBottomSheetState extends State { }), onTap: () => showDatePicker( context: context, - initialDate: taskController.task.dueDate ?? DateTime.now(), + initialDate: + taskController.data.dueDate ?? DateTime.now(), firstDate: DateTime(1960), - lastDate: DateTime.now().add(const Duration(days: 365 * 5)), + lastDate: + DateTime.now().add(const Duration(days: 365 * 5)), ).then((date) async { + if (!context.mounted) { + return; + } await showTimePicker( context: context, - initialTime: TimeOfDay.fromDateTime(taskController.task.dueDate ?? DateTime.now()), + initialTime: TimeOfDay.fromDateTime( + taskController.data.dueDate ?? DateTime.now()), ).then((time) { if (date == null && time == null) { return; } - date ??= taskController.task.dueDate; + date ??= taskController.data.dueDate; if (date == null) { return; } @@ -282,7 +303,8 @@ class _TaskBottomSheetState extends State { time.minute, ); } - taskController.changeDueDate(date); + taskController + .update(TaskCopyWithUpdate(dueDate: date)); }); }), ), @@ -295,14 +317,16 @@ class _TaskBottomSheetState extends State { mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Consumer( - builder: (_, taskController, __) => LoadingAndErrorWidget.pulsing( + 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, + isPublicVisible: taskController.data.isPublicVisible, + onChanged: (isPublic) => taskController.update( + TaskCopyWithUpdate(isPublicVisible: isPublic)), isCreating: taskController.isCreating, textStyle: editableValueTextStyle(context), ), @@ -310,17 +334,21 @@ class _TaskBottomSheetState extends State { ), ), Consumer( - builder: (_, taskController, __) => LoadingAndErrorWidget.pulsing( + builder: (_, taskController, __) => + LoadingAndErrorWidget.pulsing( state: taskController.state, child: _SheetListTile( icon: Icons.check, label: context.localization.status, valueWidget: Text( - taskStatusTranslationMap[taskController.task.status] ?? "", + taskStatusTranslationMap[ + taskController.data.status] ?? + "", style: editableValueTextStyle(context), ), // TODO show modal here - onTap: () => taskController.changeStatus(TaskStatus.done), + onTap: () => taskController.update( + TaskCopyWithUpdate(status: TaskStatus.done)), ), ), ), @@ -329,7 +357,8 @@ class _TaskBottomSheetState extends State { const SizedBox(height: distanceMedium), Text( context.localization.notes, - style: const TextStyle(fontWeight: FontWeight.bold, fontSize: 16), + style: + const TextStyle(fontWeight: FontWeight.bold, fontSize: 16), ), const SizedBox(height: distanceTiny), Consumer( @@ -348,8 +377,9 @@ class _TaskBottomSheetState extends State { color: negativeColor, ), child: TextFormFieldWithTimer( - initialValue: taskController.task.notes, - onUpdate: taskController.changeNotes, + initialValue: taskController.data.notes, + onUpdate: (value) => + taskController.update(TaskCopyWithUpdate(notes: value)), maxLines: 6, decoration: InputDecoration( contentPadding: const EdgeInsets.all(paddingMedium), @@ -365,11 +395,12 @@ class _TaskBottomSheetState extends State { state: taskController.state, loadingWidget: const PulsingContainer(height: 200), child: SubtaskList( - taskId: taskController.task.id, - subtasks: taskController.task.subtasks, + taskId: taskController.data.id, + subtasks: taskController.data.subtasks ?? [], onChange: (subtasks) { - if (taskController.task.isCreating) { - taskController.task.subtasks = subtasks; + if (taskController.data.isCreating) { + taskController.update(TaskCopyWithUpdate( + subtasks: ListUpdate(overwrite: subtasks))); } else { taskController.load(); } diff --git a/apps/tasks/lib/components/patient_card.dart b/apps/tasks/lib/components/patient_card.dart index cd4500b5..0be1cd18 100644 --- a/apps/tasks/lib/components/patient_card.dart +++ b/apps/tasks/lib/components/patient_card.dart @@ -44,8 +44,8 @@ class PatientCard extends StatelessWidget { ), ), Text( - patient.bed != null && patient.room != null - ? "${patient.room?.name} - ${patient.bed?.name}" + patient.bed.hasDataValue && patient.room.hasDataValue + ? "${patient.room.data?.name} - ${patient.bed.data?.name}" : 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 df012d6a..e7577645 100644 --- a/apps/tasks/lib/components/patient_selector.dart +++ b/apps/tasks/lib/components/patient_selector.dart @@ -6,7 +6,7 @@ import 'package:helpwave_widget/loading.dart'; class PatientSelector extends StatelessWidget { final String? initialPatientId; - final void Function(PatientMinimal? value) onChange; + final void Function(Patient? value) onChange; const PatientSelector({super.key, this.initialPatientId, required this.onChange}); diff --git a/apps/tasks/lib/components/subtask_list.dart b/apps/tasks/lib/components/subtask_list.dart index 2a6d574c..a832244d 100644 --- a/apps/tasks/lib/components/subtask_list.dart +++ b/apps/tasks/lib/components/subtask_list.dart @@ -67,7 +67,7 @@ class SubtaskList extends StatelessWidget { borderRadius: BorderRadius.circular(iconSizeSmall), ), onChanged: (isDone) => subtasksController - .update(subtask: subtask.copyWith(isDone: isDone)) + .update(subtask: subtask.copyWith(SubtaskUpdate(isDone: isDone))) .then((value) => onChange(subtasksController.subtasks)), ), trailing: GestureDetector( diff --git a/apps/tasks/lib/components/task_card.dart b/apps/tasks/lib/components/task_card.dart index c2d563b1..a585511c 100644 --- a/apps/tasks/lib/components/task_card.dart +++ b/apps/tasks/lib/components/task_card.dart @@ -8,7 +8,7 @@ import 'package:helpwave_widget/static_progress_indicator.dart'; /// A [Card] showing a [Task]'s information class TaskCard extends StatelessWidget { /// The [Task] used to display the information - final TaskWithPatient task; + final Task task; /// The [margin] of the [Card] final EdgeInsetsGeometry? margin; @@ -22,14 +22,15 @@ class TaskCard extends StatelessWidget { /// The [Function] called when the [Task] should be edited final Function() onTap; - const TaskCard({ + // TODO make the card use separate Task and Patient Objects in order to make this class constant + TaskCard({ super.key, required this.task, this.margin, this.borderRadius = borderRadiusMedium, required this.onComplete, required this.onTap, - }); + }) : assert(task.patient.hasDataValue, "Ensure the patient on the task object is properly loaded"); /// Determines the text shown for indicating the remaining time for the [Task] String getDueText(BuildContext context) { @@ -126,7 +127,7 @@ class TaskCard extends StatelessWidget { crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( - task.patient.name, + task.patient.data!.name, style: TextStyle(color: context.theme.colorScheme.primary), ), Text( diff --git a/apps/tasks/lib/components/task_expansion_tile.dart b/apps/tasks/lib/components/task_expansion_tile.dart index 1bac8b08..c1ab1127 100644 --- a/apps/tasks/lib/components/task_expansion_tile.dart +++ b/apps/tasks/lib/components/task_expansion_tile.dart @@ -12,7 +12,7 @@ import 'package:helpwave_service/tasks.dart'; /// and can be clicked to show the tasks class TaskExpansionTile extends StatelessWidget { /// The [List] of [Task]s - final List tasks; + final List tasks; /// The [Color] of the leading dot final Color color; @@ -23,11 +23,11 @@ class TaskExpansionTile extends StatelessWidget { /// The [title] of the Tile final String title; - /// The [Function] called when the [TaskWithPatient] should be completed - final Function(TaskWithPatient task) onComplete; + /// The [Function] called when the [Task] should be completed + final Function(Task task) onComplete; - /// The [Function] called when the [TaskWithPatient] should be edited - final Function(TaskWithPatient task) onOpenEdit; + /// The [Function] called when the [Task] should be edited + final Function(Task task) onOpenEdit; const TaskExpansionTile({ super.key, diff --git a/apps/tasks/lib/debug/theme_visualizer.dart b/apps/tasks/lib/debug/theme_visualizer.dart index 0b04ac05..6eb952e1 100644 --- a/apps/tasks/lib/debug/theme_visualizer.dart +++ b/apps/tasks/lib/debug/theme_visualizer.dart @@ -1,5 +1,6 @@ import 'package:flutter/material.dart'; import 'package:helpwave_localization/localization.dart'; +import 'package:helpwave_service/util.dart'; import 'package:helpwave_theme/theme.dart'; import 'package:helpwave_widget/loading.dart'; import 'package:provider/provider.dart'; @@ -39,17 +40,13 @@ class ThemeVisualizer extends StatelessWidget { ElevatedButton(onPressed: () {}, child: const Text("ElevatedButton")), OutlinedButton(onPressed: () {}, child: const Text("OutlinedButton")), TaskCard( - task: TaskWithPatient( + task: Task( id: 'task', name: "Task", notes: "Some Notes", status: TaskStatus.inProgress, - patientId: "patient", + patient: LoadableCRUDObject(data: Patient(name: "patient", isDischarged: false)), dueDate: DateTime.now().add(const Duration(hours: 2)), - patient: PatientMinimal( - id: "patient", - 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 755fa094..0287e2f7 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 @@ -26,11 +26,11 @@ class _MyTasksScreenState extends State { child: Consumer( builder: (BuildContext context, AssignedTasksController tasksController, Widget? child) { complete(Task task, AssignedTasksController controller) { - controller.updateTask(task.copyWith(status: TaskStatus.done)); + controller.updateTask(task.copyWith(TaskCopyWithUpdate(status: TaskStatus.done))); } - openEdit(TaskWithPatient task) { - context.pushModal(context: context, builder: (context) => NavigationOutlet(initialValue: TaskBottomSheet(task: task, patient: task.patient))); + openEdit(Task task) { + context.pushModal(context: context, builder: (context) => NavigationOutlet(initialValue: TaskBottomSheet(task: task, patient: task.patient.data))); } return LoadingAndErrorWidget( 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 20609cf2..4f1c1fae 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 @@ -8,6 +8,7 @@ import 'package:helpwave_service/src/api/tasks/offline_clients/ward_offline_clie 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_service/src/util/index.dart'; import 'package:helpwave_util/lists.dart'; import '../../../user.dart'; @@ -30,35 +31,35 @@ final List initialUsers = [ User( id: "user1", name: "Testine Test", - nickName: "Testine", + nickname: "Testine", email: "test@helpwave.de", profileUrl: Uri.parse(profileUrl), ), User( id: "user2", name: "Peter Pete", - nickName: "Peter", + nickname: "Peter", email: "test@helpwave.de", profileUrl: Uri.parse(profileUrl), ), User( id: "user3", name: "John Doe", - nickName: "John", + nickname: "John", email: "test@helpwave.de", profileUrl: Uri.parse(profileUrl), ), User( id: "user4", name: "Walter White", - nickName: "Walter", + nickname: "Walter", email: "test@helpwave.de", profileUrl: Uri.parse(profileUrl), ), User( id: "user5", name: "Peter Parker", - nickName: "Parker", + nickname: "Parker", email: "test@helpwave.de", profileUrl: Uri.parse(profileUrl), ), @@ -68,9 +69,9 @@ final List initialWards = initialOrganizations Ward(id: "${organization.id}${index + 1}", name: "Ward ${index + 1}", organizationId: organization.id!))) .expand((element) => element) .toList(); -final List initialRooms = initialWards +final List initialRooms = initialWards .map((ward) => range(0, 2) - .map((index) => RoomWithWardId(id: "${ward.id}${index + 1}", name: "Room ${index + 1}", wardId: ward.id))) + .map((index) => Room(id: "${ward.id}${index + 1}", name: "Room ${index + 1}", wardId: ward.id))) .expand((element) => element) .toList(); final List initialBeds = initialRooms @@ -78,22 +79,22 @@ final List initialBeds = initialRooms 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 - .map((e) => PatientWithBedId( +final List initialPatients = initialBeds.indexed + .map((e) => Patient( id: "patient${e.$1}", name: "Patient ${e.$1 + 1}", notes: "", isDischarged: e.$1 % 6 == 0, - bedId: e.$1 % 2 == 0 ? e.$2.id : null, + bed: LoadableCRUDObject(id: 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, + patient: LoadableCRUDObject(id: patient.id), notes: '', - assigneeId: [initialUsers[0].id, null, initialUsers[2].id][index], + assignee: LoadableCRUDObject(id: [initialUsers[0].id, null, initialUsers[2].id][index]), ))) .expand((element) => element) .toList(); 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 2f3c8bce..27e0db7c 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 @@ -2,7 +2,8 @@ import 'package:helpwave_service/property.dart'; import 'package:helpwave_service/util.dart'; /// The Controller for managing [Property]s in a Ward -class PropertyController extends LoadController { +class PropertyController extends LoadController { PropertyController({super.id, Property? property}) : super(initialData: property, service: PropertyService()); 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 5cd3d9f0..02713ab8 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 @@ -32,7 +32,7 @@ class PropertyUpdate { }); } -class Property extends CRUDObject { +class Property extends CRUDObject { final String name; final String description; final PropertySubjectType subjectType; @@ -91,7 +91,7 @@ class Property extends CRUDObject { } @override - Property create(String? id) { + Property attachId(String? id) { return copyWith(PropertyUpdate(id: 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 27bf25ab..db3d4465 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 @@ -1,5 +1,6 @@ import 'dart:async'; import 'package:helpwave_service/src/api/tasks/index.dart'; +import 'package:helpwave_service/src/util/identified_object.dart'; import 'package:helpwave_util/loading.dart'; /// The Controller for managing [Bed]s in a [Room] @@ -48,14 +49,14 @@ class BedsController extends LoadingChangeNotifier { notifyListeners(); return; } - await delete(beds[index].id); + 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) { + await BedService().delete(id).then((value) { if (value) { int index = _beds.indexWhere((element) => element.id == id); if (index != -1) { @@ -76,7 +77,7 @@ class BedsController extends LoadingChangeNotifier { return; } createSubtask() async { - await BedService().create(roomId: roomId!, name: bed.name).then((value) { + await BedService().create(bed.copyWith(BedUpdate(roomId: roomId))).then((value) { _beds.add(bed); }); } @@ -95,7 +96,7 @@ class BedsController extends LoadingChangeNotifier { } 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); + await BedService().update(bed.id!, BedUpdate(name: bed.name)); int index = beds.indexWhere((element) => element.id == bed.id); if (index != -1) { beds[index] = bed; 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 564b55db..618b2a1a 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 @@ -5,19 +5,19 @@ import 'package:helpwave_util/loading.dart'; /// The Controller for [Task]s of the current [User] class AssignedTasksController extends LoadingChangeNotifier { /// The currently loaded [Task]s - List _tasks = []; + List _tasks = []; /// The currently loaded [Task]s - List get tasks => _tasks; + List get tasks => _tasks; /// The loaded [Task]s which have [TaskStatus.todo] - List get todo => _tasks.where((element) => element.status == TaskStatus.todo).toList(); + List get todo => _tasks.where((element) => element.status == TaskStatus.todo).toList(); /// The loaded [Task]s which have [TaskStatus.inProgress] - List get inProgress => _tasks.where((element) => element.status == TaskStatus.inProgress).toList(); + List get inProgress => _tasks.where((element) => element.status == TaskStatus.inProgress).toList(); /// The loaded [Task]s which have [TaskStatus.done] - List get done => _tasks.where((element) => element.status == TaskStatus.done).toList(); + List get done => _tasks.where((element) => element.status == TaskStatus.done).toList(); AssignedTasksController() { load(); @@ -36,7 +36,7 @@ class AssignedTasksController extends LoadingChangeNotifier { Future updateTask(Task task) async { assert(!task.isCreating); loadTasksFuture() async { - await TaskService().updateTask(taskId: task.id!, status: task.status); + await TaskService().update(task.id!, TaskUpdate(status: task.status)); _tasks = await TaskService().getAssignedTasks(); } 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 522f7910..804db70a 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,61 +1,32 @@ -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 { - /// The current [Patient] - Patient _patient = Patient.empty(); - - /// The current [Patient] - Patient get patient => _patient; - - set patient(Patient value) { - _patient = value; - changeState(LoadingState.loaded); - } - - /// Is the current [Patient] already saved on the server or are we creating? - get isCreating => _patient.isCreating; - - 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 = _patient.copyWith(id: id); - } - load(); - } - - /// A function to load the [Patient] - Future load() async { - loadPatient() async { - if (isCreating) { - return; - } - patient = await PatientService().getPatientDetails(patientId: patient.id!); - } - - loadHandler( - future: loadPatient(), - ); - } +class PatientController + extends LoadController { + PatientController({super.id, Patient? patient}) + : super(initialData: patient, service: PatientService()); /// Unassigns the [Patient] from their [Bed] Future unassign() async { unassignPatient() async { - if(patient.isCreating) { - final patientCopy = patient.copyWith(); - patientCopy.bed = null; - patientCopy.room = null; - _patient = patientCopy; + if (data.isCreating) { + changeData( + data.copyWith(PatientUpdate( + bed: LoadableCRUDObjectUpdate(remove: true, removeLoadedData: true), + room: LoadableCRUDObjectUpdate(remove: true, removeLoadedData: true), + )), + isNotifying: false, + ); } - await PatientService().unassignPatient(patientId: patient.id!).then((value) { - final patientCopy = patient.copyWith(); - patientCopy.bed = null; - patientCopy.room = null; - _patient = patientCopy; + await PatientService().unassignPatient(patientId: data.id!).then((value) { + changeData( + data.copyWith(PatientUpdate( + bed: LoadableCRUDObjectUpdate(remove: true, removeLoadedData: true), + room: LoadableCRUDObjectUpdate(remove: true, removeLoadedData: true), + )), + isNotifying: false, + ); }); } @@ -64,13 +35,19 @@ class PatientController extends LoadingChangeNotifier { /// Discharges the [Patient] Future discharge() async { - assert(!patient.isCreating, "You can only discharge created patients"); + assert(!isCreating, "You can only discharge created patients"); dischargePatient() async { - await PatientService().dischargePatient(patientId: patient.id!).then((value) { - final patientCopy = patient.copyWith(isDischarged: true); - patientCopy.bed = null; - patientCopy.room = null; - patient = patientCopy; + await PatientService() + .dischargePatient(patientId: data.id!) + .then((value) { + changeData( + data.copyWith(PatientUpdate( + isDischarged: true, + bed: LoadableCRUDObjectUpdate(remove: true, removeLoadedData: true), + room: LoadableCRUDObjectUpdate(remove: true, removeLoadedData: true), + )), + isNotifying: false, + ); }); } @@ -78,69 +55,53 @@ class PatientController extends LoadingChangeNotifier { } /// Assigns the [Patient] to a [Bed] and [Room] - Future assignToBed(RoomMinimal room, BedMinimal bed) async { + Future assignToBed(Room room, Bed bed) async { assignPatientToBed() async { if (isCreating) { - patient.room = room; - patient.bed = bed; - return; + changeData( + data.copyWith(PatientUpdate( + bed: LoadableCRUDObjectUpdate(overwrite: bed), + room: LoadableCRUDObjectUpdate(overwrite: room), + )), + isNotifying: false, + ); } - await PatientService().assignBed(patientId: patient.id!, bedId: bed.id).then((value) { - patient = patient.copyWith(bed: bed, room: room); + await PatientService() + .assignBed(patientId: data.id!, bedId: bed.id!) + .then((value) { + changeData( + data.copyWith(PatientUpdate( + bed: LoadableCRUDObjectUpdate(overwrite: bed), + room: LoadableCRUDObjectUpdate(overwrite: room), + )), + isNotifying: false, + ); }); } loadHandler(future: assignPatientToBed()); } - /// Change the name of the [Patient] - Future changeName(String name) async { - if (isCreating) { - patient.name = name; - return; - } - updateName() async { - await PatientService().updatePatient(id: patient.id!, name: name).then((_) { - patient.name = name; - }); - } - - loadHandler(future: updateName()); - } - - /// Change the notes of the [Patient] - Future changeNotes(String notes) async { - if (isCreating) { - patient.notes = notes; - return; - } - updateNotes() async { - await PatientService().updatePatient(id: patient.id!, notes: notes).then((_) { - patient.notes = notes; - }); - } - - loadHandler(future: updateNotes()); - } - Future updateTaskStatus(Task task) async { // TODO handle errors better - await TaskService().updateTask(taskId: task.id!, status: task.status).then((id) { + await TaskService() + .update(task.id!, TaskUpdate(status: task.status)) + .then((id) { load(); }); } /// 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!); - } + @override + Future createOp() async { + await PatientService().create(data).then((value) { + changeData(value, isNotifying: false); + }); + if (data.isActive) { + await assignToBed(data.room.data!, data.bed.data!); } - - loadHandler(future: createPatient()); } + + @override + defaultData() => Patient.empty(); } 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 index 3c7184a9..013b2bef 100644 --- a/packages/helpwave_service/lib/src/api/tasks/controllers/room_controller.dart +++ b/packages/helpwave_service/lib/src/api/tasks/controllers/room_controller.dart @@ -1,89 +1,14 @@ -import 'dart:async'; import 'package:helpwave_service/src/api/tasks/index.dart'; -import 'package:helpwave_util/loading.dart'; +import 'package:helpwave_service/src/util/index.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; +class RoomController extends LoadController { + @override + Room defaultData() => Room(name: ""); - 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()); - } + RoomController({super.id, Room? room}) : super(initialData: room, service: RoomService()); } 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 c836b894..65b3c63c 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 @@ -1,5 +1,6 @@ import 'dart:async'; import 'package:helpwave_service/src/api/tasks/index.dart'; +import 'package:helpwave_service/src/util/identified_object.dart'; import 'package:helpwave_util/loading.dart'; /// The Controller for managing [Room]s in a [Ward] @@ -9,11 +10,11 @@ import 'package:helpwave_util/loading.dart'; /// only used locally class RoomsController extends LoadingChangeNotifier { /// The [Room]s - List _rooms = []; + List _rooms = []; - List get rooms => [..._rooms]; + List get rooms => [..._rooms]; - set rooms(List value) { + set rooms(List value) { _rooms = value; notifyListeners(); } @@ -30,11 +31,11 @@ class RoomsController extends LoadingChangeNotifier { /// Loads the [Room]s Future load() async { - if (isCreating) { - return; - } loadOp() async { - rooms = await RoomService().getRooms(wardId: wardId!); + if (isCreating) { + return; + } + rooms = await RoomService().getMany(wardId: wardId!); } loadHandler(future: loadOp()); @@ -48,14 +49,14 @@ class RoomsController extends LoadingChangeNotifier { notifyListeners(); return; } - await delete(rooms[index].id); + 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) { + await RoomService().delete(id).then((value) { if (value) { int index = _rooms.indexWhere((element) => element.id == id); if (index != -1) { @@ -69,14 +70,14 @@ class RoomsController extends LoadingChangeNotifier { } /// Add the [Room] - Future create(RoomWithBedWithMinimalPatient room) async { + Future create(Room room) async { if (isCreating) { _rooms.add(room); notifyListeners(); return; } createOp() async { - await RoomService().createRoom(wardId: wardId!, name: room.name).then((value) { + await RoomService().create(room.copyWith(RoomUpdate(wardId: wardId!))).then((value) { _rooms.add(room); }); } @@ -84,7 +85,7 @@ class RoomsController extends LoadingChangeNotifier { loadHandler(future: createOp()); } - Future update({required RoomWithBedWithMinimalPatient room, int? index}) async { + Future update({required Room room, int? index}) async { if (isCreating) { assert( index != null && index >= 0 && index < rooms.length, @@ -95,7 +96,7 @@ class RoomsController extends LoadingChangeNotifier { } 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); + await RoomService().update(room.id!, RoomUpdate(name: room.name)); int index = rooms.indexWhere((element) => element.id == room.id); if (index != -1) { rooms[index] = room; 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 7e0eb529..c41e132b 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 @@ -35,8 +35,8 @@ class SubtasksController extends LoadingChangeNotifier { if (isCreating) { return; } - final task = await TaskService().getTask(id: taskId); - subtasks = task.subtasks; + final task = await TaskService().get(taskId!); + subtasks = task.subtasks ?? []; } loadHandler(future: loadTask()); 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 b4f5bf21..080a3d6c 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,164 +1,80 @@ -import 'package:helpwave_util/loading.dart'; import 'package:helpwave_service/src/api/tasks/index.dart'; import 'package:helpwave_service/util.dart'; import 'package:helpwave_service/user.dart'; -/// The Controller for managing a [TaskWithPatient] -class TaskController extends LoadingChangeNotifier { - /// The current [Task] - TaskWithPatient _task; +/// The Controller for managing a [Task] +class TaskController + extends LoadController { + Patient get patient => data.patient.data!; - TaskController(this._task) { - load(); - } - - TaskWithPatient get task => _task; - - set task(TaskWithPatient value) { - _task = value; - notifyListeners(); - } - - PatientMinimal get patient => task.patient; - - User? _assignee; - - User? get assignee => _assignee; - - bool get isCreating => _task.isCreating; + User? get assignee => data.assignee.data; /// 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; - - /// 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) { - await UserService().getUser(id: task.assigneeId!).then((value) => _assignee = value); - } - }); - } - - loadHandler(future: loadTask()); - } - - /// Changes the assigned [User] - 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; - }); - } + bool get isReadyForCreate => !data.patient.isCreating; - await loadHandler(future: changeAssigneeFuture()); - } + @override + Task defaultData() => Task.empty(null); - Future changeName(String name) async { - 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()); - } + TaskController({super.id, Task? task}) + : super(initialData: task, service: TaskService()); - Future changeStatus(TaskStatus status) async { - if (isCreating) { - task = TaskWithPatient.fromTaskAndPatient(task: task.copyWith(status: status), patient: task.patient); - notifyListeners(); + @override + loadOp() async { + if (data.isCreating) { 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); - 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 { - 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()); + await TaskService().get(data.id!).then((value) async { + changeData(value, isNotifying: false); + if (data.hasAssignee) { + await UserService() + .get(id: data.assignee.id!) + .then((user) => changeData(value.copyWith(TaskCopyWithUpdate( + assignee: LoadableCRUDObjectUpdate(overwrite: user), + )))); + } + }); } - Future changeDueDate(DateTime? dueDate) async { - 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)); + /// Changes the assigned [User] + Future changeAssignee(User? user) async { + changeAssigneeOp() async { + if (isCreating) { + changeData( + data.copyWith( + TaskCopyWithUpdate(assignee: LoadableCRUDObjectUpdate(overwrite: user))), + isNotifying: false, + ); + return; + } + await TaskService() + .changeAssignee(taskId: data.id!, userId: user?.id) + .then((value) { + changeData( + data.copyWith( + TaskCopyWithUpdate(assignee: LoadableCRUDObjectUpdate(overwrite: user))), + isNotifying: false, + ); + }); } - loadHandler(future: dueDate == null ? removeDueDate() : updateDueDate()); + await loadHandler(future: changeAssigneeOp()); } /// 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(); - } - - /// Creates the Task and returns - Future create() async { - assert(isReadyForCreate, "A the patient must be set to create a task"); - createTask() async { - await TaskService().createTask(task).then((value) { - task.copyWith(id: value); - }); + Future changePatient(Patient patient) async { + changePatientOp() 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"); + changeData( + data.copyWith(TaskCopyWithUpdate( + patient: LoadableCRUDObjectUpdate(overwrite: patient))), + isNotifying: false); } - return loadHandler(future: createTask()); + await loadHandler(future: changePatientOp()); } } 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 c1a29d76..0f765e6c 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 @@ -54,11 +54,11 @@ class WardPatientsController extends LoadingChangeNotifier { usedPatients, (patient) { List searchTags = [patient.name]; - if (patient.bed != null) { - searchTags.add(patient.bed!.name); + if (patient.bed.hasDataValue) { + searchTags.add(patient.bed.data!.name); } - if (patient.room != null) { - searchTags.add(patient.room!.name); + if (patient.room.hasDataValue) { + searchTags.add(patient.room.data!.name); } return searchTags; }, 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 d89de03d..82e7d60a 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,26 +1,40 @@ import 'package:helpwave_service/src/api/tasks/index.dart'; +import 'package:helpwave_service/src/util/index.dart'; -/// Data class for a [Bed] -class BedMinimal { - String id; - String name; - - BedMinimal({ - required this.id, - required this.name, - }); +class BedUpdate { + String? id; + String? name; + Patient? patient; + String? roomId; - bool get isCreating => id == ""; + BedUpdate({this.id, this.name, this.patient, this.roomId}); } -class Bed extends BedMinimal { - PatientMinimal? patient; - String roomId; +/// Data class for a [Bed] +class Bed extends CRUDObject { + final String name; + final Patient? patient; + final String? roomId; Bed({ - required super.id, - required super.name, - required this.roomId, + super.id, + required this.name, this.patient, + this.roomId, }); + + @override + Bed copyWith(BedUpdate? update) { + return Bed( + id: update?.id ?? id, + name: update?.name ?? name, + patient: update?.patient ?? patient, + roomId: update?.roomId ?? roomId, + ); + } + + @override + Bed attachId(String id) { + return copyWith(BedUpdate(id: id)); + } } 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 45ce4409..1389008d 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,89 +1,66 @@ import 'package:helpwave_service/src/api/tasks/index.dart'; +import 'package:helpwave_service/src/util/list_update.dart'; +import 'package:helpwave_service/src/util/loadable_crud_object.dart'; import 'package:helpwave_service/util.dart'; enum PatientAssignmentStatus { active, unassigned, discharged, all } -class PatientMinimal extends IdentifiedObject { - String name; - - factory PatientMinimal.empty() => PatientMinimal(name: ""); - - PatientMinimal({ - super.id, - required this.name, +class PatientUpdate { + String? id; + String? name; + String? notes; + LoadableCRUDObjectUpdate? bed; + LoadableCRUDObjectUpdate? room; + bool? isDischarged; + ListUpdate? tasks; + + PatientUpdate({ + this.id, + this.name, + this.notes, + this.isDischarged, + this.tasks, + this.bed, + this.room, }); +} - @override - bool operator ==(Object other) { - if (identical(this, other)) { - return true; - } +class Patient extends CRUDObject { + final String name; + final String notes; + final List? tasks; + final bool isDischarged; + late final LoadableCRUDObject bed; + late final LoadableCRUDObject room; - if (other is PatientMinimal) { - return id == other.id && name == other.name; - } + bool get isRoomAndBedAvailable => bed.isPresent && room.isPresent; - return false; + bool get isNotAssignedToBed { + assert(isRoomAndBedAvailable, "The bed assignmentStatus can only be inferred if the bed and room information are available"); + return !bed.isNotNull && !room.isNotNull; } - @override - int get hashCode => id.hashCode + name.hashCode; - - @override - String toString() { - return "$runtimeType{id: $id, name: $name}"; + bool get isAssignedToBed { + assert(isRoomAndBedAvailable, "The bed assignmentStatus can only be inferred if the bed and room information are available"); + return bed.isNotNull && room.isNotNull; } -} - -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, - ); + // TODO remove this when discharge information becomes always available + bool get isActive { + assert(isRoomAndBedAvailable, "The bed assignmentStatus can only be inferred if the bed and room information are available"); + return bed.isNotNull && room.isNotNull; } - bool get hasBed => bedId != null; -} - -/// data class for [Patient] with TaskCount -class Patient extends PatientMinimal { - RoomMinimal? room; - BedMinimal? bed; - String notes; - List tasks; - bool isDischarged; - - get isNotAssignedToBed => bed == null && room == null; - - get isActive => bed != null && room != null; - - List get unscheduledTasks => tasks.where((task) => task.status == TaskStatus.todo).toList(); + 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(); + List get doneTasks => + (tasks ?? []).where((task) => task.status == TaskStatus.done).toList(); get unscheduledCount => unscheduledTasks.length; @@ -92,38 +69,67 @@ class Patient extends PatientMinimal { get doneCount => doneTasks.length; factory Patient.empty({String? id}) { - return Patient(id: id, name: "Patient", tasks: [], notes: "", isDischarged: false); + return Patient( + id: id, + name: "Patient", + tasks: [], + notes: "", + isDischarged: false); } Patient({ super.id, - required super.name, - required this.tasks, - required this.notes, + required this.name, + this.notes = "", + this.tasks, required this.isDischarged, - this.room, - this.bed, - }); - - Patient copyWith({ - String? id, - String? name, - List? tasks, - String? notes, - bool? isDischarged, - RoomMinimal? room, - BedMinimal? bed, + LoadableCRUDObject? bed, + LoadableCRUDObject? room, }) { + assert(!(room?.isPresent ?? false) || (bed?.isPresent ?? false), + "If the room is present the bed must be as well."); + this.bed = LoadableCRUDObject.notPresentObject(); + this.room = LoadableCRUDObject.notPresentObject(); + } + + @override + bool operator ==(Object other) { + if (identical(this, other)) { + return true; + } + + if (other is Patient) { + return id == other.id && name == other.name; + } + + return false; + } + + @override + int get hashCode => id.hashCode + name.hashCode; + + @override + String toString() { + return "$runtimeType{id: $id, name: $name}"; + } + + @override + Patient copyWith(PatientUpdate? update) { 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, + id: update?.id ?? id, + name: update?.name ?? name, + notes: update?.notes ?? notes, + isDischarged: update?.isDischarged ?? isDischarged, + tasks: update?.tasks?.apply(tasks), + bed: bed.copyWith(update?.bed), + room: room.copyWith(update?.room), ); } + + @override + Patient attachId(String id) { + return copyWith(PatientUpdate(id: id)); + } } /// A data class which maps all [PatientAssignmentStatus]es to a [List] of [Patient]s 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 0385aac0..188c5fe7 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 @@ -1,37 +1,62 @@ import 'package:helpwave_service/src/api/tasks/data_types/bed.dart'; +import 'package:helpwave_service/src/util/crud_object_interface.dart'; +import 'package:helpwave_service/src/util/list_update.dart'; + +class RoomUpdate { + String? id; + String? name; + String? wardId; + ListUpdate? beds; + + RoomUpdate({this.id, this.name, this.wardId, this.beds}); +} /// data class for [Room] -class RoomMinimal { - String id; +class Room extends CRUDObject{ String name; + String? wardId; + List? beds; - RoomMinimal({ - required this.id, + Room({ + super.id, required this.name, + this.wardId, + this.beds, }); - bool get isCreating => id == ""; -} + @override + Room copyWith(RoomUpdate? update) { + return Room( + id: update?.id ?? id, + name: update?.name ?? name, + beds: update?.beds?.apply(beds) + ); + } -class RoomWithWardId extends RoomMinimal { - String wardId; + @override + Room attachId(String id) { + return copyWith(RoomUpdate(id: id)); + } - RoomWithWardId({required super.id, required super.name, required this.wardId}); + @override + String toString() { + return "$runtimeType{id: $id, name: $name, wardId: $wardId, bed: $beds}"; + } } class RoomWithBeds { String id; String name; - List beds; + List beds; RoomWithBeds({required this.id, required this.name, required this.beds}); } -class RoomWithBedFlat { - RoomMinimal room; - BedMinimal bed; +class RoomAndBed { + Room room; + Bed bed; - RoomWithBedFlat({ + RoomAndBed({ required this.room, required this.bed, }); @@ -42,7 +67,7 @@ class RoomWithBedFlat { return true; } - if (other is RoomWithBedFlat) { + if (other is RoomAndBed) { return room.id == other.room.id && room.name == other.room.name && bed.id == other.bed.id && @@ -54,19 +79,9 @@ class RoomWithBedFlat { @override String toString() { - return "RoomWithBedFlat {room: {id: ${room.id}, name: ${room.name}}, bed: {id: ${bed.id}, name: ${bed.name}}}"; + return "$runtimeType{room: $room, bed: $bed}"; } @override int get hashCode => room.id.hashCode + bed.id.hashCode; } - -class RoomWithBedWithMinimalPatient extends RoomMinimal { - List beds; - - RoomWithBedWithMinimalPatient({ - required super.id, - required super.name, - required this.beds, - }); -} 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 e7dbcdeb..89d84866 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,26 +1,36 @@ import 'package:helpwave_service/util.dart'; +class SubtaskUpdate { + String? id; + String? taskId; + String? name; + bool? isDone; + + SubtaskUpdate({this.id, this.taskId, this.name, this.isDone}); +} + /// Data class for a [Subtask] -class Subtask extends IdentifiedObject { +class Subtask extends CRUDObject { final String? taskId; String name; bool isDone; - - Subtask({super.id, required this.taskId, required this.name, this.isDone = false}) : assert(id == null || taskId != null); + Subtask({super.id, required this.taskId, required this.name, this.isDone = false}) + : assert(id == null || taskId != null, "Either provide a taskId or remove the id"); /// Create a copy of the [Subtask] - Subtask copyWith({ - String? id, - String? taskId, - String? name, - bool? isDone, - }) { + @override + Subtask copyWith(SubtaskUpdate? update) { return Subtask( - id: id ?? this.id, - taskId: taskId ?? this.taskId, - name: name ?? this.name, - isDone: isDone ?? this.isDone, + id: update?.id ?? id, + taskId: update?.taskId ?? taskId, + name: update?.name ?? name, + isDone: update?.isDone ?? isDone, ); } + + @override + attachId(String id) { + return copyWith(SubtaskUpdate(id: id)); + } } 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 44498595..d8afc854 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,7 @@ 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/util.dart'; +import '../../../../user.dart'; enum TaskStatus { unspecified, @@ -9,29 +10,82 @@ enum TaskStatus { done, } -/// data class for [Task] -class Task extends IdentifiedObject { - String name; - String? assigneeId; - String notes; - TaskStatus status; - List subtasks; +class TaskUpdate { + String? id; + String? name; + String? notes; + TaskStatus? status; DateTime? dueDate; - final DateTime? creationDate; - final String? createdBy; - bool isPublicVisible; - final String? patientId; + bool? isPublicVisible; + + TaskUpdate({ + this.id, + this.name, + this.notes, + this.status, + this.dueDate, + this.isPublicVisible, + }); +} - factory Task.empty(String? patientId) => Task(name: "name", notes: "", patientId: patientId); +class TaskCopyWithUpdate extends TaskUpdate{ + LoadableCRUDObjectUpdate? assignee; + ListUpdate? subtasks; + bool removeDueDate; + DateTime? creationDate; + LoadableCRUDObjectUpdate? creator; + LoadableCRUDObjectUpdate? patient; - final _nullID = "00000000-0000-0000-0000-000000000000"; + TaskCopyWithUpdate({ + super.id, + super.name, + super.notes, + super.status, + super.dueDate, + super.isPublicVisible, + this.assignee, + this.subtasks, + this.removeDueDate = false, + this.creationDate, + this.creator, + this.patient, + }) { + assert( + !removeDueDate || dueDate == null, + "You can either remove the dueDate or set it. Try removing the parameter you don't need.", + ); + } +} - double get progress => subtasks.isNotEmpty ? subtasks.where((element) => element.isDone).length / subtasks.length : 1; +/// data class for [Task] +class Task extends CRUDObject { + final String name; + late final LoadableCRUDObject assignee; + final String notes; + final TaskStatus status; + final List? subtasks; + final DateTime? dueDate; + final DateTime? creationDate; + late final LoadableCRUDObject creator; + final bool isPublicVisible; + late final LoadableCRUDObject patient; + + factory Task.empty(String? patientId) => + Task(name: "name", notes: "", patient: LoadableCRUDObject(id: patientId)); + + double get progress { + if (subtasks?.isEmpty ?? true) { + return 1; + } + return subtasks!.where((element) => element.isDone).length / + subtasks!.length; + } /// 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; @@ -39,104 +93,55 @@ class Task extends IdentifiedObject { bool get inNextHour => remainingTime.inHours < 1; - bool get hasAssignee => assigneeId != null && assigneeId != "" && assigneeId != _nullID; + bool get hasAssignee => !assignee.isNotNull; Task({ super.id, required this.name, required this.notes, - this.assigneeId, + LoadableCRUDObject? assignee, this.status = TaskStatus.todo, this.subtasks = const [], this.dueDate, this.creationDate, - this.createdBy, + LoadableCRUDObject? creator, 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, + required this.patient, }) { - 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, - ); + assert(patient.isNotNull, "The patient must at least have an id."); + this.assignee = assignee ?? LoadableCRUDObject.notPresentObject(); + this.creator = creator ?? LoadableCRUDObject.notPresentObject(); } @override - String toString() { - return "{id: $id, name: $name, description: $notes, subtasks: $subtasks, patientId: $patientId}"; - } -} - -class TaskWithPatient extends Task { - final PatientMinimal patient; + Task copyWith(TaskCopyWithUpdate? update) { + DateTime? updatedDueDate = update?.dueDate ?? dueDate; + if (update?.removeDueDate ?? false) { + updatedDueDate = null; + } - factory TaskWithPatient.empty({ - String? taskId, - PatientMinimal? patient, - }) { - return TaskWithPatient( - id: taskId, - name: "task name", - notes: "", - patient: patient ?? PatientMinimal.empty(), - patientId: patient?.id, + return Task( + id: update?.id ?? id, + name: update?.name ?? name, + assignee: assignee.copyWith(update?.assignee), + notes: update?.notes ?? notes, + status: update?.status ?? status, + subtasks: update?.subtasks?.apply(subtasks), + dueDate: updatedDueDate, + creationDate: update?.creationDate ?? creationDate, + creator: creator.copyWith(update?.creator), + isPublicVisible: update?.isPublicVisible ?? isPublicVisible, + patient: patient.copyWith(update?.patient), ); } - factory TaskWithPatient.fromTaskAndPatient({ - required Task task, - PatientMinimal? patient, - }) { - return TaskWithPatient( - id: task.id, - name: task.name, - notes: task.notes, - isPublicVisible: task.isPublicVisible, - // maybe do deep copy here - subtasks: task.subtasks, - status: task.status, - dueDate: task.dueDate, - creationDate: task.creationDate, - assigneeId: task.assigneeId, - patient: patient ?? PatientMinimal.empty(), - patientId: patient?.id, - ); + @override + String toString() { + return "{id: $id, name: $name, description: $notes, subtasks: $subtasks, patientId: ${patient.data?.id}"; } - TaskWithPatient({ - required super.id, - required super.name, - required super.notes, - super.assigneeId, - super.status, - super.subtasks, - super.dueDate, - super.creationDate, - super.createdBy, - super.isPublicVisible, - required super.patientId, - required this.patient, - }) : assert(patientId == patient.id); + @override + Task attachId(String id) { + return copyWith(TaskCopyWithUpdate(id: 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 20e61709..89aea18a 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 @@ -73,10 +73,11 @@ class BedOfflineClient extends BedServiceClient { throw "Bed with bed id ${request.id} not found"; } - final response = GetBedResponse() - ..id = bed.id - ..name = bed.name - ..roomId = bed.roomId; + final response = GetBedResponse( + id: bed.id, + name: bed.name, + roomId: bed.roomId + ); return MockResponseFuture.value(response); } @@ -97,14 +98,14 @@ class BedOfflineClient extends BedServiceClient { if (patient == null) { throw "Patient with id ${request.patientId} not found"; } - if (!patient.hasBed) { + if (!patient.bed.isNotNull) { throw "Patient with id ${request.patientId} has no bed"; } - final bed = OfflineClientStore().bedStore.findBed(patient.bedId!); + final bed = OfflineClientStore().bedStore.findBed(patient.bed.id!); if (bed == null) { - throw "Inconsistent Data: Bed with id ${patient.bedId} not found"; + throw "Inconsistent Data: Bed with id ${patient.bed.id} not found"; } - final room = OfflineClientStore().roomStore.findRoom(bed.roomId); + final room = bed.roomId != null ? OfflineClientStore().roomStore.findRoom(bed.roomId!) : null; if (room == null) { throw "Inconsistent Data: Room with id ${bed.roomId} not found"; } @@ -133,7 +134,7 @@ class BedOfflineClient extends BedServiceClient { OfflineClientStore().bedStore.create(newBed); - final response = CreateBedResponse()..id = newBed.id; + final response = CreateBedResponse(id: newBed.id); 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 index 577d3c25..b45debe2 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 @@ -4,20 +4,12 @@ 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; - String? name; - String? notes; - bool? isDischarged; - - PatientUpdate({required this.id, this.name, this.notes, this.isDischarged}); -} +import 'package:helpwave_service/src/util/index.dart'; class PatientOfflineService { - List patients = []; + List patients = []; - PatientWithBedId? findPatient(String id) { + Patient? findPatient(String id) { int index = OfflineClientStore().patientStore.patients.indexWhere((value) => value.id == id); if (index == -1) { return null; @@ -25,12 +17,12 @@ class PatientOfflineService { return patients[index]; } - PatientWithBedId? findPatientByBed(String bedId) { + Patient? findPatientByBed(String bedId) { final valueStore = OfflineClientStore().patientStore; - return valueStore.patients.where((value) => value.bedId == bedId).firstOrNull; + return valueStore.patients.where((value) => value.bed.id == bedId).firstOrNull; } - void create(PatientWithBedId patient) { + void create(Patient patient) { OfflineClientStore().patientStore.patients.add(patient); } @@ -41,11 +33,7 @@ class PatientOfflineService { 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.copyWith(patientUpdate); } return value; }).toList(); @@ -66,7 +54,7 @@ class PatientOfflineService { valueStore.patients = valueStore.patients.map((value) { if (value.id == patientId) { found = true; - return value.copyWith(bedId: bedId); + return value.copyWith(PatientUpdate(bed: LoadableCRUDObjectUpdate(id: bedId))); } return value; }).toList(); @@ -83,9 +71,7 @@ class PatientOfflineService { valueStore.patients = valueStore.patients.map((value) { if (value.id == patientId) { found = true; - final copy = value.copyWith(); - copy.bedId = null; - return copy; + return value.copyWith(PatientUpdate(bed: LoadableCRUDObjectUpdate(setToNull: true))); } return value; }).toList(); @@ -118,14 +104,14 @@ class PatientOfflineClient extends PatientServiceClient { final response = GetPatientResponse(id: patient.id, humanReadableIdentifier: patient.name, notes: patient.notes); - if (patient.bedId == null) { + if (patient.bed.isNull) { return MockResponseFuture.value(response); } - final bed = OfflineClientStore().bedStore.findBed(patient.bedId!); + final bed = OfflineClientStore().bedStore.findBed(patient.bed.id!); if (bed == null) { return MockResponseFuture.value(response); } - final room = OfflineClientStore().roomStore.findRoom(bed.roomId); + final room = bed.roomId != null ? OfflineClientStore().roomStore.findRoom(bed.roomId!) : null; if (room == null) { return MockResponseFuture.value(response); } @@ -152,7 +138,7 @@ class PatientOfflineClient extends PatientServiceClient { id: task.id, name: task.name, patientId: patient.id, - assignedUserId: task.assigneeId, + assignedUserId: task.assignee.id, description: task.notes, public: task.isPublicVisible, status: TasksGRPCTypeConverter.taskStatusToGRPC(task.status), @@ -167,14 +153,14 @@ class PatientOfflineClient extends PatientServiceClient { )), ); - if (patient.bedId == null) { + if (patient.bed.isNull) { return MockResponseFuture.value(response); } - final bed = OfflineClientStore().bedStore.findBed(patient.bedId!); + final bed = OfflineClientStore().bedStore.findBed(patient.bed.id!); if (bed == null) { return MockResponseFuture.value(response); } - final room = OfflineClientStore().roomStore.findRoom(bed.roomId); + final room = bed.roomId != null ? OfflineClientStore().roomStore.findRoom(bed.roomId!) : null; if (room == null) { return MockResponseFuture.value(response); } @@ -195,7 +181,7 @@ class PatientOfflineClient extends PatientServiceClient { id: patient.id, humanReadableIdentifier: patient.name, notes: patient.notes, - bedId: patient.bedId, + bedId: patient.bed.id, ); return MockResponseFuture.value(response); } @@ -211,7 +197,7 @@ class PatientOfflineClient extends PatientServiceClient { id: task.id, name: task.name, patientId: patient.id, - assignedUserId: task.assigneeId, + assignedUserId: task.assignee.id, description: task.notes, public: task.isPublicVisible, status: TasksGRPCTypeConverter.taskStatusToGRPC(task.status), @@ -232,7 +218,7 @@ class PatientOfflineClient extends PatientServiceClient { if (bed == null) { return res; } - final room = OfflineClientStore().roomStore.findRoom(bed.roomId); + final room = bed.roomId != null ? OfflineClientStore().roomStore.findRoom(bed.roomId!) : null; if (room == null) { return res; } @@ -242,9 +228,9 @@ class PatientOfflineClient extends PatientServiceClient { } final patients = OfflineClientStore().patientStore.patients; - final active = patients.where((element) => element.hasBed).map(mapping); + final active = patients.where((element) => element.isActive).map(mapping); final discharged = patients.where((element) => element.isDischarged).map(mapping); - final unassigned = patients.where((element) => !element.isDischarged && element.bedId == null).map(mapping); + final unassigned = patients.where((element) => !element.isDischarged && element.bed.isNull).map(mapping); final response = GetPatientListResponse( active: active, dischargedPatients: discharged, @@ -257,19 +243,19 @@ class PatientOfflineClient extends PatientServiceClient { @override ResponseFuture getRecentPatients(GetRecentPatientsRequest request, {CallOptions? options}) { - final patients = OfflineClientStore().patientStore.patients.where((element) => element.hasBed).map((patient) { + final patients = OfflineClientStore().patientStore.patients.where((element) => element.bed.isNotNull).map((patient) { final res = GetRecentPatientsResponse_Patient( id: patient.id, humanReadableIdentifier: patient.name, ); - if (patient.bedId == null) { + if (patient.bed.isNull) { return res; } - final bed = OfflineClientStore().bedStore.findBed(patient.bedId!); + final bed = OfflineClientStore().bedStore.findBed(patient.bed.id!); if (bed == null) { return res; } - final room = OfflineClientStore().roomStore.findRoom(bed.roomId); + final room = bed.roomId != null ? OfflineClientStore().roomStore.findRoom(bed.roomId!) : null; if (room == null) { return res; } @@ -290,10 +276,10 @@ class PatientOfflineClient extends PatientServiceClient { List patients = []; for (final bed in beds) { - final patient = OfflineClientStore().patientStore.findPatientByBed(bed.id); + 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)); + id: patient.id, notes: patient.notes, humanReadableIdentifier: patient.name, bedId: patient.bed.id)); } } final response = GetPatientsByWardResponse(patients: patients); @@ -311,7 +297,7 @@ class PatientOfflineClient extends PatientServiceClient { 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); + final patient = OfflineClientStore().patientStore.findPatientByBed(bed.id!); if (patient != null) { res.patient = GetPatientAssignmentByWardResponse_Room_Bed_Patient( id: patient.id, @@ -328,7 +314,7 @@ class PatientOfflineClient extends PatientServiceClient { @override ResponseFuture createPatient(CreatePatientRequest request, {CallOptions? options}) { - final newPatient = PatientWithBedId( + final newPatient = Patient( id: DateTime.now().millisecondsSinceEpoch.toString(), name: request.humanReadableIdentifier, notes: request.notes, 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 57ed4799..fa24ea82 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 @@ -12,17 +12,20 @@ class RoomUpdate { } class RoomOfflineService { - List rooms = []; + List rooms = []; - RoomWithWardId? findRoom(String id) { - int index = OfflineClientStore().roomStore.rooms.indexWhere((value) => value.id == id); + Room? 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]) { + List findRooms([String? wardId]) { final valueStore = OfflineClientStore().roomStore; if (wardId == null) { return valueStore.rooms; @@ -30,7 +33,7 @@ class RoomOfflineService { return valueStore.rooms.where((value) => value.wardId == wardId).toList(); } - void create(RoomWithWardId room) { + void create(Room room) { OfflineClientStore().roomStore.rooms.add(room); } @@ -41,7 +44,8 @@ class RoomOfflineService { 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 Room( + id: room.id, name: room.name ?? value.name, wardId: value.wardId); } return value; }).toList(); @@ -53,9 +57,10 @@ class RoomOfflineService { 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); + valueStore.rooms = + valueStore.rooms.where((value) => value.id != roomId).toList(); + OfflineClientStore().bedStore.findBeds(roomId).forEach((bed) { + OfflineClientStore().bedStore.delete(bed.id!); }); } } @@ -64,32 +69,39 @@ class RoomOfflineClient extends RoomServiceClient { RoomOfflineClient(super.channel); @override - ResponseFuture getRoom(GetRoomRequest request, {CallOptions? options}) { + 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; + final response = GetRoomResponse( + id: room.id, + name: room.name, + wardId: room.wardId, + beds: [], // TODO add this + ); return MockResponseFuture.value(response); } @override - ResponseFuture getRooms(GetRoomsRequest request, {CallOptions? options}) { + 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, - )), + beds: OfflineClientStore() + .bedStore + .findBeds(room.id) + .map((bed) => GetRoomsResponse_Room_Bed( + id: bed.id, + name: bed.name, + )), )); final response = GetRoomsResponse(rooms: roomsList); @@ -97,35 +109,50 @@ class RoomOfflineClient extends RoomServiceClient { } @override - ResponseFuture getRoomOverviewsByWard(GetRoomOverviewsByWardRequest request, + 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; - })))); + final response = GetRoomOverviewsByWardResponse( + rooms: rooms.map((room) => GetRoomOverviewsByWardResponse_Room( + id: room.id, + name: room.name, + beds: 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( + ResponseFuture createRoom(CreateRoomRequest request, + {CallOptions? options}) { + final newRoom = Room( id: DateTime.now().millisecondsSinceEpoch.toString(), name: request.name, wardId: request.wardId, @@ -133,13 +160,14 @@ class RoomOfflineClient extends RoomServiceClient { OfflineClientStore().roomStore.create(newRoom); - final response = CreateRoomResponse()..id = newRoom.id; + final response = CreateRoomResponse(id: newRoom.id); return MockResponseFuture.value(response); } @override - ResponseFuture updateRoom(UpdateRoomRequest request, {CallOptions? options}) { + ResponseFuture updateRoom(UpdateRoomRequest request, + {CallOptions? options}) { final update = RoomUpdate( id: request.id, name: request.name, @@ -152,7 +180,8 @@ class RoomOfflineClient extends RoomServiceClient { } @override - ResponseFuture deleteRoom(DeleteRoomRequest request, {CallOptions? options}) { + ResponseFuture deleteRoom(DeleteRoomRequest request, + {CallOptions? options}) { OfflineClientStore().roomStore.delete(request.id); final response = DeleteRoomResponse(); 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 238d4fdd..cdaccc95 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 @@ -5,31 +5,14 @@ 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}); -} +import 'package:helpwave_service/src/util/index.dart'; class TaskOfflineService { List tasks = []; Task? findTask(String id) { - int index = OfflineClientStore().taskStore.tasks.indexWhere((value) => value.id == id); + int index = OfflineClientStore().taskStore.tasks.indexWhere((value) => + value.id == id); if (index == -1) { return null; } @@ -41,98 +24,35 @@ class TaskOfflineService { if (patientId == null) { return valueStore.tasks; } - return valueStore.tasks.where((value) => value.patientId == patientId).toList(); + return valueStore.tasks.where((value) => value.patient.id == 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"; - } + void update(String taskId, TaskCopyWithUpdate taskUpdate) { 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.copyWith(taskUpdate); } 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'); + throw Exception('UpdateTask: 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(); + valueStore.tasks = + valueStore.tasks.where((value) => value.id != taskId).toList(); OfflineClientStore().subtaskStore.findSubtasks(taskId).forEach((subtask) { OfflineClientStore().subtaskStore.delete(subtask.id!); }); @@ -143,7 +63,8 @@ class SubtaskOfflineService { List subtasks = []; Subtask? findSubtask(String id) { - int index = OfflineClientStore().subtaskStore.subtasks.indexWhere((value) => value.id == id); + int index = OfflineClientStore().subtaskStore.subtasks.indexWhere(( + value) => value.id == id); if (index == -1) { return null; } @@ -155,7 +76,8 @@ class SubtaskOfflineService { if (taskId == null) { return valueStore.subtasks; } - return valueStore.subtasks.where((value) => value.taskId == taskId).toList(); + return valueStore.subtasks.where((value) => value.taskId == taskId) + .toList(); } void create(Subtask subtask) { @@ -169,19 +91,22 @@ class SubtaskOfflineService { valueStore.subtasks = valueStore.subtasks.map((value) { if (value.id == subtaskUpdate.id) { found = true; - return value.copyWith(name: subtaskUpdate.name, isDone: subtaskUpdate.isDone); + return value.copyWith(SubtaskUpdate( + name: subtaskUpdate.name, isDone: subtaskUpdate.isDone)); } return value; }).toList(); if (!found) { - throw Exception('UpdateSubtask: Could not find subtask with id ${subtaskUpdate.id}'); + 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(); + valueStore.subtasks = + valueStore.subtasks.where((value) => value.id != subtaskId).toList(); } } @@ -189,63 +114,75 @@ class TaskOfflineClient extends TaskServiceClient { TaskOfflineClient(super.channel); @override - ResponseFuture getTask(GetTaskRequest request, {CallOptions? options}) { + 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 = task.patientId == null ? null : OfflineClientStore().patientStore.findPatient(task.patientId!); + final patient = task.patient.isNull ? null : OfflineClientStore() + .patientStore.findPatient(task.patient.id!); if (patient == null) { - throw "Inconsistency error: Patient with patient id ${task.patientId} not found"; + throw "Inconsistency error: Patient with patient id ${task.patient + .id} not found"; } final subtasks = OfflineClientStore() .subtaskStore .findSubtasks(task.id) - .map((subtask) => GetTaskResponse_SubTask(id: subtask.id, name: subtask.name, done: subtask.isDone)); + .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: 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!), + dueAt: task.dueDate == null ? null : Timestamp.fromDateTime( + task.dueDate!), + createdBy: task.creator.id, + createdAt: task.creationDate == null ? null : Timestamp.fromDateTime( + task.creationDate!), public: task.isPublicVisible, - assignedUserId: task.assigneeId, - patient: GetTaskResponse_Patient(id: patient.id, humanReadableIdentifier: patient.name), + assignedUserId: task.assignee.id, + patient: GetTaskResponse_Patient( + id: patient.id, humanReadableIdentifier: patient.name), subtasks: subtasks); return MockResponseFuture.value(response); } @override - ResponseFuture getTasksByPatient(GetTasksByPatientRequest request, + 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: 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!), - 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, - )), - )); + OfflineClientStore().taskStore.findTasks(request.patientId).map((task) => + GetTasksByPatientResponse_Task( + id: task.id, + name: task.name, + description: task.notes, + status: TasksGRPCTypeConverter.taskStatusToGRPC(task.status), + dueAt: task.dueDate == null ? null : Timestamp.fromDateTime( + task.dueDate!), + createdBy: task.creator.id, + createdAt: task.creationDate == null ? null : Timestamp.fromDateTime( + task.creationDate!), + public: task.isPublicVisible, + assignedUserId: task.assignee.id, + 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); @@ -253,33 +190,41 @@ class TaskOfflineClient extends TaskServiceClient { } @override - ResponseFuture getAssignedTasks(GetAssignedTasksRequest request, {CallOptions? options}) { + 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 tasks = OfflineClientStore().taskStore.findTasks().where(( + task) => task.assignee.id == user.id).map((task) { final res = GetAssignedTasksResponse_Task( id: task.id, name: task.name, description: task.notes, 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!), + dueAt: task.dueDate == null ? null : Timestamp.fromDateTime( + task.dueDate!), + createdBy: task.creator.id, + createdAt: task.creationDate == null ? null : Timestamp.fromDateTime( + task.creationDate!), public: task.isPublicVisible, - assignedUserId: task.assigneeId, + assignedUserId: task.assignee.id, subtasks: OfflineClientStore() .subtaskStore .findSubtasks(task.id) - .map((subtask) => GetAssignedTasksResponse_Task_SubTask( - id: subtask.id, - name: subtask.name, - done: subtask.isDone, - )), + .map((subtask) => + GetAssignedTasksResponse_Task_SubTask( + id: subtask.id, + name: subtask.name, + done: subtask.isDone, + )), ); - final patient = task.patientId == null ? null : OfflineClientStore().patientStore.findPatient(task.patientId!); + final patient = task.patient.id == null ? null : OfflineClientStore() + .patientStore.findPatient(task.patient.id!); if (patient == null) { - throw "Inconsistency error: patient with id ${task.patientId} not found"; + throw "Inconsistency error: patient with id ${task.patient + .id} not found"; } - res.patient = GetAssignedTasksResponse_Task_Patient(id: patient.id, humanReadableIdentifier: patient.name); + res.patient = GetAssignedTasksResponse_Task_Patient( + id: patient.id, humanReadableIdentifier: patient.name); return res; }); @@ -289,77 +234,97 @@ class TaskOfflineClient extends TaskServiceClient { } @override - ResponseFuture getTasksByPatientSortedByStatus( + ResponseFuture< + GetTasksByPatientSortedByStatusResponse> getTasksByPatientSortedByStatus( GetTasksByPatientSortedByStatusRequest request, {CallOptions? options}) { - mapping(task) => GetTasksByPatientSortedByStatusResponse_Task( + mapping(task) => + GetTasksByPatientSortedByStatusResponse_Task( id: task.id, name: task.name, description: task.description, - dueAt: task.dueDate == null ? null : Timestamp.fromDateTime(task.dueDate!), + dueAt: task.dueDate == null ? null : Timestamp.fromDateTime( + task.dueDate!), createdBy: task.createdBy, - createdAt: task.creationDate == null ? null : Timestamp.fromDateTime(task.creationDate!), + 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, - )), + .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), + 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); + 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(), + id: DateTime + .now() + .millisecondsSinceEpoch + .toString(), name: request.name, notes: request.description, - patientId: request.patientId, + patient: LoadableCRUDObject(id: request.patientId), creationDate: DateTime.now(), - createdBy: OfflineClientStore().userStore.users[0].id, + // TODO update this to get the right creator + creator: LoadableCRUDObject( + id: OfflineClientStore().userStore.users[0].id), dueDate: request.hasDueAt() ? request.dueAt.toDateTime() : null, status: TasksGRPCTypeConverter.taskStatusFromGRPC(request.initialStatus), isPublicVisible: request.public, - assigneeId: request.assignedUserId, + assignee: LoadableCRUDObject(id: 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, - )); + id: DateTime + .now() + .millisecondsSinceEpoch + .toString(), + taskId: newTask.id, + name: subtask.name, + isDone: subtask.done, + )); } - final response = CreateTaskResponse()..id = newTask.id!; + final response = CreateTaskResponse() + ..id = newTask.id!; return MockResponseFuture.value(response); } @override - ResponseFuture updateTask(UpdateTaskRequest request, {CallOptions? options}) { - final update = TaskUpdate( + ResponseFuture updateTask(UpdateTaskRequest request, + {CallOptions? options}) { + final update = TaskCopyWithUpdate( id: request.id, name: request.name, status: TasksGRPCTypeConverter.taskStatusFromGRPC(request.status), @@ -368,36 +333,42 @@ class TaskOfflineClient extends TaskServiceClient { dueDate: request.hasDueAt() ? request.dueAt.toDateTime() : null, ); - OfflineClientStore().taskStore.update(update); + OfflineClientStore().taskStore.update(request.id, update); final response = UpdateTaskResponse(); return MockResponseFuture.value(response); } @override - ResponseFuture assignTask(AssignTaskRequest request, {CallOptions? options}) { - OfflineClientStore().taskStore.assignUser(request.taskId, request.userId); + ResponseFuture assignTask(AssignTaskRequest request, + {CallOptions? options}) { + OfflineClientStore().taskStore.update(request.taskId, + TaskCopyWithUpdate(assignee: LoadableCRUDObjectUpdate(id: request.userId))); final response = AssignTaskResponse(); return MockResponseFuture.value(response); } @override - ResponseFuture unassignTask(UnassignTaskRequest request, {CallOptions? options}) { - OfflineClientStore().taskStore.unassignUser(request.taskId, request.userId); + ResponseFuture unassignTask(UnassignTaskRequest request, + {CallOptions? options}) { + OfflineClientStore().taskStore.update(request.taskId, + TaskCopyWithUpdate(assignee: LoadableCRUDObjectUpdate(setToNull: true))); final response = UnassignTaskResponse(); return MockResponseFuture.value(response); } @override - ResponseFuture removeTaskDueDate(RemoveTaskDueDateRequest request, + ResponseFuture removeTaskDueDate( + RemoveTaskDueDateRequest request, {CallOptions? options}) { - OfflineClientStore().taskStore.removeDueDate(request.taskId); - final response = RemoveTaskDueDateResponse(); + OfflineClientStore().taskStore.update(request.taskId, TaskCopyWithUpdate(removeDueDate: true)); + final response = RemoveTaskDueDateResponse(); return MockResponseFuture.value(response); } @override - ResponseFuture deleteTask(DeleteTaskRequest request, {CallOptions? options}) { + ResponseFuture deleteTask(DeleteTaskRequest request, + {CallOptions? options}) { OfflineClientStore().taskStore.delete(request.id); final response = DeleteTaskResponse(); @@ -405,24 +376,30 @@ class TaskOfflineClient extends TaskServiceClient { } @override - ResponseFuture createSubtask(CreateSubtaskRequest request, {CallOptions? options}) { + 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(), + 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!; + final response = CreateSubtaskResponse() + ..subtaskId = subtask.id!; return MockResponseFuture.value(response); } @override - ResponseFuture updateSubtask(UpdateSubtaskRequest request, {CallOptions? options}) { + ResponseFuture updateSubtask( + UpdateSubtaskRequest request, {CallOptions? options}) { final requestSubtask = request.subtask; final update = SubtaskUpdate( id: request.subtaskId, @@ -436,7 +413,8 @@ class TaskOfflineClient extends TaskServiceClient { } @override - ResponseFuture deleteSubtask(DeleteSubtaskRequest request, {CallOptions? options}) { + ResponseFuture deleteSubtask( + DeleteSubtaskRequest request, {CallOptions? options}) { OfflineClientStore().subtaskStore.delete(request.subtaskId); final response = DeleteSubtaskResponse(); 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 f6359fb4..4fbcdbbb 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 @@ -96,7 +96,7 @@ class TaskTemplateSubtaskOfflineService { taskTemplateSubtasks = taskTemplateSubtasks.map((value) { if (value.id == templateSubtaskUpdate.id) { found = true; - return value.copyWith(name: templateSubtaskUpdate.name); + return value.copyWith(SubtaskUpdate(name: templateSubtaskUpdate.name)); } return value; }).toList(); 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 c920f0d9..f63c0c9f 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 @@ -55,7 +55,7 @@ class WardOfflineService { 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); + OfflineClientStore().roomStore.delete(element.id!); }); final taskTemplates = OfflineClientStore().taskTemplateStore.findTaskTemplates(wardId); for (var element in taskTemplates) { @@ -97,10 +97,11 @@ class WardOfflineClient extends WardServiceClient { .map((bed) => GetWardDetailsResponse_Bed(id: bed.id, name: bed.name)) .toList(); - return GetWardDetailsResponse_Room() - ..id = room.id - ..name = room.name - ..beds.addAll(beds); + return GetWardDetailsResponse_Room( + id: room.id, + name: room.name, + beds: beds, + ); }).toList(); final response = GetWardDetailsResponse( @@ -143,9 +144,9 @@ class WardOfflineClient extends WardServiceClient { final rooms = OfflineClientStore().roomStore.findRooms(ward.id); final beds = rooms.map((room) => OfflineClientStore().bedStore.findBeds(room.id)).expand((element) => element).toList(); - List patients = []; + List patients = []; for (var bed in beds) { - final patient = OfflineClientStore().patientStore.findPatient(bed.id); + final patient = OfflineClientStore().patientStore.findPatient(bed.id!); if (patient != null) { patients.add(patient); } @@ -175,9 +176,9 @@ class WardOfflineClient extends WardServiceClient { final rooms = OfflineClientStore().roomStore.findRooms(ward.id); final beds = rooms.map((room) => OfflineClientStore().bedStore.findBeds(room.id)).expand((element) => element).toList(); - List patients = []; + List patients = []; for (var bed in beds) { - final patient = OfflineClientStore().patientStore.findPatient(bed.id); + final patient = OfflineClientStore().patientStore.findPatient(bed.id!); if (patient != null) { patients.add(patient); } 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 3a4913d7..78474701 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 @@ -2,16 +2,18 @@ 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'; +import 'package:helpwave_service/src/util/crud_service_interface.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 { +class BedService implements CRUDInterface { /// The GRPC ServiceClient which handles GRPC BedServiceClient bedService = TasksAPIServiceClients().bedServiceClient; - Future getBed({required String id}) async { + @override + Future get(String id) async { GetBedRequest request = GetBedRequest(id: id); GetBedResponse response = await bedService.getBed( request, @@ -25,7 +27,7 @@ class BedService { ); } - Future> getBeds() async { + Future> getMany() async { GetBedsRequest request = GetBedsRequest(); GetBedsResponse response = await bedService.getBeds( request, @@ -61,45 +63,46 @@ class BedService { return beds; } - Future getBedAndRoomByPatient({required String patientId}) async { + Future getBedAndRoomByPatient({required String patientId}) async { GetBedByPatientRequest request = GetBedByPatientRequest(patientId: patientId); GetBedByPatientResponse response = await bedService.getBedByPatient( request, options: CallOptions(metadata: TasksAPIServiceClients().getMetaData()), ); - return RoomWithBedFlat( + return RoomAndBed( 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), + room: Room(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); + @override + Future create(Bed value) async { + CreateBedRequest request = CreateBedRequest(roomId: value.roomId, name: value.name); CreateBedResponse response = await bedService.createBed( request, options: CallOptions(metadata: TasksAPIServiceClients().getMetaData()), ); - return Bed(id: response.id, name: name, roomId: roomId); + return value.attachId(response.id); } - Future update({ - required String id, - String? name, - }) async { - UpdateBedRequest request = UpdateBedRequest(id: id, name: name); + @override + Future update(String id, BedUpdate? update) async { + UpdateBedRequest request = UpdateBedRequest(id: id, name: update?.name); await bedService.updateBed( request, options: CallOptions(metadata: TasksAPIServiceClients().getMetaData()), ); + return true; } - Future delete({required String id}) async { + @override + Future delete(String id) async { DeleteBedRequest request = DeleteBedRequest(id: id); await bedService.deleteBed( request, 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..722d8156 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 @@ -2,14 +2,18 @@ import 'package:grpc/grpc.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 'package:helpwave_service/src/util/crud_service_interface.dart'; +import 'package:helpwave_service/src/util/loadable_crud_object.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 [TasksAPIServiceClients] -class PatientService { +class PatientService + implements CRUDInterface { /// 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 @@ -32,25 +36,33 @@ class PatientService { id: task.id, name: task.name, notes: task.description, - status: TasksGRPCTypeConverter.taskStatusFromGRPC(task.status), + status: + TasksGRPCTypeConverter.taskStatusFromGRPC(task.status), isPublicVisible: task.public, - assigneeId: task.assignedUserId, + assignee: LoadableCRUDObject(id: task.assignedUserId), subtasks: task.subtasks - .map((subtask) => - Subtask(id: subtask.id, name: subtask.name, isDone: subtask.done, taskId: task.id)) + .map((subtask) => Subtask( + id: subtask.id, + name: subtask.name, + isDone: subtask.done, + taskId: task.id)) .toList(), - patientId: patient.id, + patient: LoadableCRUDObject(id: 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), + bed: patient.hasBed() + ? LoadableCRUDObject( + id: patient.bed.id, + data: Bed(id: patient.bed.id, name: patient.bed.name)) + : LoadableCRUDObject(), + room: patient.hasRoom() + ? LoadableCRUDObject( + id: patient.room.id, + data: Room(id: patient.room.id, name: patient.room.name)) + : LoadableCRUDObject(), ); - 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; } @@ -66,7 +78,8 @@ class PatientService { } /// Loads the [Patient]s by id - Future getPatient({required String patientId}) async { + @Deprecated("Use get instead") + Future getPatient({required String patientId}) async { GetPatientRequest request = GetPatientRequest(id: patientId); GetPatientResponse response = await patientService.getPatient( request, @@ -76,14 +89,16 @@ class PatientService { ); // TODO maybe also use bedId and notes from response - return PatientMinimal( + return Patient( id: response.id, name: response.humanReadableIdentifier, + isDischarged: false, // TODO remove this hardcoded value ); } /// Loads the [Patient]s with detailed information - Future getPatientDetails({required String patientId}) async { + @override + Future get(String patientId) async { GetPatientDetailsRequest request = GetPatientDetailsRequest(id: patientId); GetPatientDetailsResponse response = await patientService.getPatientDetails( request, @@ -99,30 +114,43 @@ class PatientService { isDischarged: response.isDischarged, tasks: response.tasks .map((task) => Task( - id: task.id, - name: task.name, - notes: task.description, - assigneeId: task.assignedUserId, - status: TasksGRPCTypeConverter.taskStatusFromGRPC(task.status), - isPublicVisible: task.public, - subtasks: task.subtasks - .map((subtask) => Subtask( + id: task.id, + name: task.name, + notes: task.description, + status: TasksGRPCTypeConverter.taskStatusFromGRPC(task.status), + isPublicVisible: task.public, + assignee: LoadableCRUDObject(id: task.assignedUserId), + subtasks: task.subtasks + .map((subtask) => Subtask( id: subtask.id, - taskId: task.id, name: subtask.name, - )) - .toList(), - patientId: response.id)) + isDone: subtask.done, + taskId: task.id)) + .toList(), + patient: LoadableCRUDObject(id: response.id), + // TODO due and creation date + )) .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, + bed: response.hasBed() + ? LoadableCRUDObject( + id: response.bed.id, + data: Bed(id: response.bed.id, name: response.bed.name)) + : LoadableCRUDObject(), + room: response.hasRoom() + ? LoadableCRUDObject( + id: response.room.id, + data: Room(id: response.room.id, name: response.room.name)) + : LoadableCRUDObject(), ); } /// Loads the [Room]s with [Bed]s and an optional patient in them - Future> getPatientAssignmentByWard({required String wardId}) async { - GetPatientAssignmentByWardRequest request = GetPatientAssignmentByWardRequest(wardId: wardId); - GetPatientAssignmentByWardResponse response = await patientService.getPatientAssignmentByWard( + Future> getPatientAssignmentByWard( + {required String wardId}) async { + GetPatientAssignmentByWardRequest request = + GetPatientAssignmentByWardRequest(wardId: wardId); + GetPatientAssignmentByWardResponse response = + await patientService.getPatientAssignmentByWard( request, options: CallOptions( metadata: TasksAPIServiceClients().getMetaData(), @@ -131,7 +159,7 @@ class PatientService { return response.rooms.map((room) { var beds = room.beds; - return RoomWithBedWithMinimalPatient( + return Room( id: room.id, name: room.id, beds: beds.map((bed) { @@ -140,14 +168,21 @@ class PatientService { id: bed.id, name: bed.name, roomId: room.id, - patient: patient.isInitialized() ? PatientMinimal(id: patient.id, name: patient.name) : null, + patient: patient.isInitialized() + ? Patient( + id: patient.id, + name: patient.name, + isDischarged: false, // TODO change this when more information are available + ) + : null, ); }).toList()); }).toList(); } /// Create a [Patient] - Future createPatient(Patient patient) async { + @override + Future create(Patient patient) async { CreatePatientRequest request = CreatePatientRequest( notes: patient.notes, humanReadableIdentifier: patient.name, @@ -157,15 +192,16 @@ class PatientService { options: CallOptions(metadata: TasksAPIServiceClients().getMetaData()), ); - return response.id; + return patient.attachId(response.id); } /// Update a [Patient] - Future updatePatient({required String id, String? notes, String? name}) async { + @override + Future update(String id, PatientUpdate? update) async { UpdatePatientRequest request = UpdatePatientRequest( id: id, - notes: notes, - humanReadableIdentifier: name, + notes: update?.notes, + humanReadableIdentifier: update?.name, ); UpdatePatientResponse response = await patientService.updatePatient( request, @@ -208,7 +244,8 @@ class PatientService { } /// Assigns a [Patient] to a [Bed] - Future assignBed({required String patientId, required String bedId}) async { + Future assignBed( + {required String patientId, required String bedId}) async { AssignBedRequest request = AssignBedRequest(id: patientId, bedId: bedId); AssignBedResponse response = await patientService.assignBed( request, @@ -220,4 +257,10 @@ class PatientService { } return false; } + + @Deprecated("Do not use this method. If you meant to discharge a Patient use discharge instead.") + @override + Future delete(String id) { + throw UnimplementedError(); + } } 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 785d159b..e6a86db5 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 @@ -2,40 +2,42 @@ 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_service_clients.dart'; +import 'package:helpwave_service/src/util/crud_service_interface.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 [TasksAPIServiceClients] -class RoomService { +class RoomService extends CRUDInterface { /// The GRPC ServiceClient which handles GRPC RoomServiceClient roomService = TasksAPIServiceClients().roomServiceClient; - Future get({required String roomId}) async { - GetRoomRequest request = GetRoomRequest(id: roomId); + @override + Future get(String id) async { + GetRoomRequest request = GetRoomRequest(id: id); GetRoomResponse response = await roomService.getRoom( request, options: CallOptions(metadata: TasksAPIServiceClients().getMetaData()), ); - RoomWithBedWithMinimalPatient rooms = RoomWithBedWithMinimalPatient( + Room rooms = Room( id: response.id, name: response.name, - beds: response.beds.map((bed) => Bed(id: bed.id, name: bed.name, roomId: roomId)).toList(), + beds: response.beds.map((bed) => Bed(id: bed.id, name: bed.name, roomId: id)).toList(), ); return rooms; } - Future> getRooms({required String wardId}) async { + Future> getMany({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( + List rooms = response.rooms + .map((room) => Room( id: room.id, name: room.name, beds: room.beds.map((bed) => Bed(id: bed.id, name: bed.name, roomId: room.id)).toList(), @@ -45,15 +47,15 @@ class RoomService { return rooms; } - Future> getRoomOverviews({required String wardId}) async { + Future> getRoomOverviews({required String wardId}) async { GetRoomOverviewsByWardRequest request = GetRoomOverviewsByWardRequest(id: wardId); GetRoomOverviewsByWardResponse response = await roomService.getRoomOverviewsByWard( request, options: CallOptions(metadata: TasksAPIServiceClients().getMetaData()), ); - List rooms = response.rooms - .map((room) => RoomWithBedWithMinimalPatient( + List rooms = response.rooms + .map((room) => Room( id: room.id, name: room.name, beds: room.beds @@ -63,9 +65,10 @@ class RoomService { roomId: room.id, patient: bed.hasPatient() // TODO bed.patient possibly has more information - ? PatientMinimal( + ? Patient( name: bed.patient.humanReadableIdentifier, id: bed.patient.id, + isDischarged: false, // TODO Fix this when it is provided ) : null, )) @@ -76,30 +79,30 @@ class RoomService { return rooms; } - Future createRoom({required String wardId, required String name}) async { - CreateRoomRequest request = CreateRoomRequest(wardId: wardId, name: name); + @override + Future create(Room value) async { + CreateRoomRequest request = CreateRoomRequest(wardId: value.wardId, name: value.name); CreateRoomResponse response = await roomService.createRoom( request, options: CallOptions(metadata: TasksAPIServiceClients().getMetaData()), ); - return RoomMinimal(id: response.id, name: name); + return value.attachId(response.id); } - Future update({ - required String id, - String? name, - }) async { - UpdateRoomRequest request = UpdateRoomRequest(id: id, name: name); + @override + Future update(String id, RoomUpdate? update) async { + UpdateRoomRequest request = UpdateRoomRequest(id: id, name: update?.name); await roomService.updateRoom( request, options: CallOptions(metadata: TasksAPIServiceClients().getMetaData()), ); + + return true; } - Future delete({ - required String id, - }) async { + @override + Future delete(String id) async { DeleteRoomRequest request = DeleteRoomRequest(id: id); await roomService.deleteRoom( 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 3ace4cbc..088ad90e 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 @@ -2,6 +2,7 @@ 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 'package:helpwave_service/src/util/loadable_crud_object.dart'; import '../util/task_status_mapping.dart'; import 'package:helpwave_service/util.dart'; @@ -9,13 +10,14 @@ import 'package:helpwave_service/util.dart'; /// /// Provides queries and requests that load or alter [Task] objects on the server /// The server is defined in the underlying [TasksAPIServiceClients] -class TaskService { +class TaskService implements CRUDInterface { /// The GRPC ServiceClient which handles GRPC TaskServiceClient taskService = TasksAPIServiceClients().taskServiceClient; /// Loads the [Task]s by a [Patient] identifier Future> getTasksByPatient({String? patientId}) async { - GetTasksByPatientRequest request = GetTasksByPatientRequest(patientId: patientId); + GetTasksByPatientRequest request = + GetTasksByPatientRequest(patientId: patientId); GetTasksByPatientResponse response = await taskService.getTasksByPatient( request, options: CallOptions(metadata: TasksAPIServiceClients().getMetaData()), @@ -28,7 +30,7 @@ class TaskService { notes: task.description, isPublicVisible: task.public, status: TasksGRPCTypeConverter.taskStatusFromGRPC(task.status), - assigneeId: task.assignedUserId, + assignee: LoadableCRUDObject(id: task.assignedUserId), dueDate: task.dueAt.toDateTime(), subtasks: task.subtasks .map((subtask) => Subtask( @@ -38,29 +40,37 @@ class TaskService { isDone: subtask.done, )) .toList(), - patientId: task.patientId, - createdBy: task.createdBy, + patient: LoadableCRUDObject(id: task.patientId), + creator: LoadableCRUDObject(id: task.createdBy), creationDate: task.createdAt.toDateTime())) .toList(); } /// Loads the [Task]s by it's identifier - Future getTask({String? id}) async { + @override + Future get(String id) async { GetTaskRequest request = GetTaskRequest(id: id); GetTaskResponse response = await taskService.getTask( request, options: CallOptions(metadata: TasksAPIServiceClients().getMetaData()), ); - return TaskWithPatient( + return Task( id: response.id, name: response.name, notes: response.description, isPublicVisible: response.public, status: TasksGRPCTypeConverter.taskStatusFromGRPC(response.status), - assigneeId: response.assignedUserId, + assignee: LoadableCRUDObject(id: response.assignedUserId), dueDate: response.dueAt.toDateTime(), - patient: PatientMinimal(id: response.patient.id, name: response.patient.humanReadableIdentifier), + patient: LoadableCRUDObject( + id: response.patient.id, + data: Patient( + id: response.patient.id, + name: response.patient.humanReadableIdentifier, + isDischarged: false, // TODO add when available + ), + ), subtasks: response.subtasks .map((subtask) => Subtask( id: subtask.id, @@ -69,14 +79,13 @@ class TaskService { isDone: subtask.done, )) .toList(), - patientId: response.patient.id, - createdBy: response.createdBy, + creator: LoadableCRUDObject(id: response.createdBy), creationDate: response.createdAt.toDateTime(), ); } /// Loads the [Task]s assigned to the current [User] - Future> getAssignedTasks() async { + Future> getAssignedTasks() async { GetAssignedTasksRequest request = GetAssignedTasksRequest(); GetAssignedTasksResponse response = await taskService.getAssignedTasks( request, @@ -84,15 +93,23 @@ class TaskService { ); return response.tasks - .map((task) => TaskWithPatient( + .map((task) => Task( id: task.id, name: task.name, notes: task.description, isPublicVisible: task.public, status: TasksGRPCTypeConverter.taskStatusFromGRPC(task.status), - assigneeId: task.assignedUserId, + assignee: LoadableCRUDObject(id: task.assignedUserId), dueDate: task.dueAt.toDateTime(), - patient: PatientMinimal(id: task.patient.id, name: task.patient.humanReadableIdentifier), + patient: LoadableCRUDObject( + id: task.patient.id, + data: Patient( + id: task.patient.id, + name: task.patient.humanReadableIdentifier, + isDischarged: + false, // TODO update when information is available + ), + ), subtasks: task.subtasks .map((subtask) => Subtask( id: subtask.id, @@ -101,23 +118,24 @@ class TaskService { isDone: subtask.done, )) .toList(), - patientId: task.patient.id, - createdBy: task.createdBy, + creator: LoadableCRUDObject(id: task.createdBy), creationDate: task.createdAt.toDateTime(), )) .toList(); } - Future createTask(TaskWithPatient task) async { + @override + Future create(Task task) async { CreateTaskRequest request = CreateTaskRequest( name: task.name, description: task.notes, initialStatus: TasksGRPCTypeConverter.taskStatusToGRPC(task.status), - dueAt: task.dueDate != null ? Timestamp.fromDateTime(task.dueDate!) : null, + 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 + assignedUserId: task.assignee.id, + subtasks: (task.subtasks ?? []) .map((subtask) => CreateTaskRequest_SubTask( name: subtask.name, done: subtask.isDone, @@ -129,19 +147,22 @@ class TaskService { options: CallOptions(metadata: TasksAPIServiceClients().getMetaData()), ); - return response.id; + return task.attachId(response.id); } /// Assign a [Task] to a [User] or unassign the [User] - Future changeAssignee({required String taskId, required String? userId}) async { + Future changeAssignee( + {required String taskId, required String? userId}) async { if (userId != null) { - AssignTaskRequest request = AssignTaskRequest(taskId: taskId, userId: userId); + AssignTaskRequest request = + AssignTaskRequest(taskId: taskId, userId: userId); await taskService.assignTask( request, options: CallOptions(metadata: TasksAPIServiceClients().getMetaData()), ); } else { - UnassignTaskRequest request = UnassignTaskRequest(taskId: taskId, userId: userId); + UnassignTaskRequest request = + UnassignTaskRequest(taskId: taskId, userId: userId); await taskService.unassignTask( request, options: CallOptions(metadata: TasksAPIServiceClients().getMetaData()), @@ -150,7 +171,8 @@ class TaskService { } /// Add a [Subtask] to a [Task] - Future createSubTask({required String taskId, required Subtask subTask}) async { + Future createSubTask( + {required String taskId, required Subtask subTask}) async { CreateSubtaskRequest request = CreateSubtaskRequest( taskId: taskId, subtask: CreateSubtaskRequest_Subtask( @@ -171,8 +193,10 @@ class TaskService { } /// Delete a [Subtask] by its identifier - Future deleteSubTask({required String subtaskId, required String taskId}) async { - DeleteSubtaskRequest request = DeleteSubtaskRequest(subtaskId: subtaskId, taskId: taskId); + 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: TasksAPIServiceClients().getMetaData()), @@ -182,11 +206,13 @@ class TaskService { } /// Update a [Subtask]'s - Future updateSubtask({required Subtask subtask, required taskId}) async { + 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), + subtask: UpdateSubtaskRequest_Subtask( + done: subtask.isDone, name: subtask.name), ); UpdateSubtaskResponse response = await taskService.updateSubtask( request, @@ -196,21 +222,18 @@ class TaskService { return response.isInitialized(); } - Future updateTask({ - required String taskId, - String? name, - String? notes, - DateTime? dueDate, - bool? isPublic, - TaskStatus? status, - }) async { + // TODO merge with remove due date + @override + Future update(String id, TaskUpdate? update) async { UpdateTaskRequest request = UpdateTaskRequest( - id: taskId, - name: name, - description: notes, - dueAt: dueDate != null ? Timestamp.fromDateTime(dueDate) : null, - public: isPublic, - status: status != null ? TasksGRPCTypeConverter.taskStatusToGRPC(status) : null, + id: id, + name: update?.name, + description: update?.notes, + dueAt: update?.dueDate != null ? Timestamp.fromDateTime(update!.dueDate!) : null, + public: update?.isPublicVisible, + status: update?.status != null + ? TasksGRPCTypeConverter.taskStatusToGRPC(update!.status!) + : null, ); UpdateTaskResponse response = await taskService.updateTask( @@ -230,4 +253,15 @@ class TaskService { options: CallOptions(metadata: TasksAPIServiceClients().getMetaData()), ); } + + @override + Future delete(String id) async { + DeleteTaskRequest request = DeleteTaskRequest(id: id); + await taskService.deleteTask( + request, + options: CallOptions(metadata: TasksAPIServiceClients().getMetaData()), + ); + + return true; + } } 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 64b85857..f8a906ef 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 @@ -3,8 +3,8 @@ import 'package:helpwave_service/src/util/index.dart'; import '../../../../user.dart'; /// The Controller for managing a [Organization] -class OrganizationController - extends LoadController { +class OrganizationController extends LoadController { OrganizationController({super.id, super.initialData}) : super(service: OrganizationService()); @@ -18,15 +18,21 @@ class OrganizationController await service.update(data.id!, update).then((value) { changeData(data.copyWith(update)); }); - bool affectsCurrentWardService = update?.shortName != null || update?.longName != null; + bool affectsCurrentWardService = + update?.shortName != null || update?.longName != null; - if (affectsCurrentWardService && CurrentWardService().currentWard?.organizationId == data.id) { - CurrentWardService().currentWard = CurrentWardService().currentWard!.copyWith( - organization: CurrentWardService().currentWard!.organization.copyWith(OrganizationUpdate( - shortName: update?.shortName, - longName: update?.longName, - )), - ); + if (affectsCurrentWardService && + CurrentWardService().currentWard?.organizationId == data.id) { + CurrentWardService().currentWard = + CurrentWardService().currentWard!.copyWith( + organization: CurrentWardService() + .currentWard! + .organization + .copyWith(OrganizationUpdate( + shortName: update?.shortName, + longName: update?.longName, + )), + ); } } 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 790fd5f1..aebf1097 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 @@ -50,7 +50,7 @@ class UserController extends ChangeNotifier { /// A function to load the [User] load() async { state = LoadingState.loading; - await UserService().getUser(id: user.id).then((value) {user = value;}).catchError((error, stackTrace) { + await UserService().get(id: user.id).then((value) {user = value;}).catchError((error, stackTrace) { errorMessage = error.toString(); state = LoadingState.error; }); 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 aa7d9bb3..abdbca2c 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 @@ -53,8 +53,7 @@ class OrganizationUpdate { } /// data class for [Organization] -class Organization extends IdentifiedObject - implements CRUDObject { +class Organization extends IdentifiedObject implements CRUDObject { String shortName; String longName; OrganizationDetails? details; @@ -71,7 +70,7 @@ class Organization extends IdentifiedObject ); @override - Organization create(String id) => copyWith(OrganizationUpdate(id: id)); + Organization attachId(String id) => copyWith(OrganizationUpdate(id: id)); @override Organization copyWith(OrganizationUpdate? update) { 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 85a6c635..2d5d1ba0 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,17 +1,27 @@ import 'package:helpwave_service/src/config.dart'; import 'package:helpwave_service/util.dart'; +class UserUpdate { + String? id; + String? name; + String? nickname; + String? email; + Uri? profileUrl; + + UserUpdate({this.id, this.name, this.nickname, this.email, this.profileUrl}); +} + /// data class for [User] -class User extends IdentifiedObject { +class User extends CRUDObject { String name; - String nickName; + String nickname; String email; Uri profileUrl; User({ super.id, required this.name, - required this.nickName, + required this.nickname, required this.email, required this.profileUrl, }); @@ -19,17 +29,18 @@ class User extends IdentifiedObject { factory User.empty({String? id}) => User( id: id, name: "User", - nickName: "", + nickname: "", email: "", profileUrl: Uri.parse(avatarFallBackURL), ); - 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, - email: email ?? this.email, + @override + User copyWith(UserUpdate? update) => User( + id: update?.id ?? id, + name: update?.name ?? name, + nickname: update?.nickname ?? nickname, + profileUrl: update?.profileUrl ?? profileUrl, + email: update?.email ?? email, ); @override @@ -43,6 +54,11 @@ class User extends IdentifiedObject { @override String toString() { - return "$runtimeType{id: $id, name: $name}"; + return "$runtimeType{id: $id, name: $name, email: $email}"; + } + + @override + User attachId(String id) { + return copyWith(UserUpdate(id: id)); } } 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 b9ac1a02..751a83d8 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 @@ -42,7 +42,8 @@ class OrganizationOfflineService { }).toList(); if (!found) { - throw Exception("UpdateOrganization: Could not find organization with id ${organizationUpdate.id}"); + throw Exception( + "UpdateOrganization: Could not find organization with id ${organizationUpdate.id}"); } } @@ -57,7 +58,8 @@ class OrganizationOfflineClient extends OrganizationServiceClient { OrganizationOfflineClient(super.channel); @override - ResponseFuture createOrganization(CreateOrganizationRequest request, + ResponseFuture createOrganization( + CreateOrganizationRequest request, {CallOptions? options}) { final newOrganization = Organization( id: DateTime.now().millisecondsSinceEpoch.toString(), @@ -73,19 +75,27 @@ class OrganizationOfflineClient extends OrganizationServiceClient { OfflineClientStore().organizationStore.create(newOrganization); - return MockResponseFuture.value(CreateOrganizationResponse(id: newOrganization.id)); + return MockResponseFuture.value( + CreateOrganizationResponse(id: newOrganization.id)); } @override - ResponseFuture getOrganization(GetOrganizationRequest request, {CallOptions? options}) { - final organization = OfflineClientStore().organizationStore.find(request.id); + 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}"); + throw Exception( + "GetOrganization: Could not find organization with id ${request.id}"); } - final members = - OfflineClientStore().userStore.findUsers().map((user) => GetOrganizationMember()..userId = user.id).toList(); + final members = OfflineClientStore() + .userStore + .findUsers() + .map((user) => GetOrganizationMember(userId: user.id)) + .toList(); return MockResponseFuture.value(GetOrganizationResponse( id: organization.id, @@ -99,13 +109,20 @@ class OrganizationOfflineClient extends OrganizationServiceClient { } @override - ResponseFuture getOrganizationsByUser(GetOrganizationsByUserRequest request, + ResponseFuture getOrganizationsByUser( + GetOrganizationsByUserRequest request, {CallOptions? options}) { - final organizations = OfflineClientStore().organizationStore.findOrganizations().map((org) { + final organizations = + OfflineClientStore().organizationStore.findOrganizations().map((org) { final members = OfflineClientStore() .userStore .findUsers() - .map((user) => GetOrganizationsByUserResponse_Organization_Member()..userId = user.id) + .map((user) => GetOrganizationsByUserResponse_Organization_Member( + userId: user.id, + email: user.email, + nickname: user.nickname, + avatarUrl: user.profileUrl.toString(), + )) .toList(); return GetOrganizationsByUserResponse_Organization( @@ -119,17 +136,21 @@ class OrganizationOfflineClient extends OrganizationServiceClient { ); }).toList(); - return MockResponseFuture.value(GetOrganizationsByUserResponse()..organizations.addAll(organizations)); + return MockResponseFuture.value( + GetOrganizationsByUserResponse()..organizations.addAll(organizations)); } @override - ResponseFuture getOrganizationsForUser(GetOrganizationsForUserRequest request, + ResponseFuture getOrganizationsForUser( + GetOrganizationsForUserRequest request, {CallOptions? options}) { - final organizations = OfflineClientStore().organizationStore.findOrganizations().map((org) { + final organizations = + OfflineClientStore().organizationStore.findOrganizations().map((org) { final members = OfflineClientStore() .userStore .findUsers() - .map((user) => GetOrganizationsForUserResponse_Organization_Member()..userId = user.id) + .map((user) => GetOrganizationsForUserResponse_Organization_Member( + userId: user.id)) .toList(); return GetOrganizationsForUserResponse_Organization( @@ -143,11 +164,13 @@ class OrganizationOfflineClient extends OrganizationServiceClient { ); }).toList(); - return MockResponseFuture.value(GetOrganizationsForUserResponse()..organizations.addAll(organizations)); + return MockResponseFuture.value( + GetOrganizationsForUserResponse()..organizations.addAll(organizations)); } @override - ResponseFuture updateOrganization(UpdateOrganizationRequest request, + ResponseFuture updateOrganization( + UpdateOrganizationRequest request, {CallOptions? options}) { final update = OrganizationUpdate( id: request.id, @@ -167,7 +190,8 @@ class OrganizationOfflineClient extends OrganizationServiceClient { } @override - ResponseFuture deleteOrganization(DeleteOrganizationRequest request, + ResponseFuture deleteOrganization( + DeleteOrganizationRequest request, {CallOptions? options}) { try { OfflineClientStore().organizationStore.delete(request.id); @@ -180,71 +204,92 @@ class OrganizationOfflineClient extends OrganizationServiceClient { // Other missing methods @override - ResponseFuture getMembersByOrganization(GetMembersByOrganizationRequest request, + ResponseFuture getMembersByOrganization( + GetMembersByOrganizationRequest request, {CallOptions? options}) { - final organization = OfflineClientStore().organizationStore.find(request.id); + final organization = + OfflineClientStore().organizationStore.find(request.id); if (organization == null) { - throw Exception("GetMembersByOrganization: Could not find organization with id ${request.id}"); + 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()) + .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)); + return MockResponseFuture.value( + GetMembersByOrganizationResponse()..members.addAll(members)); } @override - ResponseFuture getInvitationsByOrganization( - GetInvitationsByOrganizationRequest request, - {CallOptions? options}) { + 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 invitationState = + UserGRPCTypeConverter.invitationStateFromGRPC(request.state); + invitations = invitations + .where((element) => element.state == invitationState) + .toList(); } final response = GetInvitationsByOrganizationResponse( - invitations: invitations.map((invitation) => GetInvitationsByOrganizationResponse_Invitation( + invitations: invitations.map((invitation) => + GetInvitationsByOrganizationResponse_Invitation( id: invitation.id, organizationId: invitation.organizationId, email: invitation.email, - state: UserGRPCTypeConverter.invitationStateToGRPC(invitation.state), + state: + UserGRPCTypeConverter.invitationStateToGRPC(invitation.state), ))); return MockResponseFuture.value(response); } @override - ResponseFuture getInvitationsByUser(GetInvitationsByUserRequest request, + ResponseFuture getInvitationsByUser( + GetInvitationsByUserRequest request, {CallOptions? options}) { final user = OfflineClientStore().userStore.users[0]; - List invitations = - OfflineClientStore().invitationStore.invitations.where((element) => element.organizationId == user.id).toList(); + 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 invitationState = + UserGRPCTypeConverter.invitationStateFromGRPC(request.state); + invitations = invitations + .where((element) => element.state == invitationState) + .toList(); } - final response = GetInvitationsByUserResponse(invitations: invitations.map((invitation) { + 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( + final organization = OfflineClientStore() + .organizationStore + .find(invitation.organizationId)!; + invite.organization = + GetInvitationsByUserResponse_Invitation_Organization( id: organization.id, longName: organization.longName, avatarUrl: organization.details?.avatarURL, @@ -256,10 +301,17 @@ class OrganizationOfflineClient extends OrganizationServiceClient { } @override - ResponseFuture inviteMember(InviteMemberRequest request, {CallOptions? options}) { - assert(OfflineClientStore().userStore.users.indexWhere((element) => element.email == request.email) != -1); + 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) == + (element) => + element.organizationId == request.organizationId && + element.email == request.email) == -1); final invite = invite_types.Invitation( @@ -273,8 +325,11 @@ class OrganizationOfflineClient extends OrganizationServiceClient { } @override - ResponseFuture revokeInvitation(RevokeInvitationRequest request, {CallOptions? options}) { - final invite = OfflineClientStore().invitationStore.find(request.invitationId); + ResponseFuture revokeInvitation( + RevokeInvitationRequest request, + {CallOptions? options}) { + final invite = + OfflineClientStore().invitationStore.find(request.invitationId); if (invite == null) { throw "Invitation with id ${request.invitationId} not found"; @@ -282,14 +337,18 @@ class OrganizationOfflineClient extends OrganizationServiceClient { if (invite_types.InvitationState.pending != invite.state) { throw "Only pending Invitations can be revoked"; } - OfflineClientStore().invitationStore.changeState(request.invitationId, invite_types.InvitationState.accepted); + OfflineClientStore().invitationStore.changeState( + request.invitationId, invite_types.InvitationState.accepted); return MockResponseFuture.value(RevokeInvitationResponse()); } @override - ResponseFuture acceptInvitation(AcceptInvitationRequest request, {CallOptions? options}) { - final invite = OfflineClientStore().invitationStore.find(request.invitationId); + ResponseFuture acceptInvitation( + AcceptInvitationRequest request, + {CallOptions? options}) { + final invite = + OfflineClientStore().invitationStore.find(request.invitationId); if (invite == null) { throw "Invitation with id ${request.invitationId} not found"; @@ -297,15 +356,18 @@ class OrganizationOfflineClient extends OrganizationServiceClient { if (invite_types.InvitationState.pending != invite.state) { throw "Only pending Invitations can be accepted"; } - OfflineClientStore().invitationStore.changeState(request.invitationId, invite_types.InvitationState.accepted); + OfflineClientStore().invitationStore.changeState( + request.invitationId, invite_types.InvitationState.accepted); return MockResponseFuture.value(AcceptInvitationResponse()); } @override - ResponseFuture declineInvitation(DeclineInvitationRequest request, + ResponseFuture declineInvitation( + DeclineInvitationRequest request, {CallOptions? options}) { - final invite = OfflineClientStore().invitationStore.find(request.invitationId); + final invite = + OfflineClientStore().invitationStore.find(request.invitationId); if (invite == null) { throw "Invitation with id ${request.invitationId} not found"; @@ -313,18 +375,21 @@ class OrganizationOfflineClient extends OrganizationServiceClient { if (invite_types.InvitationState.pending != invite.state) { throw "Only pending Invitations can be de"; } - OfflineClientStore().invitationStore.changeState(request.invitationId, invite_types.InvitationState.accepted); + OfflineClientStore().invitationStore.changeState( + request.invitationId, invite_types.InvitationState.accepted); return MockResponseFuture.value(DeclineInvitationResponse()); } @override - ResponseFuture addMember(AddMemberRequest request, {CallOptions? options}) { + ResponseFuture addMember(AddMemberRequest request, + {CallOptions? options}) { return MockResponseFuture.value(AddMemberResponse()); } @override - ResponseFuture removeMember(RemoveMemberRequest request, {CallOptions? options}) { + ResponseFuture removeMember(RemoveMemberRequest request, + {CallOptions? options}) { return MockResponseFuture.value(RemoveMemberResponse()); } } 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 71bedbd3..b17fe3b0 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 @@ -4,12 +4,6 @@ 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 = []; @@ -35,7 +29,7 @@ class UserOfflineService { users = users.map((u) { if (u.id == user.id) { found = true; - return u.copyWith(); + return u.copyWith(UserUpdate()); } return u; }).toList(); @@ -54,22 +48,26 @@ class UserOfflineClient extends UserServiceClient { UserOfflineClient(super.channel); @override - ResponseFuture readPublicProfile(ReadPublicProfileRequest request, + 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}')); + 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(); + 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}) { + ResponseFuture readSelf(ReadSelfRequest request, + {CallOptions? options}) { final user = OfflineClientStore().userStore.users[0]; final organizations = OfflineClientStore() @@ -78,34 +76,37 @@ class UserOfflineClient extends UserServiceClient { .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); + final response = ReadSelfResponse( + id: user.id, + name: user.name, + nickname: user.nickname, + avatarUrl: user.profileUrl.toString(), + organizations: organizations, + ); return MockResponseFuture.value(response); } @override - ResponseFuture createUser(CreateUserRequest request, {CallOptions? options}) { + ResponseFuture createUser(CreateUserRequest request, + {CallOptions? options}) { final newUser = User( id: DateTime.now().millisecondsSinceEpoch.toString(), name: request.name, - nickName: request.nickname, + nickname: request.nickname, email: request.email, profileUrl: Uri.parse('https://helpwave.de/favicon.ico'), ); OfflineClientStore().userStore.create(newUser); - final response = CreateUserResponse()..id = newUser.id; + final response = CreateUserResponse(id: newUser.id); return MockResponseFuture.value(response); } @override - ResponseFuture updateUser(UpdateUserRequest request, {CallOptions? options}) { + ResponseFuture updateUser(UpdateUserRequest request, + {CallOptions? options}) { final update = UserUpdate(id: request.id); 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 5bcc2b4b..6893b1fe 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 @@ -121,7 +121,7 @@ class OrganizationService implements CRUDInterface getUser({String? id}) async { + Future get({String? id}) async { ReadPublicProfileRequest request = ReadPublicProfileRequest(id: id); ReadPublicProfileResponse response = await userService.readPublicProfile( request, @@ -27,7 +27,7 @@ class UserService { return User( id: response.id, name: response.name, - nickName: response.nickname, + nickname: response.nickname, email: "no-email", // TODO replace this profileUrl: Uri.parse(response.avatarUrl), ); @@ -47,7 +47,7 @@ class UserService { return User( id: response.id, name: response.name, - nickName: response.nickname, + nickname: response.nickname, email: "no-email", // TODO replace this profileUrl: Uri.parse(response.avatarUrl), ); diff --git a/packages/helpwave_service/lib/src/util/README.md b/packages/helpwave_service/lib/src/util/README.md new file mode 100644 index 00000000..9de00562 --- /dev/null +++ b/packages/helpwave_service/lib/src/util/README.md @@ -0,0 +1,31 @@ +# CRUD Objects + +## Problem +APIs usually provide the same interface consisting of CRUD-Operations (Create, Read, Update, Delete). +Creating state management that connects data classes and the API often leads to large amounts of similar looking boiler code. +To avoid this we will use generic abstractions for API services, Data Classes and State Management. + +## Assumptions +- Data Classes have a Identifier that is comparable via `==` +- Data Classes are immutable and can only be changed by copying +- Data Classes are only Identified when they exist on the Server (otherwise their identifier is null) +- Data Classes must have all properties present when initialized, properties that are not always loaded need to be moved to an extension to allow checking for their presence +- Updates only affect a single class (you cannot update a `Subtask` by updating a `Task`) +- The Create method only requires attributes present on the Data Class and affects the Data Class only in assigning an Identifier to it (or can be handled by applying a local update to the Data Class) +- Get and Delete only need the Identifier of the Object + +## Solution + +Our Solution consists of: + +- `CRUDObject`s for the DataClasses + - can be copied and updated through the use of a specified Update class + - can bes Extended trough `LoadableCRUDObject`s or `CRUDExtension`s +- `CRUDInterface`s for Services +- `LoadController` providing generic and extensible state management + +### CRUD Object +Example: +``` + +``` diff --git a/packages/helpwave_service/lib/src/util/crud_extension.dart b/packages/helpwave_service/lib/src/util/crud_extension.dart new file mode 100644 index 00000000..25a32f46 --- /dev/null +++ b/packages/helpwave_service/lib/src/util/crud_extension.dart @@ -0,0 +1,69 @@ +import 'package:helpwave_service/src/util/crud_object_interface.dart'; +import 'package:helpwave_service/src/util/copy_with_interface.dart'; + +class CrudExtensionUpdate< +IdentifierType, +Datatype extends CRUDObject, +UpdateType> { + Datatype? overwrite; + UpdateType? update; + IdentifierType? attachId; + bool remove; + + CrudExtensionUpdate({ + this.overwrite, + this.update, + this.attachId, + this.remove = false, + }) { + int countOfExclusiveOps = 0; + if (remove) countOfExclusiveOps++; + if (attachId != null) countOfExclusiveOps++; + if (update != null) countOfExclusiveOps++; + if (overwrite != null) countOfExclusiveOps++; + assert( + countOfExclusiveOps <= 1, """Only use 1 exclusive operation at a time. + 1. update + 2. remove + 3. overwrite + 4. attachId"""); + } +} + +/// A easy way of attaching arbitrary CRUD objects to your CRUD objects +class CRUDExtensionObject< +IdentifierType, +Datatype extends CRUDObject, +UpdateType> implements CopyWithInterface< + CRUDExtensionObject, + CrudExtensionUpdate> { + final Datatype? data; + + bool get isPresent => data != null; + + CRUDExtensionObject({this.data}) : assert(data?.id == null); + + @override + CRUDExtensionObject copyWith( + CrudExtensionUpdate? update, + ) { + if (update == null) { + return CRUDExtensionObject(data: data?.copyWith(null)); + } + if (update.remove) { + return CRUDExtensionObject(); + } else if (update.attachId != null) { + assert(isPresent, + "To attach an id you need to have data. Try overwriting instead."); + return CRUDExtensionObject(data: data?.attachId(update.attachId!)); + } else if (update.update != null) { + assert(data != null, + "To update data you need to have data. Try overwriting instead."); + return CRUDExtensionObject(data: data!.copyWith(update.update)); + } else if (update.overwrite != null) { + return CRUDExtensionObject(data: update.overwrite); + } + + return CRUDExtensionObject(); + } +} diff --git a/packages/helpwave_service/lib/src/util/crud_object_interface.dart b/packages/helpwave_service/lib/src/util/crud_object_interface.dart index 1b321d53..45f77ef8 100644 --- a/packages/helpwave_service/lib/src/util/crud_object_interface.dart +++ b/packages/helpwave_service/lib/src/util/crud_object_interface.dart @@ -1,9 +1,12 @@ import 'copy_with_interface.dart'; import 'identified_object.dart'; -abstract class CRUDObject extends IdentifiedObject +abstract class CRUDObject extends IdentifiedObject implements CopyWithInterface { - CRUDObject({super.id}); + const CRUDObject({super.id}); - DataType create(IdentifierType id); + // We need this method to indicate that the + /// A method that should attach the id to the data object thus setting its + /// creation status to isCreated = true + DataType attachId(IdentifierType id); } diff --git a/packages/helpwave_service/lib/src/util/identified_object.dart b/packages/helpwave_service/lib/src/util/identified_object.dart index bb8e6e53..fe8dbefb 100644 --- a/packages/helpwave_service/lib/src/util/identified_object.dart +++ b/packages/helpwave_service/lib/src/util/identified_object.dart @@ -1,10 +1,10 @@ /// A class for the identification of objects -class IdentifiedObject { - final T? id; +class IdentifiedObject { + final IdentifierType? id; - IdentifiedObject({this.id}); + const IdentifiedObject({this.id}); - bool isReferencingSame(IdentifiedObject other) { + bool isReferencingSame(IdentifiedObject other) { return runtimeType == other.runtimeType && this.id == other.id; } @@ -15,6 +15,6 @@ class IdentifiedObject { } /// A Extension to check the creation status of an [IdentifiedObject] -extension CreationExtension on 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 index 10ed2f74..0cbc82b1 100644 --- a/packages/helpwave_service/lib/src/util/index.dart +++ b/packages/helpwave_service/lib/src/util/index.dart @@ -3,3 +3,6 @@ export 'crud_object_interface.dart'; export 'crud_service_interface.dart'; export 'load_controller.dart'; export 'copy_with_interface.dart'; +export 'crud_extension.dart'; +export 'loadable_crud_object.dart'; +export 'list_update.dart'; diff --git a/packages/helpwave_service/lib/src/util/list_update.dart b/packages/helpwave_service/lib/src/util/list_update.dart new file mode 100644 index 00000000..a9378aeb --- /dev/null +++ b/packages/helpwave_service/lib/src/util/list_update.dart @@ -0,0 +1,51 @@ +import 'package:helpwave_service/src/util/crud_object_interface.dart'; + +typedef UpdateWrapper = ({IdentifierType id, UpdateType update}); + +class ListUpdate, UpdateType> { + List removeList = []; + List appendList = []; + List> updateList = []; + List? overwrite; + + ListUpdate({ + List? removeList, + List? appendList, + List>? updateList, + this.overwrite, + }) { + this.removeList = removeList ?? []; + this.appendList = appendList ?? []; + this.updateList = updateList ?? []; + } + + List? apply(List? list) { + if(overwrite != null) { + return overwrite; + } + + List reducedAppend = appendList.where((element) => !removeList.contains(element.id)).toList(); + List> reducedUpdate = updateList.where((element) => !removeList.contains(element.id)).toList(); + if(list == null) { + assert(reducedUpdate.isEmpty, "The updateList should be empty when the list is empty."); + return reducedAppend; + } + + List resultList = []; + for (var value in list + appendList) { + IdentifierType? id = value.id; + assert(value.id != null, "All values in the appendList and list must have a valid id, id != null"); + if(removeList.contains(id)){ + continue; + } + int? updateIndex = reducedUpdate.indexWhere((element) => element.id == id); + if(updateIndex != -1) { + resultList.add(value.copyWith(reducedUpdate[updateIndex].update)); + }else { + resultList.add(value); + } + } + + return resultList; + } +} diff --git a/packages/helpwave_service/lib/src/util/load_controller.dart b/packages/helpwave_service/lib/src/util/load_controller.dart index f39a8273..ab919af0 100644 --- a/packages/helpwave_service/lib/src/util/load_controller.dart +++ b/packages/helpwave_service/lib/src/util/load_controller.dart @@ -5,10 +5,11 @@ import 'crud_object_interface.dart'; abstract class LoadController< IdentifierType, - DataType extends CRUDObject, - CreateType, + DataType extends CRUDObject, + CopyType extends UpdateType, UpdateType, - ServiceType extends CRUDInterface> extends LoadingChangeNotifier { + ServiceType extends CRUDInterface> extends LoadingChangeNotifier { late final ServiceType service; late DataType _data; // Default value @@ -28,55 +29,55 @@ abstract class LoadController< } } - 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"); + 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); + _data = defaultData().attachId(id); } load(); } + // This cannot be abstracted as we cannot know a constructor for the DataType /// The default value for the DataType - DataType defaultData(); // This cannot be abstracted as we cannot know a constructor for the DataType + DataType defaultData(); + + Future createOp() async { + await service.create(data).then((value) { + _data = value; + }); + } /// 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; - }); - } + assert(data.isCreating, + "Only call $runtimeType.create when managing a new object."); 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); + Future loadOp() async { + if (isCreating) { + return; } - - loadHandler( - future: loadOp(), - ); + _data = await service.get(data.id as IdentifierType); } + /// A function to load the [DataType] + Future load() async => loadHandler(future: loadOp()); + /// Updates the [DataType] - Future update(UpdateType? update) async { + Future update(CopyType? update) async { updateOp() async { if (isCreating) { _data = data.copyWith(update); return; } await service.update(data.id as IdentifierType, update).then((value) { - if(value){ + if (value) { _data = data.copyWith(update); } }); @@ -87,7 +88,8 @@ abstract class LoadController< /// 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."); + assert(!data.isCreating, + "Only call $runtimeType.delete when managing an existing object."); deleteOp() async { // Due to the genericity a cast is better diff --git a/packages/helpwave_service/lib/src/util/loadable_crud_object.dart b/packages/helpwave_service/lib/src/util/loadable_crud_object.dart new file mode 100644 index 00000000..67c9f80b --- /dev/null +++ b/packages/helpwave_service/lib/src/util/loadable_crud_object.dart @@ -0,0 +1,131 @@ +import 'package:helpwave_service/src/util/crud_object_interface.dart'; + +class LoadableCRUDObjectUpdate< + IdentifierType, + Datatype extends CRUDObject, + UpdateType> { + /// Updates the id attribute and the id of the data object if present + IdentifierType? id; + + /// Overwrite the data attribute with the value and sets the id to the id of this object + /// + /// To remove the data use [removeLoadedData] or [remove] instead + Datatype? overwrite; + + /// Applies the update on the CRUD object + /// + /// When provided it is necessary to have the data object set + UpdateType? update; + + /// Removes the loaded data, but maintains the id if present + bool removeLoadedData; + + /// Removes the loaded data and sets the is present to false + bool remove; + + /// Actively set the value to null meaning the data is marked as present but has a value of null + bool setToNull; + + LoadableCRUDObjectUpdate({ + this.id, + this.overwrite, + this.update, + this.setToNull = false, + this.remove = false, + this.removeLoadedData = false, + }) { + int countOfExclusiveOps = 0; + if (remove) countOfExclusiveOps++; + if (removeLoadedData) countOfExclusiveOps++; + if (setToNull) countOfExclusiveOps++; + if (this.id != null) countOfExclusiveOps++; + if (this.update != null) countOfExclusiveOps++; + if (this.overwrite != null) countOfExclusiveOps++; + assert( + countOfExclusiveOps <= 1, """Only use 1 exclusive operation at a time. + 1. update + 2. id + 3. remove + 4. removeData + 5. setToNull + 6. overwrite"""); + } +} + +class LoadableCRUDObject< + IdentifierType, + Datatype extends CRUDObject, + UpdateType> + extends CRUDObject< + IdentifierType, + LoadableCRUDObject, + LoadableCRUDObjectUpdate> { + final Datatype? data; + + // TODO make this variable easier to understand or find a different method to allow explicit null values + /// Whether or not the data is absent + final bool isPresent; + + bool get isNull { + assert(!isPresent, "isNull can only be checked when isPresent = true."); + return !isNotNull; + } + + bool get isNotNull { + assert(!isPresent, "isNotNull can only be checked when isPresent = true."); + return id != null || hasDataValue; + } + + bool get hasDataValue => data != null; + + LoadableCRUDObject({IdentifierType? id, this.data, this.isPresent = true}) + : super(id: id ?? data?.id) { + assert(data?.id == null || data?.id == id, + "The data.id and id must match or data cannot be provided"); + assert(!isPresent && data == null && id == null, + "When isPresent = false neither data nor an id can be present."); + } + + @override + LoadableCRUDObject copyWith( + LoadableCRUDObjectUpdate? update, + ) { + if (update == null) { + return LoadableCRUDObject( + id: id, data: data?.copyWith(null), isPresent: isPresent); + } + if (update.remove) { + return LoadableCRUDObject(isPresent: false); + } else if (update.removeLoadedData) { + return LoadableCRUDObject(id: id, data: null); + } else if (update.setToNull) { + return LoadableCRUDObject(); + } else if (update.id != null) { + return LoadableCRUDObject( + id: update.id, + data: data?.attachId(update.id as IdentifierType), + ); + } else if (update.update != null) { + assert(data != null, + "To update data you need to have data. Try overwriting instead."); + Datatype updatedData = data!.copyWith(update.update); + return LoadableCRUDObject(id: updatedData.id, data: updatedData); + } else if (update.overwrite != null) { + return LoadableCRUDObject( + id: update.overwrite?.id, data: update.overwrite); + } + + return LoadableCRUDObject(id: id, data: data?.copyWith(null)); + } + + factory LoadableCRUDObject.nullObject() => + LoadableCRUDObject(isPresent: false); + + factory LoadableCRUDObject.notPresentObject() => LoadableCRUDObject(); + + @override + LoadableCRUDObject attachId( + IdentifierType id) { + return copyWith(LoadableCRUDObjectUpdate(id: id)); + } +} From 1bd1cb4a1104e1d1ad6696569ffc1ce1bf3edbc1 Mon Sep 17 00:00:00 2001 From: Felix Thape Date: Thu, 9 Jan 2025 13:42:32 +0100 Subject: [PATCH 2/3] fix: fix wrong assertions --- .../lib/src/util/load_controller.dart | 21 ++++++++++++------- .../lib/src/util/loadable_crud_object.dart | 6 +++--- 2 files changed, 17 insertions(+), 10 deletions(-) diff --git a/packages/helpwave_service/lib/src/util/load_controller.dart b/packages/helpwave_service/lib/src/util/load_controller.dart index ab919af0..676d3a28 100644 --- a/packages/helpwave_service/lib/src/util/load_controller.dart +++ b/packages/helpwave_service/lib/src/util/load_controller.dart @@ -29,14 +29,21 @@ abstract class LoadController< } } - 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"); + LoadController({ + IdentifierType? id, + DataType? initialData, + required this.service, + }) { + assert(id == null || id == initialData?.id, + "id and initialData must have the same id."); if (initialData != null) { - _data = data; - } else if (id != null) { - _data = defaultData().attachId(id); + _data = initialData; + } else { + if (id != null) { + _data = defaultData().attachId(id); + } else { + _data = defaultData(); + } } load(); } diff --git a/packages/helpwave_service/lib/src/util/loadable_crud_object.dart b/packages/helpwave_service/lib/src/util/loadable_crud_object.dart index 67c9f80b..04ac1466 100644 --- a/packages/helpwave_service/lib/src/util/loadable_crud_object.dart +++ b/packages/helpwave_service/lib/src/util/loadable_crud_object.dart @@ -67,12 +67,12 @@ class LoadableCRUDObject< final bool isPresent; bool get isNull { - assert(!isPresent, "isNull can only be checked when isPresent = true."); + assert(isPresent, "isNull can only be checked when isPresent = true."); return !isNotNull; } bool get isNotNull { - assert(!isPresent, "isNotNull can only be checked when isPresent = true."); + assert(isPresent, "isNotNull can only be checked when isPresent = true."); return id != null || hasDataValue; } @@ -82,7 +82,7 @@ class LoadableCRUDObject< : super(id: id ?? data?.id) { assert(data?.id == null || data?.id == id, "The data.id and id must match or data cannot be provided"); - assert(!isPresent && data == null && id == null, + assert(isPresent || (data == null && id == null), "When isPresent = false neither data nor an id can be present."); } From 82ed28c90bfab6eff66e885b403324db766e41e9 Mon Sep 17 00:00:00 2001 From: Felix Thape Date: Fri, 10 Jan 2025 15:36:03 +0100 Subject: [PATCH 3/3] feat: update ward and organization to use optional ids --- apps/tasks/android/app/build.gradle | 7 +- apps/tasks/android/gradle.properties | 2 +- .../gradle/wrapper/gradle-wrapper.properties | 4 +- apps/tasks/android/gradlew | 307 +++++++++++------- apps/tasks/android/gradlew.bat | 66 ++-- apps/tasks/android/settings.gradle | 2 +- .../organization_bottom_sheet.dart | 9 +- .../organization_members_bottom_sheet.dart | 2 +- .../patient_bottom_sheet.dart | 2 +- .../bottom_sheet_pages/task_bottom_sheet.dart | 2 +- .../bottom_sheet_pages/user_bottom_sheet.dart | 19 +- .../ward_select_bottom_sheet.dart | 8 +- apps/tasks/lib/components/user_header.dart | 4 +- apps/tasks/lib/main.dart | 6 +- .../tasks/lib/screens/ward_select_screen.dart | 20 +- .../src/api/offline/offline_client_store.dart | 23 +- .../tasks/controllers/ward_controller.dart | 32 +- .../lib/src/api/tasks/data_types/ward.dart | 94 ++++-- .../offline_clients/ward_offline_client.dart | 26 +- .../src/api/tasks/services/ward_service.dart | 27 +- .../controllers/organization_controller.dart | 32 +- .../src/api/user/data_types/organization.dart | 58 ++-- .../organization_offline_client.dart | 41 ++- .../api/user/services/organization_svc.dart | 30 +- .../lib/src/auth/authentication_utility.dart | 2 +- .../lib/src/auth/current_ward_svc.dart | 150 ++++----- .../lib/src/auth/identity.dart | 9 +- .../lib/src/auth/user_session_service.dart | 4 +- .../lib/src/util/crud_extension.dart | 97 ++++-- .../lib/src/util/identified_object.dart | 2 + .../lib/src/util/loadable_crud_object.dart | 28 +- 31 files changed, 631 insertions(+), 484 deletions(-) diff --git a/apps/tasks/android/app/build.gradle b/apps/tasks/android/app/build.gradle index db40a4e9..d5e2a06f 100644 --- a/apps/tasks/android/app/build.gradle +++ b/apps/tasks/android/app/build.gradle @@ -37,16 +37,17 @@ if (flutterVersionName == null) { } android { + namespace = "de.helpwave.tasks" compileSdkVersion versionProperties.getProperty('flutter.compileSdkVersion').toInteger() ndkVersion flutter.ndkVersion compileOptions { - sourceCompatibility JavaVersion.VERSION_1_8 - targetCompatibility JavaVersion.VERSION_1_8 + sourceCompatibility JavaVersion.VERSION_17 + targetCompatibility JavaVersion.VERSION_17 } kotlinOptions { - jvmTarget = '1.8' + jvmTarget = 17 } sourceSets { diff --git a/apps/tasks/android/gradle.properties b/apps/tasks/android/gradle.properties index 94adc3a3..598d13fe 100644 --- a/apps/tasks/android/gradle.properties +++ b/apps/tasks/android/gradle.properties @@ -1,3 +1,3 @@ -org.gradle.jvmargs=-Xmx1536M +org.gradle.jvmargs=-Xmx4G android.useAndroidX=true android.enableJetifier=true diff --git a/apps/tasks/android/gradle/wrapper/gradle-wrapper.properties b/apps/tasks/android/gradle/wrapper/gradle-wrapper.properties index dcf0f19c..cea7a793 100644 --- a/apps/tasks/android/gradle/wrapper/gradle-wrapper.properties +++ b/apps/tasks/android/gradle/wrapper/gradle-wrapper.properties @@ -1,5 +1,7 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-8.12-bin.zip +networkTimeout=10000 +validateDistributionUrl=true zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-7.6-all.zip diff --git a/apps/tasks/android/gradlew b/apps/tasks/android/gradlew index 9d82f789..1aa94a42 100755 --- a/apps/tasks/android/gradlew +++ b/apps/tasks/android/gradlew @@ -1,74 +1,127 @@ -#!/usr/bin/env bash +#!/bin/sh + +# +# Copyright © 2015-2021 the original authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# ############################################################################## -## -## Gradle start up script for UN*X -## +# +# Gradle start up script for POSIX generated by Gradle. +# +# Important for running: +# +# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is +# noncompliant, but you have some other compliant shell such as ksh or +# bash, then to run this script, type that shell name before the whole +# command line, like: +# +# ksh Gradle +# +# Busybox and similar reduced shells will NOT work, because this script +# requires all of these POSIX shell features: +# * functions; +# * expansions «$var», «${var}», «${var:-default}», «${var+SET}», +# «${var#prefix}», «${var%suffix}», and «$( cmd )»; +# * compound commands having a testable exit status, especially «case»; +# * various built-in commands including «command», «set», and «ulimit». +# +# Important for patching: +# +# (2) This script targets any POSIX shell, so it avoids extensions provided +# by Bash, Ksh, etc; in particular arrays are avoided. +# +# The "traditional" practice of packing multiple parameters into a +# space-separated string is a well documented source of bugs and security +# problems, so this is (mostly) avoided, by progressively accumulating +# options in "$@", and eventually passing that to Java. +# +# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, +# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; +# see the in-line comments for details. +# +# There are tweaks for specific operating systems such as AIX, CygWin, +# Darwin, MinGW, and NonStop. +# +# (3) This script is generated from the Groovy template +# https://github.com/gradle/gradle/blob/HEAD/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt +# within the Gradle project. +# +# You can find Gradle at https://github.com/gradle/gradle/. +# ############################################################################## -# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. -DEFAULT_JVM_OPTS="" +# Attempt to set APP_HOME + +# Resolve links: $0 may be a link +app_path=$0 + +# Need this for daisy-chained symlinks. +while + APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path + [ -h "$app_path" ] +do + ls=$( ls -ld "$app_path" ) + link=${ls#*' -> '} + case $link in #( + /*) app_path=$link ;; #( + *) app_path=$APP_HOME$link ;; + esac +done -APP_NAME="Gradle" -APP_BASE_NAME=`basename "$0"` +# This is normally unused +# shellcheck disable=SC2034 +APP_BASE_NAME=${0##*/} +# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036) +APP_HOME=$( cd "${APP_HOME:-./}" > /dev/null && pwd -P ) || exit # Use the maximum available, or set MAX_FD != -1 to use that value. -MAX_FD="maximum" +MAX_FD=maximum -warn ( ) { +warn () { echo "$*" -} +} >&2 -die ( ) { +die () { echo echo "$*" echo exit 1 -} +} >&2 # OS specific support (must be 'true' or 'false'). cygwin=false msys=false darwin=false -case "`uname`" in - CYGWIN* ) - cygwin=true - ;; - Darwin* ) - darwin=true - ;; - MINGW* ) - msys=true - ;; +nonstop=false +case "$( uname )" in #( + CYGWIN* ) cygwin=true ;; #( + Darwin* ) darwin=true ;; #( + MSYS* | MINGW* ) msys=true ;; #( + NONSTOP* ) nonstop=true ;; esac -# Attempt to set APP_HOME -# Resolve links: $0 may be a link -PRG="$0" -# Need this for relative symlinks. -while [ -h "$PRG" ] ; do - ls=`ls -ld "$PRG"` - link=`expr "$ls" : '.*-> \(.*\)$'` - if expr "$link" : '/.*' > /dev/null; then - PRG="$link" - else - PRG=`dirname "$PRG"`"/$link" - fi -done -SAVED="`pwd`" -cd "`dirname \"$PRG\"`/" >/dev/null -APP_HOME="`pwd -P`" -cd "$SAVED" >/dev/null - CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + # Determine the Java command to use to start the JVM. if [ -n "$JAVA_HOME" ] ; then if [ -x "$JAVA_HOME/jre/sh/java" ] ; then # IBM's JDK on AIX uses strange locations for the executables - JAVACMD="$JAVA_HOME/jre/sh/java" + JAVACMD=$JAVA_HOME/jre/sh/java else - JAVACMD="$JAVA_HOME/bin/java" + JAVACMD=$JAVA_HOME/bin/java fi if [ ! -x "$JAVACMD" ] ; then die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME @@ -77,84 +130,120 @@ Please set the JAVA_HOME variable in your environment to match the location of your Java installation." fi else - JAVACMD="java" - which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + JAVACMD=java + if ! command -v java >/dev/null 2>&1 + then + die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. Please set the JAVA_HOME variable in your environment to match the location of your Java installation." + fi fi # Increase the maximum file descriptors if we can. -if [ "$cygwin" = "false" -a "$darwin" = "false" ] ; then - MAX_FD_LIMIT=`ulimit -H -n` - if [ $? -eq 0 ] ; then - if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then - MAX_FD="$MAX_FD_LIMIT" - fi - ulimit -n $MAX_FD - if [ $? -ne 0 ] ; then - warn "Could not set maximum file descriptor limit: $MAX_FD" - fi - else - warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" - fi +if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then + case $MAX_FD in #( + max*) + # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC2039,SC3045 + MAX_FD=$( ulimit -H -n ) || + warn "Could not query maximum file descriptor limit" + esac + case $MAX_FD in #( + '' | soft) :;; #( + *) + # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC2039,SC3045 + ulimit -n "$MAX_FD" || + warn "Could not set maximum file descriptor limit to $MAX_FD" + esac fi -# For Darwin, add options to specify how the application appears in the dock -if $darwin; then - GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" -fi +# Collect all arguments for the java command, stacking in reverse order: +# * args from the command line +# * the main class name +# * -classpath +# * -D...appname settings +# * --module-path (only if needed) +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. + +# For Cygwin or MSYS, switch paths to Windows format before running java +if "$cygwin" || "$msys" ; then + APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) + CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) + + JAVACMD=$( cygpath --unix "$JAVACMD" ) -# For Cygwin, switch paths to Windows format before running java -if $cygwin ; then - APP_HOME=`cygpath --path --mixed "$APP_HOME"` - CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` - JAVACMD=`cygpath --unix "$JAVACMD"` - - # We build the pattern for arguments to be converted via cygpath - ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` - SEP="" - for dir in $ROOTDIRSRAW ; do - ROOTDIRS="$ROOTDIRS$SEP$dir" - SEP="|" - done - OURCYGPATTERN="(^($ROOTDIRS))" - # Add a user-defined pattern to the cygpath arguments - if [ "$GRADLE_CYGPATTERN" != "" ] ; then - OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" - fi # Now convert the arguments - kludge to limit ourselves to /bin/sh - i=0 - for arg in "$@" ; do - CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` - CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option - - if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition - eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` - else - eval `echo args$i`="\"$arg\"" + for arg do + if + case $arg in #( + -*) false ;; # don't mess with options #( + /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath + [ -e "$t" ] ;; #( + *) false ;; + esac + then + arg=$( cygpath --path --ignore --mixed "$arg" ) fi - i=$((i+1)) + # Roll the args list around exactly as many times as the number of + # args, so each arg winds up back in the position where it started, but + # possibly modified. + # + # NB: a `for` loop captures its iteration list before it begins, so + # changing the positional parameters here affects neither the number of + # iterations, nor the values presented in `arg`. + shift # remove old arg + set -- "$@" "$arg" # push replacement arg done - case $i in - (0) set -- ;; - (1) set -- "$args0" ;; - (2) set -- "$args0" "$args1" ;; - (3) set -- "$args0" "$args1" "$args2" ;; - (4) set -- "$args0" "$args1" "$args2" "$args3" ;; - (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; - (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; - (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; - (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; - (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; - esac fi -# Split up the JVM_OPTS And GRADLE_OPTS values into an array, following the shell quoting and substitution rules -function splitJvmOpts() { - JVM_OPTS=("$@") -} -eval splitJvmOpts $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS -JVM_OPTS[${#JVM_OPTS[*]}]="-Dorg.gradle.appname=$APP_BASE_NAME" -exec "$JAVACMD" "${JVM_OPTS[@]}" -classpath "$CLASSPATH" org.gradle.wrapper.GradleWrapperMain "$@" +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' + +# Collect all arguments for the java command: +# * DEFAULT_JVM_OPTS, JAVA_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments, +# and any embedded shellness will be escaped. +# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be +# treated as '${Hostname}' itself on the command line. + +set -- \ + "-Dorg.gradle.appname=$APP_BASE_NAME" \ + -classpath "$CLASSPATH" \ + org.gradle.wrapper.GradleWrapperMain \ + "$@" + +# Stop when "xargs" is not available. +if ! command -v xargs >/dev/null 2>&1 +then + die "xargs is not available" +fi + +# Use "xargs" to parse quoted args. +# +# With -n1 it outputs one arg per line, with the quotes and backslashes removed. +# +# In Bash we could simply go: +# +# readarray ARGS < <( xargs -n1 <<<"$var" ) && +# set -- "${ARGS[@]}" "$@" +# +# but POSIX shell has neither arrays nor command substitution, so instead we +# post-process each arg (as a line of input to sed) to backslash-escape any +# character that might be a shell metacharacter, then use eval to reverse +# that process (while maintaining the separation between arguments), and wrap +# the whole thing up as a single "set" statement. +# +# This will of course break if any of these variables contains a newline or +# an unmatched quote. +# + +eval "set -- $( + printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | + xargs -n1 | + sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | + tr '\n' ' ' + )" '"$@"' + +exec "$JAVACMD" "$@" diff --git a/apps/tasks/android/gradlew.bat b/apps/tasks/android/gradlew.bat index aec99730..6689b85b 100755 --- a/apps/tasks/android/gradlew.bat +++ b/apps/tasks/android/gradlew.bat @@ -1,4 +1,20 @@ -@if "%DEBUG%" == "" @echo off +@rem +@rem Copyright 2015 the original author or authors. +@rem +@rem Licensed under the Apache License, Version 2.0 (the "License"); +@rem you may not use this file except in compliance with the License. +@rem You may obtain a copy of the License at +@rem +@rem https://www.apache.org/licenses/LICENSE-2.0 +@rem +@rem Unless required by applicable law or agreed to in writing, software +@rem distributed under the License is distributed on an "AS IS" BASIS, +@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +@rem See the License for the specific language governing permissions and +@rem limitations under the License. +@rem + +@if "%DEBUG%"=="" @echo off @rem ########################################################################## @rem @rem Gradle startup script for Windows @@ -8,20 +24,24 @@ @rem Set local scope for the variables with windows NT shell if "%OS%"=="Windows_NT" setlocal -@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. -set DEFAULT_JVM_OPTS= - set DIRNAME=%~dp0 -if "%DIRNAME%" == "" set DIRNAME=. +if "%DIRNAME%"=="" set DIRNAME=. +@rem This is normally unused set APP_BASE_NAME=%~n0 set APP_HOME=%DIRNAME% +@rem Resolve any "." and ".." in APP_HOME to make it shorter. +for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" + @rem Find java.exe if defined JAVA_HOME goto findJavaFromJavaHome set JAVA_EXE=java.exe %JAVA_EXE% -version >NUL 2>&1 -if "%ERRORLEVEL%" == "0" goto init +if %ERRORLEVEL% equ 0 goto execute echo. echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. @@ -35,7 +55,7 @@ goto fail set JAVA_HOME=%JAVA_HOME:"=% set JAVA_EXE=%JAVA_HOME%/bin/java.exe -if exist "%JAVA_EXE%" goto init +if exist "%JAVA_EXE%" goto execute echo. echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% @@ -45,44 +65,26 @@ echo location of your Java installation. goto fail -:init -@rem Get command-line arguments, handling Windowz variants - -if not "%OS%" == "Windows_NT" goto win9xME_args -if "%@eval[2+2]" == "4" goto 4NT_args - -:win9xME_args -@rem Slurp the command line arguments. -set CMD_LINE_ARGS= -set _SKIP=2 - -:win9xME_args_slurp -if "x%~1" == "x" goto execute - -set CMD_LINE_ARGS=%* -goto execute - -:4NT_args -@rem Get arguments from the 4NT Shell from JP Software -set CMD_LINE_ARGS=%$ - :execute @rem Setup the command line set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + @rem Execute Gradle -"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* :end @rem End local scope for the variables with windows NT shell -if "%ERRORLEVEL%"=="0" goto mainEnd +if %ERRORLEVEL% equ 0 goto mainEnd :fail rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of rem the _cmd.exe /c_ return code! -if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 -exit /b 1 +set EXIT_CODE=%ERRORLEVEL% +if %EXIT_CODE% equ 0 set EXIT_CODE=1 +if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% +exit /b %EXIT_CODE% :mainEnd if "%OS%"=="Windows_NT" endlocal diff --git a/apps/tasks/android/settings.gradle b/apps/tasks/android/settings.gradle index d0f044b6..c5ece8ff 100644 --- a/apps/tasks/android/settings.gradle +++ b/apps/tasks/android/settings.gradle @@ -18,7 +18,7 @@ pluginManagement { plugins { id "dev.flutter.flutter-plugin-loader" version "1.0.0" - id "com.android.application" version "7.3.0" apply false + id "com.android.application" version "8.8.0" apply false id "org.jetbrains.kotlin.android" version "2.0.21" apply false } 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 1eeef1a7..feecd915 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 @@ -1,6 +1,7 @@ import 'package:flutter/material.dart'; import 'package:helpwave_localization/localization.dart'; import 'package:helpwave_service/user.dart'; +import 'package:helpwave_service/util.dart'; import 'package:helpwave_theme/constants.dart'; import 'package:helpwave_theme/util.dart'; import 'package:helpwave_widget/bottom_sheets.dart'; @@ -59,9 +60,13 @@ class OrganizationBottomSheetPage extends StatelessWidget { Text(context.localization.contactEmail, style: context.theme.textTheme.titleMedium), const SizedBox(height: distanceTiny), TextFormFieldWithTimer( - initialValue: controller.data.details?.email, + initialValue: controller.data.details.data?.email, // TODO validation - onUpdate: (value) => controller.update(OrganizationUpdate(email: value)), + onUpdate: (value) => controller.update(OrganizationUpdate( + details: CrudExtensionUpdate( + update: OrganizationDetailsUpdate(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/organization_members_bottom_sheet.dart b/apps/tasks/lib/components/bottom_sheet_pages/organization_members_bottom_sheet.dart index 172facfb..bf7725b2 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 @@ -25,7 +25,7 @@ class OrganizationMembersBottomSheetPage extends StatelessWidget { mainAxisSize: MainAxisSize.min, children: [ Text(context.localization.members, style: context.theme.textTheme.titleMedium), - Text(CurrentWardService().currentWard?.organizationName ?? "", + Text(CurrentWardService().currentWard?.organization.data?.combinedName ?? "", style: TextStyle(color: context.theme.hintColor)), ], ), 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 23eb60a6..7781bda5 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 @@ -30,7 +30,7 @@ class PatientBottomSheet extends StatefulWidget { class _PatientBottomSheetState extends State { Future> loadRoomsWithBeds({String? patientId}) async { List rooms = await RoomService() - .getRoomOverviews(wardId: CurrentWardService().currentWard!.wardId); + .getRoomOverviews(wardId: CurrentWardService().currentWard!.id!); List flattenedRooms = []; for (Room room in rooms) { 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 d0faf84c..363d421f 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 @@ -214,7 +214,7 @@ class _TaskBottomSheetState extends State { builder: (BuildContext context) => AssigneeSelectBottomSheet( users: OrganizationService().getMembersByOrganization( - CurrentWardService().currentWard!.organizationId), + CurrentWardService().currentWard!.organization.id!), onChanged: (User? assignee) { taskController.changeAssignee(assignee); Navigator.pop(context); 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 305735ee..d1f24fcd 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 @@ -1,6 +1,7 @@ import 'package:flutter/material.dart'; import 'package:helpwave_localization/localization.dart'; import 'package:helpwave_service/user.dart'; +import 'package:helpwave_service/util.dart'; import 'package:helpwave_theme/constants.dart'; import 'package:helpwave_theme/util.dart'; import 'package:helpwave_widget/bottom_sheets.dart'; @@ -55,7 +56,7 @@ class UserBottomSheetPage extends StatelessWidget { }), Consumer( builder: (context, currentWardController, __) => Text( - currentWardController.currentWard?.organizationName ?? context.localization.loading, + currentWardController.currentWard?.organization.data?.combinedName ?? context.localization.loading, style: TextStyle( fontSize: fontSizeSmall, color: context.theme.hintColor, @@ -77,7 +78,7 @@ class UserBottomSheetPage extends StatelessWidget { children: [ Consumer(builder: (context, currentWardController, __) { return Text( - currentWardController.currentWard!.wardName, + currentWardController.currentWard!.name, style: context.theme.textTheme.labelLarge, ); }), @@ -88,12 +89,14 @@ class UserBottomSheetPage extends StatelessWidget { context.pushModal( context: context, builder: (context) => WardSelectBottomSheet( - selectedWardId: CurrentWardService().currentWard!.wardId, - onChange: (WardMinimal ward) { - CurrentWardService().currentWard = CurrentWardInformation( - ward, - CurrentWardService().currentWard!.organization, - ); + selectedWardId: CurrentWardService().currentWard!.id, + onChange: (Ward ward) { + CurrentWardService().changeCurrentWard(ward.copyWith( + WardUpdate( + organization: LoadableCRUDObjectUpdate( + overwrite: CurrentWardService().currentWard!.organization.data), + ), + )); Navigator.pop(context); }, ), 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 db2816bc..8b1761cc 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 @@ -6,17 +6,17 @@ import 'package:helpwave_widget/bottom_sheets.dart'; import 'package:helpwave_widget/loading.dart'; class WardSelectBottomSheet extends StatelessWidget { - /// The currently selected [WardMinimal] + /// The currently selected [Ward] /// - /// This is used to highlight the [WardMinimal] in the [List] + /// This is used to highlight the [Ward] in the [List] final String? selectedWardId; - /// The [Organization] identifier for which all [WardMinimal]s are loaded + /// The [Organization] identifier for which all [Ward]s are loaded /// /// If unspecified the organizationId of the [CurrentWardService] is taken final String? organizationId; - final void Function(WardMinimal ward) onChange; + final void Function(Ward ward) onChange; const WardSelectBottomSheet({super.key, this.selectedWardId, this.organizationId, required this.onChange}); diff --git a/apps/tasks/lib/components/user_header.dart b/apps/tasks/lib/components/user_header.dart index 5db1083f..5eed233d 100644 --- a/apps/tasks/lib/components/user_header.dart +++ b/apps/tasks/lib/components/user_header.dart @@ -57,14 +57,14 @@ class UserHeader extends StatelessWidget implements PreferredSizeWidget { builder: (context, currentWardController, __) => Row( children: [ Text( - "${currentWardController.currentWard?.organization.shortName ?? ""} - ", + "${currentWardController.currentWard?.organization.data?.shortName ?? ""} - ", style: TextStyle( color: context.theme.colorScheme.onSurface.withOpacity(0.7), fontSize: 14, ), ), Text( - currentWardController.currentWard?.wardName ?? "", + currentWardController.currentWard?.name ?? "", overflow: TextOverflow.fade, style: const TextStyle( fontWeight: FontWeight.bold, diff --git a/apps/tasks/lib/main.dart b/apps/tasks/lib/main.dart index e101ec86..00574718 100644 --- a/apps/tasks/lib/main.dart +++ b/apps/tasks/lib/main.dart @@ -11,9 +11,9 @@ import 'package:tasks/config/config.dart'; import 'package:helpwave_service/tasks.dart'; import 'package:tasks/screens/login_screen.dart'; -void main() { +void main() async { WidgetsFlutterBinding.ensureInitialized(); - CurrentWardService().devMode = isUsingOfflineClients; + await CurrentWardService().changeDevMode(isUsingOfflineClients); TasksAPIServiceClients() ..apiUrl = usedAPIURL ..offlineMode = isUsingOfflineClients; @@ -25,7 +25,7 @@ void main() { // ..offlineMode = isUsingOfflineClients; UserSessionService().changeMode(isUsingDevModeLogin); // TODO the line below enables direct login, but behaves somewhat weirdly - UserSessionService().tokenLogin().then((value) => CurrentWardService().fakeLogin()); + await UserSessionService().tokenLogin().then((value) => CurrentWardService().fakeLogin()); runApp(const MyApp()); } diff --git a/apps/tasks/lib/screens/ward_select_screen.dart b/apps/tasks/lib/screens/ward_select_screen.dart index def1d8dd..0282c7c8 100644 --- a/apps/tasks/lib/screens/ward_select_screen.dart +++ b/apps/tasks/lib/screens/ward_select_screen.dart @@ -3,6 +3,7 @@ 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_service/util.dart'; import 'package:helpwave_theme/constants.dart'; import 'package:helpwave_widget/content_selection.dart'; import 'package:helpwave_widget/widgets.dart'; @@ -19,7 +20,7 @@ class WardSelectScreen extends StatefulWidget { class _WardSelectScreen extends State { Organization? organization; - WardMinimal? ward; + Ward? ward; @override Widget build(BuildContext context) { @@ -43,7 +44,7 @@ class _WardSelectScreen extends State { body: Column( children: [ ForwardNavigationTile( - title:organization?.longName ?? context.localization.none, + title: organization?.longName ?? context.localization.none, subtitle: context.localization.organization, onTap: () => Navigator.push( context, @@ -74,14 +75,13 @@ class _WardSelectScreen extends State { subtitle: context.localization.ward, onTap: organization == null ? null - : () => Navigator.push( + : () => Navigator.push( context, MaterialPageRoute( - builder: (context) => ListSearch( + builder: (context) => ListSearch( title: context.localization.ward, - asyncItems: (_) async => - await WardService().getWards(organizationId: organization!.id), - elementToString: (WardMinimal ward) => ward.name, + asyncItems: (_) async => await WardService().getWards(organizationId: organization!.id), + elementToString: (Ward ward) => ward.name, ), ), ).then((value) { @@ -99,7 +99,11 @@ class _WardSelectScreen extends State { if (ward == null || organization == null) { return; } - currentWardService.currentWard = CurrentWardInformation(ward!, organization!); + currentWardService.changeCurrentWard( + ward!.copyWith(WardUpdate( + organization: LoadableCRUDObjectUpdate(overwrite: organization!), + )), + ); }, child: Text(context.localization.switch_), ), 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 4f1c1fae..4cb5d9c4 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 @@ -19,11 +19,13 @@ final List initialOrganizations = [ id: "organization1", shortName: "MK", longName: "Musterklinikum", - details: OrganizationDetails( - avatarURL: profileUrl, - email: "test@helpwave.de", - isPersonal: false, - isVerified: true, + details: CRUDExtension( + data: OrganizationDetails( + avatarURL: profileUrl, + email: "test@helpwave.de", + isPersonal: false, + isVerified: true, + ), ), ), ]; @@ -65,13 +67,16 @@ 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!))) + .map((organization) => range(0, 3).map((index) => Ward( + id: "${organization.id}${index + 1}", + name: "Ward ${index + 1}", + organization: LoadableCRUDObject(id: organization.id!), + ))) .expand((element) => element) .toList(); final List initialRooms = initialWards - .map((ward) => range(0, 2) - .map((index) => Room(id: "${ward.id}${index + 1}", name: "Room ${index + 1}", wardId: ward.id))) + .map((ward) => + range(0, 2).map((index) => Room(id: "${ward.id}${index + 1}", name: "Room ${index + 1}", wardId: ward.id))) .expand((element) => element) .toList(); final List initialBeds = initialRooms 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 306fb7f6..64e60e1a 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 @@ -3,25 +3,25 @@ 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] +/// The Controller for managing a [Ward] /// -/// 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 +/// Providing a [wardId] means loading and synchronising the [Ward]s with +/// the backend while no [wardId] or a empty [String] means that the [Ward] is /// only used locally class WardController extends LoadingChangeNotifier { - /// The [WardMinimal] - WardMinimal _ward = WardMinimal(name: "Ward"); + /// The [Ward] + Ward _ward = Ward(name: "Ward"); - WardMinimal get ward => _ward; + Ward get ward => _ward; - set ward(WardMinimal value) { + set ward(Ward value) { _ward = value; notifyListeners(); } bool get isCreating => ward.isCreating; - WardController({String? id, WardMinimal? ward}) { + WardController({String? id, Ward? 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", @@ -29,12 +29,12 @@ class WardController extends LoadingChangeNotifier { if (ward != null) { _ward = ward; } else if (id != null) { - _ward = _ward.copyWith(id: id); + _ward = _ward.copyWith(WardUpdate(id: id)); } load(); } - /// Loads the [WardMinimal]s + /// Loads the [Ward]s Future load() async { loadOp() async { if (isCreating) { @@ -46,17 +46,17 @@ class WardController extends LoadingChangeNotifier { loadHandler(future: loadOp()); } - /// Delete the [WardMinimal] by the id + /// Delete the [Ward] 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); + await WardService().delete(id: ward.id!); } loadHandler(future: deleteOp()); } - /// Add the [WardMinimal] + /// Add the [Ward] Future create() async { assert(isCreating); createOp() async { @@ -70,14 +70,14 @@ class WardController extends LoadingChangeNotifier { Future update({String? name}) async { if (isCreating) { - ward.name = name ?? ward.name; + ward = ward.copyWith(WardUpdate(name: 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; + await WardService().update(id: ward.id!, name: name); + ward = ward.copyWith(WardUpdate(name: name)); notifyListeners(); } 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 77e421f5..8605ebba 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,53 +1,75 @@ +import 'package:helpwave_service/src/api/user/data_types/index.dart'; import 'package:helpwave_service/util.dart'; -/// data class for [Ward] -class WardMinimal extends IdentifiedObject { - String name; +class WardOverviewUpdate { + int? bedCount; + int? tasksInTodo; + int? tasksInInProgress; + int? tasksInDone; - WardMinimal({ - super.id, - required this.name, - }); - - - WardMinimal copyWith({ - String? id, - String? name, - }) { - return WardMinimal( - id: id ?? this.id, - name: name ?? this.name, - ); - } - - @override - String toString() { - return "$runtimeType{id: $id, name: $name}"; - } -} - -class Ward extends WardMinimal { - String organizationId; - - Ward({ - super.id, - required super.name, - required this.organizationId, - }); + WardOverviewUpdate({this.bedCount, this.tasksInTodo, this.tasksInInProgress, this.tasksInDone}); } -class WardOverview extends WardMinimal { +class WardOverview implements CopyWithInterface { int bedCount; int tasksInTodo; int tasksInInProgress; int tasksInDone; WardOverview({ - super.id, - required super.name, required this.bedCount, required this.tasksInTodo, required this.tasksInInProgress, required this.tasksInDone, }); + + @override + WardOverview copyWith(WardOverviewUpdate? update) { + return WardOverview( + bedCount: bedCount, + tasksInTodo: tasksInTodo, + tasksInInProgress: tasksInInProgress, + tasksInDone: tasksInDone, + ); + } +} + +class WardUpdate { + String? id; + String? name; + LoadableCRUDObjectUpdate? organization; + CrudExtensionUpdate? overview; + + WardUpdate({this.id, this.name, this.organization, this.overview}); +} + +class Ward extends CRUDObject { + final String name; + late final LoadableCRUDObject organization; + late final CRUDExtension overview; + + Ward({ + super.id, + required this.name, + LoadableCRUDObject? organization, + CRUDExtension? overview, + }) { + this.organization = organization ?? LoadableCRUDObject.notPresentObject(); + this.overview = overview ?? CRUDExtension(isPresent: false); + } + + @override + Ward copyWith(WardUpdate? update) { + return Ward( + id: update?.id ?? id, + name: update?.name ?? name, + organization: organization.copyWith(update?.organization), + overview: overview.copyWith(update?.overview), + ); + } + + @override + Ward attachId(String id) { + return copyWith(WardUpdate(id: id)); + } } 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 f63c0c9f..c907a591 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 @@ -3,6 +3,7 @@ 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'; +import 'package:helpwave_service/src/util/index.dart'; class WardUpdate { String id; @@ -27,7 +28,7 @@ class WardOfflineService { if (organizationId == null) { return valueStore.wards; } - return valueStore.wards.where((value) => value.organizationId == organizationId).toList(); + return valueStore.wards.where((value) => value.organization.id == organizationId).toList(); } void create(Ward ward) { @@ -41,7 +42,11 @@ class WardOfflineService { 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 Ward( + id: ward.id, + name: ward.name ?? value.name, + organization: LoadableCRUDObject(id: value.organization.id), + ); } return value; }).toList(); @@ -75,10 +80,7 @@ class WardOfflineClient extends WardServiceClient { throw "Ward with ward id ${request.id} not found"; } - final response = GetWardResponse() - ..id = ward.id - ..name = ward.name; - + final response = GetWardResponse(id: ward.id, name: ward.name); return MockResponseFuture.value(response); } @@ -126,13 +128,9 @@ class WardOfflineClient extends WardServiceClient { @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 wardsList = wards.map((ward) => GetWardsResponse_Ward(id: ward.id, name: ward.name)).toList(); - final response = GetWardsResponse()..wards.addAll(wardsList); + final response = GetWardsResponse(wards: wardsList); return MockResponseFuture.value(response); } @@ -206,12 +204,12 @@ class WardOfflineClient extends WardServiceClient { final newWard = Ward( id: DateTime.now().millisecondsSinceEpoch.toString(), name: request.name, - organizationId: 'organization', // TODO: Check organization + organization: LoadableCRUDObject(id: 'organization'), // TODO: Check organization ); OfflineClientStore().wardStore.create(newWard); - final response = CreateWardResponse()..id = newWard.id; + final response = CreateWardResponse(id: newWard.id); return MockResponseFuture.value(response); } 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 a7097f6b..18b4f08d 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 @@ -2,6 +2,7 @@ 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_service_clients.dart'; +import 'package:helpwave_service/src/util/crud_extension.dart'; /// The Service for [Ward]s /// @@ -11,22 +12,22 @@ class WardService { /// The GRPC ServiceClient which handles GRPC WardServiceClient wardService = TasksAPIServiceClients().wardServiceClient; - /// Loads a [WardMinimal] by its identifier - Future get({required String id}) async { + /// Loads a [Ward] by its identifier + Future get({required String id}) async { GetWardRequest request = GetWardRequest(id: id); GetWardResponse response = await wardService.getWard( request, options: CallOptions(metadata: TasksAPIServiceClients().getMetaData()), ); - return WardMinimal( + return Ward( id: response.id, name: response.name, ); } /// Loads the wards in the current organization - Future> getWardOverviews({String? organizationId}) async { + Future> getWardOverviews({String? organizationId}) async { GetWardOverviewsRequest request = GetWardOverviewsRequest(); GetWardOverviewsResponse response = await wardService.getWardOverviews( request, @@ -34,18 +35,20 @@ class WardService { ); return response.wards - .map((ward) => WardOverview( - id: ward.id, - name: ward.name, + .map((ward) => Ward( + id: ward.id, + name: ward.name, + overview: CRUDExtension( + data: WardOverview( bedCount: ward.bedCount, tasksInTodo: ward.tasksTodo, tasksInInProgress: ward.tasksInProgress, tasksInDone: ward.tasksDone, - )) + )))) .toList(); } - Future> getWards({String? organizationId}) async { + Future> getWards({String? organizationId}) async { GetWardsRequest request = GetWardsRequest(); GetWardsResponse response = await wardService.getWards( request, @@ -53,21 +56,21 @@ class WardService { ); return response.wards - .map((ward) => WardMinimal( + .map((ward) => Ward( id: ward.id, name: ward.name, )) .toList(); } - Future create({required WardMinimal ward}) async { + Future create({required Ward 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); + return ward.attachId(response.id); } Future update({required String id, String? name}) async { 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 f8a906ef..1fd26c17 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,12 +1,12 @@ import 'package:helpwave_service/auth.dart'; +import 'package:helpwave_service/src/api/tasks/data_types/index.dart'; import 'package:helpwave_service/src/util/index.dart'; import '../../../../user.dart'; /// The Controller for managing a [Organization] -class OrganizationController extends LoadController { - OrganizationController({super.id, super.initialData}) - : super(service: OrganizationService()); +class OrganizationController + extends LoadController { + OrganizationController({super.id, super.initialData}) : super(service: OrganizationService()); @override Future update(OrganizationUpdate? update) async { @@ -18,21 +18,17 @@ class OrganizationController extends LoadController { +class OrganizationDetailsUpdate { + String? avatarURL; + String? email; + bool? isPersonal; + bool? isVerified; + + OrganizationDetailsUpdate({ + this.email, + this.avatarURL, + this.isPersonal, + this.isVerified, + }); +} + +class OrganizationDetails implements CopyWithInterface { String avatarURL; String email; bool isPersonal; @@ -14,7 +28,7 @@ class OrganizationDetails implements CopyWithInterface OrganizationDetails( + factory OrganizationDetails.defaultData() => OrganizationDetails( email: "test@helpwave.de", avatarURL: avatarFallBackURL, isPersonal: false, @@ -22,7 +36,7 @@ class OrganizationDetails implements CopyWithInterface? details; OrganizationUpdate({ this.id, this.shortName, this.longName, - this.isPersonal, - this.isVerified, - this.avatarURL, - this.email, + this.details, }); } /// data class for [Organization] -class Organization extends IdentifiedObject implements CRUDObject { - String shortName; - String longName; - OrganizationDetails? details; +class Organization extends CRUDObject { + final String shortName; + final String longName; + late final CRUDExtension details; - bool get hasDetails => details != null; + bool get hasDetails => details.hasDataValue; - Organization({super.id, required this.shortName, required this.longName, this.details}); + Organization({ + super.id, + required this.shortName, + required this.longName, + CRUDExtension? details, + }) { + this.details = details ?? CRUDExtension(); + } - factory Organization.empty({String? id, bool hasEmptyDetails = true}) => Organization( + factory Organization.empty({String? id, bool hasDefaultDetails = true}) => Organization( id: id, shortName: "ORG", longName: "Organization Name", - details: hasEmptyDetails ? OrganizationDetails.empty() : null, + details: CRUDExtension( + data: hasDefaultDetails ? OrganizationDetails.defaultData() : null, + isPresent: hasDefaultDetails, + ), ); @override @@ -78,7 +96,7 @@ class Organization extends IdentifiedObject implements CRUDObject "$longName ($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 751a83d8..09bb5ecf 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 @@ -3,6 +3,7 @@ import 'package:helpwave_proto_dart/services/user_svc/v1/organization_svc.pbgrpc 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/src/util/index.dart'; import 'package:helpwave_service/user.dart'; import '../data_types/invitation.dart' as invite_types; @@ -30,13 +31,7 @@ class OrganizationOfflineService { organizations = organizations.map((org) { if (org.id == organizationUpdate.id) { found = true; - return org.copyWith(OrganizationUpdate( - shortName: organizationUpdate.shortName, - longName: organizationUpdate.longName, - avatarURL: organizationUpdate.avatarURL, - email: organizationUpdate.email, - isPersonal: organizationUpdate.isPersonal, - )); + return org.copyWith(organizationUpdate); } return org; }).toList(); @@ -65,12 +60,12 @@ class OrganizationOfflineClient extends OrganizationServiceClient { id: DateTime.now().millisecondsSinceEpoch.toString(), shortName: request.shortName, longName: request.longName, - details: OrganizationDetails( + details: CRUDExtension(data: OrganizationDetails( avatarURL: 'https://helpwave.de/favicon.ico', email: request.contactEmail, isPersonal: request.isPersonal, isVerified: true, - ), + )), ); OfflineClientStore().organizationStore.create(newOrganization); @@ -101,9 +96,9 @@ class OrganizationOfflineClient extends OrganizationServiceClient { id: organization.id, shortName: organization.shortName, longName: organization.longName, - avatarUrl: organization.details?.avatarURL, - contactEmail: organization.details?.email, - isPersonal: organization.details?.isPersonal, + avatarUrl: organization.details.data?.avatarURL, + contactEmail: organization.details.data?.email, + isPersonal: organization.details.data?.isPersonal, members: members, )); } @@ -129,10 +124,10 @@ class OrganizationOfflineClient extends OrganizationServiceClient { id: org.id, shortName: org.shortName, longName: org.longName, - contactEmail: org.details?.email, - avatarUrl: org.details?.avatarURL, + contactEmail: org.details.data?.email, + avatarUrl: org.details.data?.avatarURL, members: members, - isPersonal: org.details?.isPersonal, + isPersonal: org.details.data?.isPersonal, ); }).toList(); @@ -157,10 +152,10 @@ class OrganizationOfflineClient extends OrganizationServiceClient { id: org.id, shortName: org.shortName, longName: org.longName, - contactEmail: org.details?.email, - avatarUrl: org.details?.avatarURL, + contactEmail: org.details.data?.email, + avatarUrl: org.details.data?.avatarURL, members: members, - isPersonal: org.details?.isPersonal, + isPersonal: org.details.data?.isPersonal, ); }).toList(); @@ -176,9 +171,11 @@ class OrganizationOfflineClient extends OrganizationServiceClient { id: request.id, 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, + details: CrudExtensionUpdate(update: OrganizationDetailsUpdate( + email: request.hasContactEmail() ? request.contactEmail : null, + avatarURL: request.hasAvatarUrl() ? request.avatarUrl : null, + isPersonal: request.hasIsPersonal() ? request.isPersonal : null, + )), ); try { @@ -292,7 +289,7 @@ class OrganizationOfflineClient extends OrganizationServiceClient { GetInvitationsByUserResponse_Invitation_Organization( id: organization.id, longName: organization.longName, - avatarUrl: organization.details?.avatarURL, + avatarUrl: organization.details.data?.avatarURL, ); return invite; })); 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 6893b1fe..01d69536 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 @@ -5,6 +5,7 @@ import 'package:helpwave_service/src/api/user/data_types/invitation.dart' as inv 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 'package:helpwave_service/src/util/index.dart'; import '../data_types/index.dart'; /// The GRPC Service for [Organization]s @@ -28,11 +29,13 @@ class OrganizationService implements CRUDInterface update(String id, OrganizationUpdate? update) async { + OrganizationDetailsUpdate? details = update?.details?.update; UpdateOrganizationRequest request = UpdateOrganizationRequest( id: id, longName: update?.longName, shortName: update?.shortName, - isPersonal: update?.isPersonal, - contactEmail: update?.email, - avatarUrl: update?.avatarURL, + isPersonal: details?.isPersonal, + contactEmail: details?.email, + avatarUrl: details?.avatarURL, ); await organizationService.updateOrganization( request, @@ -222,7 +226,7 @@ class OrganizationService implements CRUDInterface // Maybe throw a error instead for the last case - CurrentWardService().currentWard?.organizationId ?? UserSessionService().identity?.firstOrganization; + CurrentWardService().currentWard?.organization.id ?? UserSessionService().identity?.firstOrganization; } 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 245b4ab5..8d7fd974 100644 --- a/packages/helpwave_service/lib/src/auth/current_ward_svc.dart +++ b/packages/helpwave_service/lib/src/auth/current_ward_svc.dart @@ -1,47 +1,10 @@ import 'package:flutter/foundation.dart'; import 'package:helpwave_service/src/api/offline/offline_client_store.dart'; +import 'package:helpwave_service/src/util/index.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'; -/// A readonly class for getting the CurrentWard information -class CurrentWardInformation { - /// The identifier of the ward - final WardMinimal ward; - - /// The identifier of the organization - final Organization organization; - - String get wardId => ward.id; // Id can be null - - String get wardName => ward.name; - - String get organizationId => organization.id ?? ""; // Id can be null - - String get organizationName => "${organization.longName} (${organization.shortName})"; - - bool get isEmpty => wardId == "" || organizationId == ""; // TODO the ids are null and would throw an error - - bool get hasFullInformation => ward.name != "" && organization.longName != ""; - - CurrentWardInformation(this.ward, this.organization); - - CurrentWardInformation copyWith({ - WardMinimal? ward, - Organization? organization, - }) { - return CurrentWardInformation( - ward ?? this.ward, - organization ?? this.organization, - ); - } - - @override - String toString() { - return "CurrentWardInformation: {ward: ${ward.toString()}, organization: ${organization.toString()}"; - } -} - // TODO consider other storage alternatives /// Service for reading and writing the [CurrentWard] Preference class _CurrentWardPreferences { @@ -52,85 +15,88 @@ class _CurrentWardPreferences { final String sharedPreferencesCurrentOrganizationKey = "current_ward_organization"; /// Clears the shared preferences - clear() async { + Future clear() async { SharedPreferences sharedPreferences = await SharedPreferences.getInstance(); sharedPreferences.remove(sharedPreferencesCurrentWardKey); sharedPreferences.remove(sharedPreferencesCurrentOrganizationKey); } /// Puts the new current ward to the shared preferences - setInformation({required CurrentWardInformation currentWard}) async { + Future setInformation({required Ward ward}) async { + assert( + !ward.isIdentified && !ward.organization.isIdentified, + "Only wards and organizations that have an id set " + "can be stored for later loading"); SharedPreferences sharedPreferences = await SharedPreferences.getInstance(); - sharedPreferences.setString(sharedPreferencesCurrentWardKey, currentWard.wardId); - sharedPreferences.setString(sharedPreferencesCurrentOrganizationKey, currentWard.organizationId); + sharedPreferences.setString(sharedPreferencesCurrentWardKey, ward.id!); + sharedPreferences.setString(sharedPreferencesCurrentOrganizationKey, ward.organization.id!); } /// Reads the current ward from the shared preferences - Future getInformation() async { + Future getInformation() async { SharedPreferences sharedPreferences = await SharedPreferences.getInstance(); String? wardId = sharedPreferences.getString(sharedPreferencesCurrentWardKey); String? organizationId = sharedPreferences.getString(sharedPreferencesCurrentOrganizationKey); - if (wardId != null && organizationId != null) { - return CurrentWardInformation( - WardMinimal(id: wardId, name: ""), - Organization.empty(id: organizationId), - ); + Ward ward = Ward(id: wardId, name: "", organization: LoadableCRUDObject(id: organizationId)); + if (!ward.isIdentified || !ward.organization.isIdentified) { + return null; } - return null; + return ward; } } -/// Service for the [CurrentWardInformation] +/// Service for the storing the currently active [Ward] /// -/// Changes the [CurrentWardInformation] globally +/// Changes the currently active [Ward] globally class CurrentWardService extends Listenable { bool _devMode = false; // TODO remove bool get devMode => _devMode; - set devMode(bool value) { - _devMode = value; + Future changeDevMode(bool isEnabled) async { + _devMode = isEnabled; if (UserAPIServiceClients().offlineMode) { Ward firstWard = OfflineClientStore().wardStore.wards[0]; Organization firstOrganization = OfflineClientStore().organizationStore.organizations[0]; - currentWard = CurrentWardInformation(firstWard, firstOrganization); + await changeCurrentWard(firstWard.copyWith(WardUpdate( + organization: LoadableCRUDObjectUpdate(overwrite: firstOrganization), + ))); } + notifyListeners(); } /// A storage for the current ward final _CurrentWardPreferences _preferences = _CurrentWardPreferences(); /// The current ward information - CurrentWardInformation? _currentWard; + LoadableCRUDObject _currentWard = LoadableCRUDObject.notPresentObject(); /// Whether this Controller has been initialized - bool get isInitialized => currentWard != null && !currentWard!.isEmpty; + bool get isInitialized => + _currentWard.isPresent && _currentWard.isIdentified && (_currentWard.data?.organization.isIdentified ?? false); /// Whether all information are loaded - bool get isLoaded => currentWard != null && currentWard!.hasFullInformation; + bool get isLoaded => + isInitialized && _currentWard.hasDataValue && (_currentWard.data?.organization.hasDataValue ?? false); /// Listeners final List _listeners = []; CurrentWardService._initialize() { - load(); + loadPreference(); } static final CurrentWardService _currentWardService = CurrentWardService._initialize(); factory CurrentWardService() => _currentWardService; - set currentWard(CurrentWardInformation? currentWard) { + Future changeCurrentWard(Ward ward) async { if (!devMode) { - if (currentWard == null) { - _preferences.clear(); - } else { - _preferences.setInformation(currentWard: currentWard); - } + await _preferences.setInformation(ward: ward); } - _currentWard = currentWard; + _currentWard = LoadableCRUDObject(data: ward); if (!isLoaded) { - fetch(); + await fetch(); } if (kDebugMode) { // TODO use logger @@ -139,22 +105,24 @@ class CurrentWardService extends Listenable { notifyListeners(); } - CurrentWardInformation? get currentWard => _currentWard; + Ward? get currentWard => _currentWard.data; /// Load the preferences with the [_CurrentWardPreferences] - Future load() async { - await _preferences.getInformation().then((value) { - if (!devMode) { - currentWard = value; - } - }); + Future loadPreference() async { + Ward? ward = await _preferences.getInformation(); + if (ward == null) { + return; + } + if (!devMode) { + await changeCurrentWard(ward); + } } Future fakeLogin() async { try { Organization organization = (await OrganizationService().getOrganizationsForUser())[0]; - WardMinimal ward = (await WardService().getWards(organizationId: organization.id))[0]; - currentWard = CurrentWardInformation(ward, organization); + Ward ward = (await WardService().getWards(organizationId: organization.id))[0]; + await changeCurrentWard(ward.copyWith(WardUpdate(organization: LoadableCRUDObjectUpdate(overwrite: organization)))); } catch (e) { if (kDebugMode) { print(e); @@ -167,15 +135,18 @@ class CurrentWardService extends Listenable { if (!isInitialized) { return; } - Organization organization = await OrganizationService().get(currentWard!.organizationId); - WardMinimal ward = await WardService().get(id: currentWard!.wardId); - _currentWard = CurrentWardInformation(ward, organization); + Organization organization = await OrganizationService().get(currentWard!.organization.id!); + Ward ward = await WardService().get(id: currentWard!.id!); + changeCurrentWard(ward.copyWith( + WardUpdate(organization: LoadableCRUDObjectUpdate(overwrite: organization)), + )); notifyListeners(); } /// Clears the [CurrentWardInformation] void clear() { - currentWard = null; + _currentWard = LoadableCRUDObject.notPresentObject(); + notifyListeners(); } @override @@ -205,22 +176,25 @@ class CurrentWardController extends ChangeNotifier { bool get isInitialized => service.isInitialized; CurrentWardController({bool devMode = false}) { - service.devMode = devMode; service.addListener(notifyListeners); - if (!service.isInitialized) { - load(); - } + service.changeDevMode(devMode).then( + (value) { + if (!service.isInitialized) { + load(); + } + }, + ); } - set currentWard(CurrentWardInformation? currentWard) { - service.currentWard = currentWard; + Future changeCurrentWard(Ward ward) async { + await service.changeCurrentWard(ward); } - CurrentWardInformation? get currentWard => service.currentWard; + Ward? get currentWard => service.currentWard; /// Load the information Future load() async { - service.load(); + await service.loadPreference(); } @override @@ -230,7 +204,7 @@ class CurrentWardController extends ChangeNotifier { } /// Clears the [CurrentWardInformation] - void clear() { + void clear() async { service.clear(); } } diff --git a/packages/helpwave_service/lib/src/auth/identity.dart b/packages/helpwave_service/lib/src/auth/identity.dart index c10376eb..1b1bbf58 100644 --- a/packages/helpwave_service/lib/src/auth/identity.dart +++ b/packages/helpwave_service/lib/src/auth/identity.dart @@ -31,10 +31,11 @@ class Identity { /// The default login data factory Identity.defaultIdentity() { return Identity( - idToken: "eyJzdWIiOiIxODE1OTcxMy01ZDRlLTRhZDUtOTRhZC1mYmI2YmIxNDc5ODQiLCJlbWFpbCI6Im1heC5tdXN0ZXJtYW5uQGhlbHB3Y" - "XZlLmRlIiwiZW1haWxfdmVyaWZpZWQiOnRydWUsIm5hbWUiOiJNYXggTXVzdGVybWFubiIsInByZWZlcnJlZF91c2VybmFtZSI6Im1he" - "C5tdXN0ZXJtYW5uIiwiZ2l2ZW5fbmFtZSI6Ik1heCIsImZhbWlseV9uYW1lIjoiTXVzdGVybWFubiIsIm9yZ2FuaXphdGlvbiI6ey" - "JpZCI6IjNiMjVjNmY1LTQ3MDUtNDA3NC05ZmM2LWE1MGMyOGViYTQwNiIsIm5hbWUiOiJoZWxwd2F2ZSB0ZXN0In19", + idToken: "eyJzdWIiOiIxODE1OTcxMy01ZDRlLTRhZDUtOTRhZC1mYmI2YmIxNDc5ODQiLCJlbWFpbCI6Im1heC5tdXN0ZXJ" + "tYW5uQGhlbHB3YXZlLmRlIiwiZW1haWxfdmVyaWZpZWQiOnRydWUsIm5hbWUiOiJNYXggTXVzdGVybWFubiIsIn" + "ByZWZlcnJlZF91c2VybmFtZSI6Im1heC5tdXN0ZXJtYW5uIiwiZ2l2ZW5fbmFtZSI6Ik1heCIsImZhbWlseV9uYW1lIjoiTXVzdGVybWF" + "ubiIsIm9yZ2FuaXphdGlvbiI6eyJpZCI6IjNiMjVjNmY1LTQ3MDUtNDA3NC05ZmM2LWE1MGMyOGViYTQwNiIsIm5" + "hbWUiOiJoZWxwd2F2ZSB0ZXN0In19", // TODO add a default here id: "18159713-5d4e-4ad5-94ad-fbb6bb147984", name: "Max Mustermann", 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 1fa59daf..1886384e 100644 --- a/packages/helpwave_service/lib/src/auth/user_session_service.dart +++ b/packages/helpwave_service/lib/src/auth/user_session_service.dart @@ -28,9 +28,9 @@ class UserSessionService { bool get devMode => _devMode; /// **Only use this** once before using the service - changeMode(bool isDevMode) { + Future changeMode(bool isDevMode) async { _devMode = isDevMode; - CurrentWardService().devMode = isDevMode; + CurrentWardService().changeDevMode(isDevMode); } /// Logs a User in by using the stored tokens diff --git a/packages/helpwave_service/lib/src/util/crud_extension.dart b/packages/helpwave_service/lib/src/util/crud_extension.dart index 25a32f46..4e14dd9a 100644 --- a/packages/helpwave_service/lib/src/util/crud_extension.dart +++ b/packages/helpwave_service/lib/src/util/crud_extension.dart @@ -1,69 +1,94 @@ -import 'package:helpwave_service/src/util/crud_object_interface.dart'; import 'package:helpwave_service/src/util/copy_with_interface.dart'; -class CrudExtensionUpdate< -IdentifierType, -Datatype extends CRUDObject, -UpdateType> { +class CrudExtensionUpdate, UpdateType> { + /// Overwrite the data attribute with the value and sets the id to the id of this object + /// + /// To remove the data use [removeLoadedData] or [remove] instead Datatype? overwrite; + + /// Applies the update on the CRUD object + /// + /// When provided it is necessary to have the data object set UpdateType? update; - IdentifierType? attachId; + + /// Removes the loaded data, but maintains the id if present + bool removeLoadedData; + + /// Removes the loaded data and sets the is present to false bool remove; + /// Actively set the value to null meaning the data is marked as present but has a value of null + bool setToNull; + CrudExtensionUpdate({ this.overwrite, this.update, - this.attachId, + this.setToNull = false, this.remove = false, + this.removeLoadedData = false, }) { int countOfExclusiveOps = 0; if (remove) countOfExclusiveOps++; - if (attachId != null) countOfExclusiveOps++; - if (update != null) countOfExclusiveOps++; - if (overwrite != null) countOfExclusiveOps++; + if (removeLoadedData) countOfExclusiveOps++; + if (setToNull) countOfExclusiveOps++; + if (this.update != null) countOfExclusiveOps++; + if (this.overwrite != null) countOfExclusiveOps++; assert( countOfExclusiveOps <= 1, """Only use 1 exclusive operation at a time. 1. update 2. remove - 3. overwrite - 4. attachId"""); + 3. removeData + 4. setToNull + 5. overwrite"""); } } -/// A easy way of attaching arbitrary CRUD objects to your CRUD objects -class CRUDExtensionObject< -IdentifierType, -Datatype extends CRUDObject, -UpdateType> implements CopyWithInterface< - CRUDExtensionObject, - CrudExtensionUpdate> { - final Datatype? data; +/// A easy way of attaching arbitrary Objects to your CRUD objects +class CRUDExtension, Update> + extends CopyWithInterface, CrudExtensionUpdate> { + final Data? data; + + // TODO make this variable easier to understand or find a different method to allow explicit null values + /// Whether or not the data is absent + final bool isPresent; - bool get isPresent => data != null; + bool get isNull { + assert(isPresent, "isNull can only be checked when isPresent = true."); + return !isNotNull; + } - CRUDExtensionObject({this.data}) : assert(data?.id == null); + bool get isNotNull { + assert(isPresent, "isNotNull can only be checked when isPresent = true."); + return hasDataValue; + } + + bool get hasDataValue => data != null; + + + CRUDExtension({this.data, this.isPresent = true}) { + assert(isPresent || (data == null), + "When isPresent = false, data cannot be present."); + } @override - CRUDExtensionObject copyWith( - CrudExtensionUpdate? update, - ) { + CRUDExtension copyWith(CrudExtensionUpdate? update) { if (update == null) { - return CRUDExtensionObject(data: data?.copyWith(null)); + return CRUDExtension(isPresent: isPresent); } if (update.remove) { - return CRUDExtensionObject(); - } else if (update.attachId != null) { - assert(isPresent, - "To attach an id you need to have data. Try overwriting instead."); - return CRUDExtensionObject(data: data?.attachId(update.attachId!)); + return CRUDExtension(isPresent: false); + } else if (update.removeLoadedData) { + return CRUDExtension(data: null); + } else if (update.setToNull) { + return CRUDExtension(); } else if (update.update != null) { - assert(data != null, - "To update data you need to have data. Try overwriting instead."); - return CRUDExtensionObject(data: data!.copyWith(update.update)); + assert(data != null, "To update data you need to have data. Try overwriting instead."); + Data updatedData = data!.copyWith(update.update); + return CRUDExtension(data: updatedData); } else if (update.overwrite != null) { - return CRUDExtensionObject(data: update.overwrite); + return CRUDExtension(data: update.overwrite); } - return CRUDExtensionObject(); + return CRUDExtension(data: data?.copyWith(null)); } } diff --git a/packages/helpwave_service/lib/src/util/identified_object.dart b/packages/helpwave_service/lib/src/util/identified_object.dart index fe8dbefb..649f3785 100644 --- a/packages/helpwave_service/lib/src/util/identified_object.dart +++ b/packages/helpwave_service/lib/src/util/identified_object.dart @@ -4,6 +4,8 @@ class IdentifiedObject { const IdentifiedObject({this.id}); + bool get isIdentified => id != null; + bool isReferencingSame(IdentifiedObject other) { return runtimeType == other.runtimeType && this.id == other.id; } diff --git a/packages/helpwave_service/lib/src/util/loadable_crud_object.dart b/packages/helpwave_service/lib/src/util/loadable_crud_object.dart index 04ac1466..f2bdeac7 100644 --- a/packages/helpwave_service/lib/src/util/loadable_crud_object.dart +++ b/packages/helpwave_service/lib/src/util/loadable_crud_object.dart @@ -52,15 +52,11 @@ class LoadableCRUDObjectUpdate< } } -class LoadableCRUDObject< - IdentifierType, - Datatype extends CRUDObject, - UpdateType> - extends CRUDObject< - IdentifierType, - LoadableCRUDObject, - LoadableCRUDObjectUpdate> { - final Datatype? data; +class LoadableCRUDObject, Update> + extends CRUDObject, + LoadableCRUDObjectUpdate> { + final Data? data; // TODO make this variable easier to understand or find a different method to allow explicit null values /// Whether or not the data is absent @@ -78,7 +74,7 @@ class LoadableCRUDObject< bool get hasDataValue => data != null; - LoadableCRUDObject({IdentifierType? id, this.data, this.isPresent = true}) + LoadableCRUDObject({ID? id, this.data, this.isPresent = true}) : super(id: id ?? data?.id) { assert(data?.id == null || data?.id == id, "The data.id and id must match or data cannot be provided"); @@ -87,8 +83,8 @@ class LoadableCRUDObject< } @override - LoadableCRUDObject copyWith( - LoadableCRUDObjectUpdate? update, + LoadableCRUDObject copyWith( + LoadableCRUDObjectUpdate? update, ) { if (update == null) { return LoadableCRUDObject( @@ -103,12 +99,12 @@ class LoadableCRUDObject< } else if (update.id != null) { return LoadableCRUDObject( id: update.id, - data: data?.attachId(update.id as IdentifierType), + data: data?.attachId(update.id as ID), ); } else if (update.update != null) { assert(data != null, "To update data you need to have data. Try overwriting instead."); - Datatype updatedData = data!.copyWith(update.update); + Data updatedData = data!.copyWith(update.update); return LoadableCRUDObject(id: updatedData.id, data: updatedData); } else if (update.overwrite != null) { return LoadableCRUDObject( @@ -124,8 +120,8 @@ class LoadableCRUDObject< factory LoadableCRUDObject.notPresentObject() => LoadableCRUDObject(); @override - LoadableCRUDObject attachId( - IdentifierType id) { + LoadableCRUDObject attachId( + ID id) { return copyWith(LoadableCRUDObjectUpdate(id: id)); } }