diff --git a/apps/tasks/lib/components/assignee_select.dart b/apps/tasks/lib/components/assignee_select.dart index d04bd06f..02e4794f 100644 --- a/apps/tasks/lib/components/assignee_select.dart +++ b/apps/tasks/lib/components/assignee_select.dart @@ -1,48 +1,50 @@ +import 'dart:async'; import 'package:flutter/material.dart'; import 'package:helpwave_localization/localization.dart'; +import 'package:helpwave_service/user.dart'; import 'package:helpwave_theme/constants.dart'; import 'package:helpwave_widget/bottom_sheets.dart'; -import 'package:helpwave_widget/loading.dart'; -import 'package:provider/provider.dart'; -import 'package:tasks/controllers/assignee_select_controller.dart'; -import 'package:tasks/dataclasses/user.dart'; +import 'package:helpwave_widget/content_selection.dart'; /// A [BottomSheet] for selecting a assignee -class AssigneeSelect extends StatelessWidget { +class AssigneeSelectBottomSheet extends StatelessWidget { /// The callback when the assignee should be changed - final Function(User assignee) onChanged; + /// + /// Null if the assignee should be removed + final Function(User? assignee) onChanged; - const AssigneeSelect({super.key, required this.onChanged}); + final FutureOr> users; + + final String? selectedId; + + const AssigneeSelectBottomSheet({super.key, required this.onChanged, required this.users, this.selectedId}); @override Widget build(BuildContext context) { return BottomSheetBase( titleText: context.localization!.assignee, onClosing: () => {}, - builder: (context) => Flexible( - child: Padding( - padding: const EdgeInsets.symmetric(vertical: paddingMedium), - child: Consumer(builder: (context, assigneeSelectController, __) { - return LoadingAndErrorWidget( - state: assigneeSelectController.state, - child: ListView.builder( - itemCount: assigneeSelectController.users.length, - itemBuilder: (context, index) { - User user = assigneeSelectController.users[index]; - return ListTile( - onTap: () { - assigneeSelectController.changeAssignee(user.id).then((value) { - onChanged(user); - }); - }, - leading: CircleAvatar( - foregroundColor: Colors.blue, backgroundImage: NetworkImage(user.profile.toString())), - title: Text(user.nickName), - ); - }, + builder: (context) => Padding( + padding: const EdgeInsets.symmetric(vertical: paddingMedium), + child: Column( + children: [ + TextButton( + child: Text(context.localization!.remove), + onPressed: () => onChanged(null), + ), + const SizedBox(height: 10), + ListSelect( + items: users, + onSelect: onChanged, + builder: (context, user, select) => ListTile( + onTap: select, + leading: CircleAvatar( + foregroundColor: Colors.blue, backgroundImage: NetworkImage(user.profileUrl.toString())), + title: Text(user.nickName, + style: TextStyle(decoration: user.id == selectedId ? TextDecoration.underline : null)), ), - ); - }), + ), + ], ), ), ); diff --git a/apps/tasks/lib/components/patient_bottom_sheet.dart b/apps/tasks/lib/components/patient_bottom_sheet.dart index a15015e0..431773a4 100644 --- a/apps/tasks/lib/components/patient_bottom_sheet.dart +++ b/apps/tasks/lib/components/patient_bottom_sheet.dart @@ -1,5 +1,6 @@ import 'package:flutter/material.dart'; import 'package:helpwave_localization/localization.dart'; +import 'package:helpwave_service/auth.dart'; import 'package:helpwave_theme/constants.dart'; import 'package:helpwave_theme/util.dart'; import 'package:helpwave_widget/bottom_sheets.dart'; @@ -10,13 +11,8 @@ import 'package:helpwave_widget/text_input.dart'; import 'package:provider/provider.dart'; import 'package:tasks/components/task_bottom_sheet.dart'; import 'package:tasks/components/task_expansion_tile.dart'; -import 'package:tasks/controllers/patient_controller.dart'; -import 'package:tasks/dataclasses/bed.dart'; -import 'package:tasks/dataclasses/patient.dart'; -import 'package:tasks/dataclasses/room.dart'; -import 'package:tasks/dataclasses/task.dart'; -import 'package:tasks/services/current_ward_svc.dart'; -import 'package:tasks/services/room_svc.dart'; +import 'package:helpwave_service/tasks.dart'; +import 'package:helpwave_util/loading.dart'; /// A [BottomSheet] for showing [Patient] information and [Task]s for that [Patient] class PatientBottomSheet extends StatefulWidget { @@ -83,194 +79,196 @@ class _PatientBottomSheetState extends State { return LoadingAndErrorWidget( state: patientController.state, child: Row( - mainAxisAlignment: patientController.isCreating ? MainAxisAlignment.end : MainAxisAlignment.spaceBetween, - children: patientController.isCreating - ? [ - TextButton( - style: buttonStyleBig, - onPressed: patientController.create, - child: Text(context.localization!.create), - ) - ] - : [ - SizedBox( - width: width * 0.4, - // TODO make this state checking easier and more readable - child: TextButton( - onPressed: patientController.patient.isUnassigned - ? null - : () { - patientController.unassign(); - }, - style: buttonStyleMedium.copyWith( - backgroundColor: resolveByStatesAndContextBackground( - context: context, - defaultValue: inProgressColor, - ), - foregroundColor: resolveByStatesAndContextForeground( - context: context, - ), - ), - child: Text(context.localization!.unassigne), - ), - ), - SizedBox( - width: width * 0.4, - child: TextButton( - // TODO check whether the patient is active - onPressed: patientController.patient.isDischarged ? null : () { - showDialog( - context: context, - builder: (context) => AcceptDialog(titleText: context.localization!.dischargePatient), - ).then((value) { - if (value) { - patientController.discharge(); - Navigator.of(context).pop(); - } - }); - }, - style: buttonStyleMedium.copyWith( - backgroundColor: resolveByStatesAndContextBackground( - context: context, - defaultValue: negativeColor, - ), - foregroundColor: resolveByStatesAndContextForeground( - context: context, - ), - ), - child: Text(context.localization!.discharge), - ), - ), - ], - )); - } ), - ), builder: (BuildContext context) => Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Center( - child: Consumer(builder: (context, patientController, _) { - return LoadingFutureBuilder( - future: loadRoomsWithBeds(patientController.patient.id), - // TODO use a better loading widget - loadingWidget: const SizedBox(), - thenWidgetBuilder: (context, beds) { - if (beds.isEmpty) { - return Text( - context.localization!.noFreeBeds, - style: TextStyle(color: Theme.of(context).disabledColor, fontWeight: FontWeight.bold), - ); - } - return DropdownButtonHideUnderline( - child: DropdownButton( - iconEnabledColor: Theme.of(context).colorScheme.secondary.withOpacity(0.6), - padding: EdgeInsets.zero, - isDense: true, - hint: Text( - context.localization!.assignBed, - style: TextStyle(color: Theme.of(context).colorScheme.secondary.withOpacity(0.6)), - ), - value: !patientController.patient.isUnassigned - ? RoomWithBedFlat( - room: patientController.patient.room!, bed: patientController.patient.bed!) - : null, - items: beds - .map((roomWithBed) => DropdownMenuItem( - value: roomWithBed, - child: Text( - "${roomWithBed.room.name} - ${roomWithBed.bed.name}", - style: TextStyle(color: Theme.of(context).colorScheme.primary.withOpacity(0.6)), + mainAxisAlignment: + patientController.isCreating ? MainAxisAlignment.end : MainAxisAlignment.spaceBetween, + children: patientController.isCreating + ? [ + TextButton( + style: buttonStyleBig, + onPressed: patientController.create, + child: Text(context.localization!.create), + ) + ] + : [ + SizedBox( + width: width * 0.4, + // TODO make this state checking easier and more readable + child: TextButton( + onPressed: patientController.patient.isNotAssignedToBed + ? null + : () { + patientController.unassign(); + }, + style: buttonStyleMedium.copyWith( + backgroundColor: resolveByStatesAndContextBackground( + context: context, + defaultValue: inProgressColor, + ), + foregroundColor: resolveByStatesAndContextForeground( + context: context, + ), + ), + child: Text(context.localization!.unassigne), + ), + ), + SizedBox( + width: width * 0.4, + child: TextButton( + // TODO check whether the patient is active + onPressed: patientController.patient.isDischarged + ? null + : () { + showDialog( + context: context, + builder: (context) => + AcceptDialog(titleText: context.localization!.dischargePatient), + ).then((value) { + if (value) { + patientController.discharge(); + Navigator.of(context).pop(); + } + }); + }, + style: buttonStyleMedium.copyWith( + backgroundColor: resolveByStatesAndContextBackground( + context: context, + defaultValue: negativeColor, + ), + foregroundColor: resolveByStatesAndContextForeground( + context: context, + ), + ), + child: Text(context.localization!.discharge), + ), + ), + ], + )); + }), + ), + builder: (BuildContext context) => Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Center( + child: Consumer(builder: (context, patientController, _) { + return LoadingFutureBuilder( + data: loadRoomsWithBeds(patientController.patient.id), + // TODO use a better loading widget + loadingWidget: const SizedBox(), + thenWidgetBuilder: (context, beds) { + if (beds.isEmpty) { + return Text( + context.localization!.noFreeBeds, + style: TextStyle(color: Theme.of(context).disabledColor, fontWeight: FontWeight.bold), + ); + } + return DropdownButtonHideUnderline( + child: DropdownButton( + iconEnabledColor: Theme.of(context).colorScheme.secondary.withOpacity(0.6), + padding: EdgeInsets.zero, + isDense: true, + hint: Text( + context.localization!.assignBed, + style: TextStyle(color: Theme.of(context).colorScheme.secondary.withOpacity(0.6)), ), - )) - .toList(), - onChanged: (RoomWithBedFlat? value) { - // TODO later unassign here - if (value == null) { - return; - } - patientController.changeBed(value.room, value.bed); - }, - ), - ); - }, - ); - }), - ), - Text( - context.localization!.notes, - style: const TextStyle(fontSize: fontSizeBig, fontWeight: FontWeight.bold), - ), - const SizedBox(height: distanceSmall), - Consumer( - builder: (context, patientController, _) => - patientController.state == LoadingState.loaded || patientController.isCreating - ? TextFormFieldWithTimer( - initialValue: patientController.patient.notes, - maxLines: 6, - onUpdate: patientController.changeNotes, - ) - : TextFormField(maxLines: 6), - ), - Padding( - padding: const EdgeInsets.symmetric(vertical: paddingMedium), - child: Consumer(builder: (context, patientController, _) { - Patient patient = patientController.patient; - return AddList( - maxHeight: width * 0.5, - items: [ - ...patient.unscheduledTasks, - ...patient.inProgressTasks, - ...patient.doneTasks, - ], - itemBuilder: (_, index, taskList) { - if (index == 0) { - return TaskExpansionTile( - tasks: patient.unscheduledTasks - .map((task) => TaskWithPatient.fromTaskAndPatient( - task: task, - patient: patient, - )) - .toList(), - title: context.localization!.upcoming, - color: upcomingColor, + value: beds.where((beds) => beds.bed.id == patientController.patient.bed?.id).firstOrNull, + items: beds + .map((roomWithBed) => DropdownMenuItem( + value: roomWithBed, + child: Text( + "${roomWithBed.room.name} - ${roomWithBed.bed.name}", + style: TextStyle(color: Theme.of(context).colorScheme.primary.withOpacity(0.6)), + ), + )) + .toList(), + onChanged: (RoomWithBedFlat? value) { + // TODO later unassign here + if (value == null) { + return; + } + patientController.assignToBed(value.room, value.bed); + }, + ), ); - } - if (index == 2) { + }, + ); + }), + ), + Text( + context.localization!.notes, + style: const TextStyle(fontSize: fontSizeBig, fontWeight: FontWeight.bold), + ), + const SizedBox(height: distanceSmall), + Consumer( + builder: (context, patientController, _) => + patientController.state == LoadingState.loaded || patientController.isCreating + ? TextFormFieldWithTimer( + initialValue: patientController.patient.notes, + maxLines: 6, + onUpdate: patientController.changeNotes, + ) + : TextFormField(maxLines: 6), + ), + Padding( + padding: const EdgeInsets.symmetric(vertical: paddingMedium), + child: Consumer(builder: (context, patientController, _) { + Patient patient = patientController.patient; + return AddList( + maxHeight: width * 0.5, + items: [ + ...patient.unscheduledTasks, + ...patient.inProgressTasks, + ...patient.doneTasks, + ], + itemBuilder: (_, index, taskList) { + if (index == 0) { + return TaskExpansionTile( + tasks: patient.unscheduledTasks + .map((task) => TaskWithPatient.fromTaskAndPatient( + task: task, + patient: patient, + )) + .toList(), + title: context.localization!.upcoming, + color: upcomingColor, + ); + } + if (index == 2) { + return TaskExpansionTile( + tasks: patient.doneTasks + .map((task) => TaskWithPatient.fromTaskAndPatient( + task: task, + patient: patient, + )) + .toList(), + title: context.localization!.inProgress, + color: inProgressColor, + ); + } return TaskExpansionTile( - tasks: patient.doneTasks + tasks: patient.inProgressTasks .map((task) => TaskWithPatient.fromTaskAndPatient( - task: task, - patient: patient, - )) + task: task, + patient: patient, + )) .toList(), - title: context.localization!.inProgress, - color: inProgressColor, + title: context.localization!.done, + color: doneColor, ); - } - return TaskExpansionTile( - tasks: patient.inProgressTasks - .map((task) => TaskWithPatient.fromTaskAndPatient( - task: task, - patient: patient, - )) - .toList(), - title: context.localization!.done, - color: doneColor, - ); - }, - title: Text( - context.localization!.tasks, - style: const TextStyle(fontSize: fontSizeBig, fontWeight: FontWeight.bold), - ), - // TODO use return value to add it to task list or force a refetch - onAdd: () => context.pushModal( - context: context, - builder: (context) => TaskBottomSheet(task: Task.empty, patient: patient), - ), - ); - }), - ), - ], - ), + }, + title: Text( + context.localization!.tasks, + style: const TextStyle(fontSize: fontSizeBig, fontWeight: FontWeight.bold), + ), + // TODO use return value to add it to task list or force a refetch + onAdd: () => context.pushModal( + context: context, + builder: (context) => TaskBottomSheet(task: Task.empty(patient.id), patient: patient), + ), + ); + }), + ), + ], + ), ), ); } diff --git a/apps/tasks/lib/components/patient_card.dart b/apps/tasks/lib/components/patient_card.dart index cffd161e..d26c63a3 100644 --- a/apps/tasks/lib/components/patient_card.dart +++ b/apps/tasks/lib/components/patient_card.dart @@ -1,8 +1,8 @@ import 'package:flutter/material.dart'; +import 'package:helpwave_service/tasks.dart'; import 'package:tasks/components/task_status_pill_box.dart'; import 'package:helpwave_localization/localization.dart'; import 'package:helpwave_theme/constants.dart'; -import 'package:tasks/dataclasses/patient.dart'; /// A [Widget] for displaying a card containing [Patient] information class PatientCard extends StatelessWidget { diff --git a/apps/tasks/lib/components/patient_status_chip_select.dart b/apps/tasks/lib/components/patient_status_chip_select.dart index c352d9f2..093c5c2f 100644 --- a/apps/tasks/lib/components/patient_status_chip_select.dart +++ b/apps/tasks/lib/components/patient_status_chip_select.dart @@ -1,7 +1,7 @@ import 'package:flutter/cupertino.dart'; import 'package:helpwave_localization/localization.dart'; +import 'package:helpwave_service/tasks.dart'; import 'package:helpwave_widget/content_selection.dart'; -import 'package:tasks/dataclasses/patient.dart'; enum PatientStatusChipSelectOptions { all, active, unassigned, discharged } diff --git a/apps/tasks/lib/components/subtask_list.dart b/apps/tasks/lib/components/subtask_list.dart index 5f78657b..f1db6772 100644 --- a/apps/tasks/lib/components/subtask_list.dart +++ b/apps/tasks/lib/components/subtask_list.dart @@ -1,25 +1,23 @@ import 'package:flutter/material.dart'; import 'package:helpwave_localization/localization.dart'; +import 'package:helpwave_service/tasks.dart'; import 'package:helpwave_theme/constants.dart'; import 'package:helpwave_theme/util.dart'; import 'package:helpwave_widget/lists.dart'; import 'package:provider/provider.dart'; -import 'package:tasks/controllers/subtask_list_controller.dart'; -import 'package:tasks/dataclasses/subtask.dart'; -import 'package:tasks/dataclasses/task.dart'; -/// A [Widget] for displaying an updating a [List] of [SubTask]s +/// A [Widget] for displaying an updating a [List] of [Subtask]s class SubtaskList extends StatelessWidget { - /// The identifier of the [Task] to which all of these [SubTask]s belong + /// The identifier of the [Task] to which all of these [Subtask]s belong final String taskId; /// The [List] of initial subtasks - final List subtasks; + final List subtasks; /// The callback when the [subtasks] are changed /// /// Should **only** be used when [taskId == ""] - final void Function(List subtasks) onChange; + final void Function(List subtasks) onChange; const SubtaskList({ super.key, @@ -43,7 +41,7 @@ class SubtaskList extends StatelessWidget { style: const TextStyle(fontWeight: FontWeight.bold, fontSize: 16), ), onAdd: () => subtasksController - .add(SubTask(id: "", name: "Subtask ${subtasksController.subtasks.length + 1}")) + .create(Subtask(id: "", name: "Subtask ${subtasksController.subtasks.length + 1}", taskId: taskId)) .then((_) => onChange(subtasksController.subtasks)), itemBuilder: (context, _, subtask) => ListTile( contentPadding: EdgeInsets.zero, @@ -54,7 +52,7 @@ class SubtaskList extends StatelessWidget { ).then((value) { if (value != null) { subtask.name = value; - subtasksController.updateSubtask(subTask: subtask); + subtasksController.update(subtask: subtask); } }), title: Row( @@ -71,8 +69,8 @@ class SubtaskList extends StatelessWidget { shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(iconSizeSmall), ), - onChanged: (value) => subtasksController - .changeStatus(subTask: subtask, value: value ?? false) + onChanged: (isDone) => subtasksController + .update(subtask: subtask.copyWith(isDone: isDone)) .then((value) => onChange(subtasksController.subtasks)), ), trailing: GestureDetector( diff --git a/apps/tasks/lib/components/task_bottom_sheet.dart b/apps/tasks/lib/components/task_bottom_sheet.dart index 84f33257..7f2590d4 100644 --- a/apps/tasks/lib/components/task_bottom_sheet.dart +++ b/apps/tasks/lib/components/task_bottom_sheet.dart @@ -1,6 +1,9 @@ import 'package:flutter/material.dart'; import 'package:helpwave_localization/localization.dart'; +import 'package:helpwave_service/auth.dart'; +import 'package:helpwave_service/user.dart'; import 'package:helpwave_theme/constants.dart'; +import 'package:helpwave_util/loading.dart'; import 'package:helpwave_widget/bottom_sheets.dart'; import 'package:helpwave_widget/loading.dart'; import 'package:helpwave_widget/text_input.dart'; @@ -8,13 +11,7 @@ import 'package:provider/provider.dart'; import 'package:tasks/components/assignee_select.dart'; import 'package:tasks/components/subtask_list.dart'; import 'package:tasks/components/visibility_select.dart'; -import 'package:tasks/controllers/task_controller.dart'; -import 'package:tasks/controllers/user_controller.dart'; -import 'package:tasks/dataclasses/patient.dart'; -import 'package:tasks/dataclasses/user.dart'; -import 'package:tasks/services/patient_svc.dart'; -import '../controllers/assignee_select_controller.dart'; -import '../dataclasses/task.dart'; +import 'package:helpwave_service/tasks.dart'; /// A private [Widget] similar to a [ListTile] that has an icon and then to text /// @@ -142,24 +139,24 @@ class _TaskBottomSheetState extends State { child: Consumer( builder: (context, taskController, child) => taskController.isCreating ? Padding( - padding: const EdgeInsets.only(top: paddingSmall), - child: Align( - alignment: Alignment.topRight, - child: TextButton( - style: buttonStyleBig, - onPressed: taskController.isReadyForCreate - ? () { - taskController.create().then((value) { - if (value) { - Navigator.pop(context); - } - }); - } - : null, - child: Text(context.localization!.create), - ), - ), - ) + padding: const EdgeInsets.only(top: paddingSmall), + child: Align( + alignment: Alignment.topRight, + child: TextButton( + style: buttonStyleBig, + onPressed: taskController.isReadyForCreate + ? () { + taskController.create().then((value) { + if (value) { + Navigator.pop(context); + } + }); + } + : null, + child: Text(context.localization!.create), + ), + ), + ) : const SizedBox(), ), ), @@ -171,34 +168,35 @@ class _TaskBottomSheetState extends State { children: [ Center( child: Consumer(builder: - // TODO move this to its own component + // TODO move this to its own component (context, taskController, __) { return LoadingAndErrorWidget.pulsing( state: taskController.state, child: !taskController.isCreating ? Text(taskController.patient.name) : LoadingFutureBuilder( - future: PatientService().getPatientList(), - loadingWidget: const PulsingContainer(), - thenWidgetBuilder: (context, patientList) { - List patients = patientList.active + patientList.unassigned; - return DropdownButton( - underline: const SizedBox(), - iconEnabledColor: Theme.of(context).colorScheme.secondary.withOpacity(0.6), - // removes the default underline - padding: EdgeInsets.zero, - hint: Text( - context.localization!.selectPatient, - style: TextStyle(color: Theme.of(context).colorScheme.secondary.withOpacity(0.6)), - ), - isDense: true, - items: patients - .map((patient) => DropdownMenuItem(value: patient, child: Text(patient.name))) - .toList(), - value: taskController.patient.isCreating ? null : taskController.patient, - onChanged: (patient) => taskController.changePatient(patient ?? PatientMinimal.empty()), - ); - }), + data: PatientService().getPatientList(), + loadingWidget: const PulsingContainer(), + thenWidgetBuilder: (context, patientList) { + List patients = patientList.active + patientList.unassigned; + return DropdownButton( + underline: const SizedBox(), + iconEnabledColor: Theme.of(context).colorScheme.secondary.withOpacity(0.6), + // removes the default underline + padding: EdgeInsets.zero, + hint: Text( + context.localization!.selectPatient, + style: TextStyle(color: Theme.of(context).colorScheme.secondary.withOpacity(0.6)), + ), + isDense: true, + items: patients + .map((patient) => DropdownMenuItem(value: patient, child: Text(patient.name))) + .toList(), + value: taskController.patient.isCreating ? null : taskController.patient, + onChanged: (patient) => + taskController.changePatient(patient ?? PatientMinimal.empty()), + ); + }), ); }), ), @@ -212,43 +210,30 @@ class _TaskBottomSheetState extends State { label: context.localization!.assignedTo, onTap: () => context.pushModal( context: context, - builder: (BuildContext context) => LoadingAndErrorWidget( - state: taskController.state, - child: ChangeNotifierProvider( - create: (BuildContext context) => AssigneeSelectController( - selected: taskController.task.assignee, - taskId: taskController.task.id, - ), - child: AssigneeSelect( - onChanged: (assignee) { - taskController.changeAssignee(assignee.id).then((value) => Navigator.of(context).pop()); - }, - ), - ), + builder: (BuildContext context) => AssigneeSelectBottomSheet( + users: OrganizationService() + .getMembersByOrganization(CurrentWardService().currentWard!.organizationId), + onChanged: (User? assignee) { + taskController.changeAssignee(assignee); + Navigator.pop(context); + }, + selectedId: taskController.task.assigneeId, ), ), - // TODO maybe do some optimisations here - // TODO update the error and loading widgets - valueWidget: LoadingAndErrorWidget.pulsing( - state: taskController.state, - child: taskController.task.hasAssignee - ? ChangeNotifierProvider( - create: (context) => UserController(User.empty(id: taskController.task.assignee!)), - child: Consumer( - builder: (context, userController, __) => LoadingAndErrorWidget.pulsing( - state: userController.state, + valueWidget: taskController.task.hasAssignee + ? LoadingAndErrorWidget.pulsing( + state: taskController.assignee != null ? LoadingState.loaded : LoadingState.loading, child: Text( - userController.user.name, + // Never the case that we display the empty String, but the text is computed + // before being displayed + taskController.assignee?.name ?? "", style: editableValueTextStyle(context), ), + ) + : Text( + context.localization!.unassigned, + style: editableValueTextStyle(context), ), - ), - ) - : Text( - context.localization!.unassigned, - style: editableValueTextStyle(context), - ), - ), ); }), Consumer( @@ -283,7 +268,8 @@ class _TaskBottomSheetState extends State { lastDate: DateTime.now().add(const Duration(days: 365 * 5)), builder: (context, child) { // Overwrite the Theme - ThemeData pickerTheme = Theme.of(context).copyWith(textButtonTheme: const TextButtonThemeData()); + ThemeData pickerTheme = + Theme.of(context).copyWith(textButtonTheme: const TextButtonThemeData()); return Theme(data: pickerTheme, child: child ?? const SizedBox()); }, ).then((date) async { @@ -394,8 +380,7 @@ class _TaskBottomSheetState extends State { ), ), ], - ) - ), + )), ), ), ); diff --git a/apps/tasks/lib/components/task_card.dart b/apps/tasks/lib/components/task_card.dart index 0ec86450..3eade9a4 100644 --- a/apps/tasks/lib/components/task_card.dart +++ b/apps/tasks/lib/components/task_card.dart @@ -1,8 +1,8 @@ import 'package:flutter/material.dart'; import 'package:helpwave_localization/localization.dart'; +import 'package:helpwave_service/tasks.dart'; import 'package:helpwave_theme/constants.dart'; import 'package:helpwave_widget/static_progress_indicator.dart'; -import 'package:tasks/dataclasses/task.dart'; /// A [Card] showing a [Task]'s information class TaskCard extends StatelessWidget { diff --git a/apps/tasks/lib/components/task_expansion_tile.dart b/apps/tasks/lib/components/task_expansion_tile.dart index ed768d44..de316ace 100644 --- a/apps/tasks/lib/components/task_expansion_tile.dart +++ b/apps/tasks/lib/components/task_expansion_tile.dart @@ -5,8 +5,7 @@ import 'package:helpwave_widget/shapes.dart'; import 'package:provider/provider.dart'; import 'package:tasks/components/task_bottom_sheet.dart'; import 'package:tasks/components/task_card.dart'; -import '../controllers/my_tasks_controller.dart'; -import '../dataclasses/task.dart'; +import 'package:helpwave_service/tasks.dart'; /// A [ExpansionTile] for showing a [List] of [Task]s /// @@ -58,7 +57,7 @@ class TaskExpansionTile extends StatelessWidget { context: context, builder: (context) => TaskBottomSheet(task: task, patient: task.patient), ).then((_) { - MyTasksController controller = Provider.of(context, listen: false); + AssignedTasksController controller = Provider.of(context, listen: false); controller.load(); }); }, diff --git a/apps/tasks/lib/components/user_bottom_sheet.dart b/apps/tasks/lib/components/user_bottom_sheet.dart index a574454d..4b18600a 100644 --- a/apps/tasks/lib/components/user_bottom_sheet.dart +++ b/apps/tasks/lib/components/user_bottom_sheet.dart @@ -5,12 +5,9 @@ import 'package:helpwave_theme/constants.dart'; import 'package:helpwave_widget/bottom_sheets.dart'; import 'package:helpwave_widget/loading.dart'; import 'package:provider/provider.dart'; -import 'package:tasks/controllers/user_session_controller.dart'; -import 'package:tasks/services/current_ward_svc.dart'; -import '../dataclasses/ward.dart'; -import '../screens/login_screen.dart'; -import '../services/user_session_service.dart'; -import '../services/ward_service.dart'; +import 'package:helpwave_service/tasks.dart'; +import 'package:helpwave_service/auth.dart'; +import 'package:tasks/screens/login_screen.dart'; /// A [BottomSheet] for showing the [User]s information class UserBottomSheet extends StatefulWidget { @@ -80,7 +77,7 @@ class _UserBottomSheetState extends State { child: Consumer(builder: (context, currentWardController, __) { return LoadingFutureBuilder( loadingWidget: const SizedBox(), - future: + data: WardService().getWardOverviews(organizationId: currentWardController.currentWard!.organizationId), thenWidgetBuilder: (BuildContext context, List data) { double menuWidth = min(250, width * 0.7); diff --git a/apps/tasks/lib/components/user_header.dart b/apps/tasks/lib/components/user_header.dart index 4506c38b..b66cac3a 100644 --- a/apps/tasks/lib/components/user_header.dart +++ b/apps/tasks/lib/components/user_header.dart @@ -1,13 +1,11 @@ import 'package:flutter/material.dart'; +import 'package:helpwave_service/auth.dart'; +import 'package:helpwave_service/tasks.dart'; import 'package:helpwave_theme/constants.dart'; import 'package:helpwave_widget/bottom_sheets.dart'; import 'package:provider/provider.dart'; import 'package:tasks/components/user_bottom_sheet.dart'; -import 'package:tasks/controllers/user_session_controller.dart'; -import 'package:tasks/dataclasses/organization.dart'; -import 'package:tasks/dataclasses/ward.dart'; import 'package:tasks/screens/settings_screen.dart'; -import 'package:tasks/services/current_ward_svc.dart'; /// A [AppBar] for displaying the current [User], [Organization] and [Ward] class UserHeader extends StatelessWidget implements PreferredSizeWidget { diff --git a/apps/tasks/lib/components/visibility_select.dart b/apps/tasks/lib/components/visibility_select.dart index 677e93ed..2c458b20 100644 --- a/apps/tasks/lib/components/visibility_select.dart +++ b/apps/tasks/lib/components/visibility_select.dart @@ -3,7 +3,6 @@ import 'package:helpwave_localization/localization.dart'; import 'package:helpwave_theme/constants.dart'; import 'package:helpwave_widget/bottom_sheets.dart'; import 'package:helpwave_widget/dialog.dart'; -import 'package:tasks/dataclasses/task.dart'; /// A [BottomSheet] to change the visibility class _VisibilityBottomSheet extends StatelessWidget { diff --git a/apps/tasks/lib/config/config.dart b/apps/tasks/lib/config/config.dart index 207fcd97..a0f1e628 100644 --- a/apps/tasks/lib/config/config.dart +++ b/apps/tasks/lib/config/config.dart @@ -4,7 +4,7 @@ const minimumPasswordCharacters = 6; /// Whether the development mode should be enabled /// /// Shortens the login -const bool devMode = false; +const bool devMode = true; /// The API for testing const String stagingAPIURL = "staging.api.helpwave.de"; diff --git a/apps/tasks/lib/controllers/assignee_select_controller.dart b/apps/tasks/lib/controllers/assignee_select_controller.dart deleted file mode 100644 index 19d3c5d5..00000000 --- a/apps/tasks/lib/controllers/assignee_select_controller.dart +++ /dev/null @@ -1,71 +0,0 @@ -import 'package:flutter/foundation.dart'; -import 'package:helpwave_widget/loading.dart'; -import 'package:logger/logger.dart'; -import 'package:tasks/dataclasses/task.dart'; -import 'package:tasks/services/current_ward_svc.dart'; -import 'package:tasks/services/organization_svc.dart'; -import 'package:tasks/services/task_svc.dart'; -import '../dataclasses/user.dart'; - -/// The Controller for selecting a [User] as the assignee of a [Task] -class AssigneeSelectController extends ChangeNotifier { - /// The [LoadingState] of the Controller - LoadingState _state = LoadingState.initializing; - - LoadingState get state => _state; - - set state(LoadingState value) { - _state = value; - notifyListeners(); - } - - /// The selected [User] identifier - String? _selected; - - String? get selected => _selected; - - User? get selectedUser => _users.firstWhere((user) => user.id == selected); - - /// The currently loaded user - List _users = []; - - /// The currently loaded tasks - List get users => _users; - - /// The identifier of the current [Task] it determines whether - /// changes are pushed to the server - String? _taskId; - - AssigneeSelectController({String? selected, String? taskId}) { - _selected = selected; - _taskId = taskId; - load(); - } - - /// Loads the tasks - Future load() async { - state = LoadingState.loading; - String? currentOrganization = CurrentWardService().currentWard?.organizationId; - if(currentOrganization == null){ - if(kDebugMode){ - Logger().w("Organization Id not set in CurrentWardService" - " while trying to load in AssigneeSelectController"); - } - state = LoadingState.error; - return; - } - - _users = await OrganizationService().getMembersByOrganization(currentOrganization); - state = LoadingState.loaded; - } - - /// Change the assignee - Future changeAssignee(String id) async{ - if(_taskId != null && _taskId!.isNotEmpty){ - state = LoadingState.loading; - await TaskService().assignToUser(taskId: _taskId!, userId: id); - } - _selected = id; - state = LoadingState.loaded; - } -} diff --git a/apps/tasks/lib/controllers/my_tasks_controller.dart b/apps/tasks/lib/controllers/my_tasks_controller.dart deleted file mode 100644 index 1513c18a..00000000 --- a/apps/tasks/lib/controllers/my_tasks_controller.dart +++ /dev/null @@ -1,65 +0,0 @@ -import 'package:flutter/cupertino.dart'; -import 'package:helpwave_widget/loading.dart'; -import 'package:tasks/dataclasses/task.dart'; -import 'package:tasks/services/current_ward_svc.dart'; -import 'package:tasks/services/patient_svc.dart'; -import 'package:tasks/services/task_svc.dart'; - -import '../dataclasses/patient.dart'; - -/// The Controller for [Task]s of the current [User] -class MyTasksController extends ChangeNotifier { - /// The [LoadingState] of the Controller - LoadingState state = LoadingState.initializing; - - /// The currently loaded tasks - List _tasks = []; - - /// The currently loaded tasks - List get tasks => _tasks; - - /// The loaded tasks which have [TaskStatus.todo] - List get todo => _tasks.where((element) => element.status == TaskStatus.todo).toList(); - - /// The loaded tasks which have [TaskStatus.inProgress] - List get inProgress => _tasks.where((element) => element.status == TaskStatus.inProgress).toList(); - - /// The loaded tasks which have [TaskStatus.done] - List get done => _tasks.where((element) => element.status == TaskStatus.done).toList(); - - MyTasksController() { - load(); - } - - /// Loads the tasks - Future load() async { - if (state == LoadingState.initializing) { - state = LoadingState.loading; - notifyListeners(); - } - - var patients = await PatientService().getPatientList(wardId: CurrentWardService().currentWard?.wardId); - ListallPatients = patients.all; - - _tasks = []; - // TODO use the already given information by the later updated getPatientList - for(Patient patient in allPatients) { - List tasks = await TaskService().getTasksByPatient(patientId: patient.id); - for(var task in tasks) { - _tasks.add(TaskWithPatient( - id: task.id, - name: task.name, - assignee: task.assignee, - notes: task.notes, - dueDate: task.dueDate, - status: task.status, - subtasks: task.subtasks, - patient: patient, - )); - } - } - - state = LoadingState.loaded; - notifyListeners(); - } -} diff --git a/apps/tasks/lib/controllers/patient_controller.dart b/apps/tasks/lib/controllers/patient_controller.dart deleted file mode 100644 index bf47e763..00000000 --- a/apps/tasks/lib/controllers/patient_controller.dart +++ /dev/null @@ -1,154 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:helpwave_widget/loading.dart'; -import 'package:tasks/services/patient_svc.dart'; -import '../dataclasses/bed.dart'; -import '../dataclasses/patient.dart'; -import '../dataclasses/room.dart'; - -/// The Controller for managing [Patient]s in a Ward -class PatientController extends ChangeNotifier { - /// The [LoadingState] of the Controller - LoadingState _state = LoadingState.initializing; - - /// The current [Patient], may or may not be loaded depending on the [_state] - Patient _patient; - - /// The error message to show should only be used when [state] == [LoadingState.error] - String errorMessage = ""; - - PatientController(this._patient) { - if (!_patient.isCreating) { - load(); - } - } - - LoadingState get state => _state; - - set state(LoadingState value) { - _state = value; - notifyListeners(); - } - - Patient get patient => _patient; - - set patient(Patient value) { - _patient = value; - _state = LoadingState.loaded; - notifyListeners(); - } - - /// Saves whether we are currently creating of a patient or already have them - get isCreating => _patient.isCreating; - - // Used to trigger the notify call without having to copy or save the patient locally - updatePatient(void Function(Patient patient) updateTransform, void Function(Patient patient) revertTransform) { - updateTransform(patient); - if (!isCreating) { - PatientService().updatePatient(patient).then((value) { - if (value) { - state = LoadingState.loaded; - } else { - throw Error(); - } - }).catchError((error, stackTrace) { - revertTransform(patient); - errorMessage = error.toString(); - state = LoadingState.error; - }); - } else { - state = LoadingState.loaded; - } - } - - /// A function to load the [Patient] - Future load() async { - state = LoadingState.loading; - await PatientService().getPatientDetails(patientId: patient.id).then((value) { - patient = value; - state = LoadingState.loaded; - }).catchError((error, stackTrace) { - errorMessage = error.toString(); - state = LoadingState.error; - }); - } - - /// Unassigns the patient the [patients] - Future unassign() async { - state = LoadingState.loading; - notifyListeners(); - await PatientService().unassignPatient(patientId: patient.id); - // Here we can maybe use optimistic updates - load(); - } - - /// Discharges the patient the [patients] - Future discharge() async { - state = LoadingState.loading; - notifyListeners(); - await PatientService().dischargePatient(patientId: patient.id); - // Here we can maybe use optimistic updates - load(); - } - - /// Assigns the patient to a bed - Future changeBed(RoomMinimal room, BedMinimal bed) async { - if (isCreating) { - patient.room = room; - patient.bed = bed; - state = LoadingState.loaded; - return; - } - state = LoadingState.loading; - await PatientService().assignBed(patientId: patient.id, bedId: bed.id).then((value) { - patient.room = room; - patient.bed = bed; - state = LoadingState.loaded; - }).catchError((error, stackTrace) { - errorMessage = error.toString(); - state = LoadingState.error; - }); - } - - /// Change the name of the [patient] - Future changeName(String name) async { - String oldName = name; - updatePatient( - (patient) { - patient.name = name; - }, - (patient) { - patient.name = oldName; - }, - ); - } - - /// Change the notes of the [patient] - Future changeNotes(String notes) async { - String oldNotes = notes; - updatePatient( - (patient) { - patient.notes = notes; - }, - (patient) { - patient.notes = oldNotes; - }, - ); - } - - /// Creates the patient and returns - Future create() async { - state = LoadingState.loading; - return await PatientService().createPatient(patient).then((value) async { - patient.id = value; - if (!patient.isUnassigned) { - await PatientService().assignBed(patientId: patient.id, bedId: patient.bed!.id); - } - state = LoadingState.loaded; - return true; - }).catchError((error, stackTrace) { - errorMessage = error.toString(); - state = LoadingState.error; - return false; - }); - } -} diff --git a/apps/tasks/lib/controllers/subtask_list_controller.dart b/apps/tasks/lib/controllers/subtask_list_controller.dart deleted file mode 100644 index 66620456..00000000 --- a/apps/tasks/lib/controllers/subtask_list_controller.dart +++ /dev/null @@ -1,145 +0,0 @@ -import 'dart:async'; -import 'package:flutter/cupertino.dart'; -import 'package:helpwave_widget/loading.dart'; -import 'package:tasks/dataclasses/subtask.dart'; -import 'package:tasks/services/task_svc.dart'; - -/// The Controller for managing [Subtask]s in a [Task] -/// -/// Providing a [taskId] means loading and synchronising the [SubTask]s with -/// the backend while no [taskId] or a empty [String] means that the subtasks -/// only used locally -class SubtasksController extends ChangeNotifier { - /// The [LoadingState] of the Controller - LoadingState _state = LoadingState.initializing; - - LoadingState get state => _state; - - set state(LoadingState value) { - _state = state; - notifyListeners(); - } - - /// The [Subtask]s - List _subtasks = []; - - List get subtasks => [..._subtasks]; - - set subtasks(List value) { - _subtasks = value; - notifyListeners(); - } - - bool _isCreating = false; - - bool get isCreating => _isCreating; - - String taskId; - - /// Only valid in case [state] == [LoadingState.error] - String errorMessage = ""; - - SubtasksController({this.taskId = "", List? subtasks}) { - _isCreating = taskId == ""; - if (!isCreating) { - load(); - } else { - state = LoadingState.unspecified; - } - } - - Future load() async { - state = LoadingState.loading; - if (!isCreating) { - await TaskService().getTask(id: taskId).then((task) { - subtasks = task.subtasks; - state = LoadingState.loaded; - }).onError( - (error, stackTrace) { - state = LoadingState.error; - }, - ); - return; - } - state = LoadingState.loaded; - } - - /// Delete the subtask by the index - Future deleteByIndex(int index) async { - if (index < 0 || index >= subtasks.length) { - return; - } - if (isCreating) { - _subtasks.removeAt(index); - notifyListeners(); - return; - } - state = LoadingState.loading; - await TaskService().deleteSubTask(id: subtasks[index].id).then((value) { - if (value) { - _subtasks.removeAt(index); - state = LoadingState.loaded; - } - }).catchError((error, stackTrace) { - errorMessage = error.toString(); - state = LoadingState.error; - return null; - }); - } - - /// Delete the [SubTask] by the id - Future delete(String id) async { - assert(!isCreating, "delete should not be used when creating a completely new SubTask list"); - int index = _subtasks.indexWhere((element) => element.id == id); - if (index != -1) { - deleteByIndex(index); - } - } - - /// Add the [SubTask] - Future add(SubTask subTask) async { - if (isCreating) { - _subtasks.add(subTask); - notifyListeners(); - return; - } - await TaskService().addSubTask(taskId: taskId, subTask: subTask).then((value) { - _subtasks.add(value); - state = LoadingState.loaded; - }).catchError((error, stackTrace) { - errorMessage = error.toString(); - state = LoadingState.error; - return null; - }); - } - - Future changeStatus({required SubTask subTask, required bool value}) async { - if (subTask.isCreating) { - subTask.isDone = value; - } else { - state = LoadingState.loading; - await TaskService().changeSubtaskStatus(id: subTask.id, isDone: value).then((value) { - subTask.isDone = value; - state = LoadingState.loaded; - }).catchError((error, stackTrace) { - errorMessage = error.toString(); - state = LoadingState.error; - return null; - }); - } - notifyListeners(); - } - - Future updateSubtask({required SubTask subTask}) async { - if (!subTask.isCreating) { - state = LoadingState.loading; - await TaskService().updateSubTask(subTask: subTask).then((value) { - state = LoadingState.loaded; - }).catchError((error, stackTrace) { - // Just reload in case of an error - load(); - }); - } - notifyListeners(); - } -} diff --git a/apps/tasks/lib/controllers/task_controller.dart b/apps/tasks/lib/controllers/task_controller.dart deleted file mode 100644 index 917961fe..00000000 --- a/apps/tasks/lib/controllers/task_controller.dart +++ /dev/null @@ -1,164 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:helpwave_widget/loading.dart'; -import '../dataclasses/patient.dart'; -import '../dataclasses/task.dart'; -import '../services/task_svc.dart'; - -/// The Controller for managing a [TaskWithPatient] -class TaskController extends ChangeNotifier { - /// The [LoadingState] of the Controller - LoadingState _state = LoadingState.initializing; - - /// The current [Task], may or may not be loaded depending on the [_state] - TaskWithPatient _task; - - /// The error message to show should only be used when [state] == [LoadingState.error] - String errorMessage = ""; - - TaskController(this._task) { - if (!_task.isCreating) { - load(); - } else { - state = LoadingState.unspecified; - } - } - - LoadingState get state => _state; - - set state(LoadingState value) { - _state = value; - notifyListeners(); - } - - TaskWithPatient get task => _task; - - set task(TaskWithPatient value) { - _task = value; - _state = LoadingState.loaded; - notifyListeners(); - } - - /// Used to trigger the notify call without having to copy or save the Task locally - updateTask(void Function(TaskWithPatient task) updateTransform, void Function(TaskWithPatient task) revertTransform) { - updateTransform(task); - if (!isCreating) { - TaskService().updateTask(task).then((value) { - if (value) { - state = LoadingState.loaded; - } else { - throw Error(); - } - }).catchError((error, stackTrace) { - revertTransform(task); - errorMessage = error.toString(); - state = LoadingState.error; - }); - } else { - state = LoadingState.loaded; - } - } - - bool get isCreating => _task.isCreating; - - // only create when a patient is assigned - bool get isReadyForCreate => !task.patient.isCreating; - - PatientMinimal get patient => task.patient; - - /// A function to load the [Task] - load() async { - state = LoadingState.loading; - await TaskService().getTask(id: task.id).then((value) { - task = value; - state = LoadingState.loaded; - }).catchError((error, stackTrace) { - errorMessage = error.toString(); - state = LoadingState.error; - }); - } - - /// Changes the Assignee - /// - /// Without a backend request as we expect this to be done in the [AssigneeSelectController] - Future changeAssignee(String assigneeId) async { - String? old = task.assignee; - updateTask( - (task) { - task.assignee = assigneeId; - }, - (task) { - task.assignee = old; - }, - ); - } - - Future changeName(String name) async { - String oldName = task.name; - updateTask( - (task) { - task.name = name; - }, - (task) { - task.name = oldName; - }, - ); - } - - Future changeIsPublic(bool isPublic) async { - bool old = task.isPublicVisible; - updateTask( - (task) { - task.isPublicVisible = isPublic; - }, - (task) { - task.isPublicVisible = old; - }, - ); - } - - Future changeNotes(String notes) async { - String oldNotes = notes; - updateTask( - (task) { - task.notes = notes; - }, - (task) { - task.notes = oldNotes; - }, - ); - } - - Future changeDueDate(DateTime? dueDate) async { - DateTime? old = task.dueDate; - updateTask( - (task) { - task.dueDate = dueDate; - }, - (task) { - task.dueDate = old; - }, - ); - } - - /// Only usable when creating - Future changePatient(PatientMinimal patient) async { - assert(task.isCreating, "Only use TaskController.changePatient, when you create a new task."); - task.patient = patient; - notifyListeners(); - } - - /// Creates the Task and returns - Future create() async { - assert(!task.patient.isCreating, "A the patient must be set to create a task"); - state = LoadingState.loading; - return await TaskService().createTask(task).then((value) { - task.id = value; - state = LoadingState.loaded; - return true; - }).catchError((error, stackTrace) { - errorMessage = error.toString(); - state = LoadingState.error; - return false; - }); - } -} diff --git a/apps/tasks/lib/dataclasses/organization.dart b/apps/tasks/lib/dataclasses/organization.dart deleted file mode 100644 index 0d3a6e96..00000000 --- a/apps/tasks/lib/dataclasses/organization.dart +++ /dev/null @@ -1,17 +0,0 @@ -/// data class for [Organization] -class Organization { - String id; - String name; - String shortName; - - Organization({ - required this.id, - required this.name, - required this.shortName, - }); - - @override - String toString() { - return "{id: $id, name: $name, shortName: $shortName}"; - } -} diff --git a/apps/tasks/lib/dataclasses/subtask.dart b/apps/tasks/lib/dataclasses/subtask.dart deleted file mode 100644 index b48c6d32..00000000 --- a/apps/tasks/lib/dataclasses/subtask.dart +++ /dev/null @@ -1,14 +0,0 @@ -/// data class for [SubTask] -class SubTask { - String id; - String name; - bool isDone; - - bool get isCreating => id == ""; - - SubTask({ - required this.id, - required this.name, - this.isDone = false - }); -} diff --git a/apps/tasks/lib/dataclasses/task.dart b/apps/tasks/lib/dataclasses/task.dart deleted file mode 100644 index b1633960..00000000 --- a/apps/tasks/lib/dataclasses/task.dart +++ /dev/null @@ -1,102 +0,0 @@ -// TODO delete later and import from protobufs -import 'package:tasks/dataclasses/patient.dart'; -import 'package:tasks/dataclasses/subtask.dart'; - -enum TaskStatus { - unspecified, - todo, - inProgress, - done, -} - -/// data class for [Task] -class Task { - String id; - String name; - String? assignee; - String notes; - TaskStatus status; - List subtasks; - DateTime? dueDate; - DateTime? creationDate; - bool isPublicVisible; - - static get empty => Task(id: "", name: "name", notes: ""); - - final _nullID = "00000000-0000-0000-0000-000000000000"; - - double get progress => subtasks.isNotEmpty - ? subtasks.where((element) => element.isDone).length / subtasks.length - : 1; - - /// the remaining time until a task is due - /// - /// **NOTE**: returns [Duration.zero] if [dueDate] is null - Duration get remainingTime => - dueDate != null ? dueDate!.difference(DateTime.now()) : Duration.zero; - - bool get isOverdue => remainingTime.isNegative; - - bool get inNextTwoDays => remainingTime.inDays < 2; - - bool get inNextHour => remainingTime.inHours < 1; - - bool get isCreating => id == ""; - - bool get hasAssignee => assignee != null && assignee != "" && assignee != _nullID; - - Task({ - required this.id, - required this.name, - required this.notes, - this.assignee, - this.status = TaskStatus.todo, - this.subtasks = const [], - this.dueDate, - this.creationDate, - this.isPublicVisible = false, - }); -} - -class TaskWithPatient extends Task { - PatientMinimal patient; - - factory TaskWithPatient.empty({ - String taskId = "", - PatientMinimal? patient, - }) { - return TaskWithPatient(id: taskId, name: "task name", notes: "", patient: patient ?? PatientMinimal.empty()); - } - - 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, - assignee: task.assignee, - patient: patient ?? PatientMinimal.empty(), - ); - } - - TaskWithPatient({ - required super.id, - required super.name, - required super.notes, - super.assignee, - super.status, - super.subtasks, - super.dueDate, - super.creationDate, - super.isPublicVisible, - required this.patient, - }); -} diff --git a/apps/tasks/lib/dataclasses/task_template.dart b/apps/tasks/lib/dataclasses/task_template.dart deleted file mode 100644 index 35309363..00000000 --- a/apps/tasks/lib/dataclasses/task_template.dart +++ /dev/null @@ -1,22 +0,0 @@ -import 'package:tasks/dataclasses/task_template_subtask.dart'; - -/// data class for [TaskTemplate] -class TaskTemplate { - String id; - String? wardID; - String name; - String notes; - List subtasks; - bool isPublicVisible; - - get isWardTemplate => wardID != null; - - TaskTemplate({ - required this.id, - this.wardID, - required this.name, - required this.notes, - this.subtasks = const [], - this.isPublicVisible = false, - }); -} diff --git a/apps/tasks/lib/debug/theme_visualizer.dart b/apps/tasks/lib/debug/theme_visualizer.dart index fe0b59bf..be3cee9f 100644 --- a/apps/tasks/lib/debug/theme_visualizer.dart +++ b/apps/tasks/lib/debug/theme_visualizer.dart @@ -5,9 +5,7 @@ import 'package:helpwave_widget/loading.dart'; import 'package:provider/provider.dart'; import 'package:tasks/components/subtask_list.dart'; import 'package:tasks/components/task_card.dart'; -import 'package:tasks/dataclasses/patient.dart'; - -import '../dataclasses/task.dart'; +import 'package:helpwave_service/tasks.dart'; class ThemeVisualizer extends StatelessWidget { const ThemeVisualizer({super.key}); @@ -46,6 +44,7 @@ class ThemeVisualizer extends StatelessWidget { name: "Task", notes: "Some Notes", status: TaskStatus.inProgress, + patientId: "patient", dueDate: DateTime.now().add(const Duration(hours: 2)), patient: PatientMinimal( id: "patient", diff --git a/apps/tasks/lib/main.dart b/apps/tasks/lib/main.dart index f6e1808b..65b37680 100644 --- a/apps/tasks/lib/main.dart +++ b/apps/tasks/lib/main.dart @@ -1,14 +1,23 @@ import 'package:flutter/material.dart'; +import 'package:helpwave_service/auth.dart'; +import 'package:helpwave_service/user.dart'; import 'package:provider/provider.dart'; import 'package:helpwave_localization/l10n/app_localizations.dart'; import 'package:helpwave_localization/localization.dart'; import 'package:helpwave_localization/localization_model.dart'; import 'package:helpwave_theme/theme.dart'; +import 'package:tasks/config/config.dart'; +import 'package:helpwave_service/tasks.dart'; import 'package:tasks/screens/login_screen.dart'; -import 'package:tasks/services/current_ward_svc.dart'; -import 'controllers/user_session_controller.dart'; void main() { + UserSessionService().changeMode(devMode); + TasksAPIServiceClients() + ..apiUrl = usedAPIURL + ..offlineMode = true; + UserAPIServiceClients() + ..apiUrl = usedAPIURL + ..offlineMode = true; runApp(const MyApp()); } @@ -26,7 +35,7 @@ class MyApp extends StatelessWidget { create: (_) => LanguageModel(), ), ChangeNotifierProvider( - create: (_) => CurrentWardController(), + create: (_) => CurrentWardController(devMode: devMode), ), ChangeNotifierProvider( create: (_) => UserSessionController(), diff --git a/apps/tasks/lib/screens/landing_screen.dart b/apps/tasks/lib/screens/landing_screen.dart index ba487dd1..052622a1 100644 --- a/apps/tasks/lib/screens/landing_screen.dart +++ b/apps/tasks/lib/screens/landing_screen.dart @@ -1,10 +1,11 @@ import 'package:flutter/material.dart'; import 'package:helpwave_localization/localization.dart'; +import 'package:helpwave_service/auth.dart'; import 'package:helpwave_theme/constants.dart'; import 'package:helpwave_theme/theme.dart'; +import 'package:helpwave_theme/util.dart'; import 'package:helpwave_widget/loading.dart'; import 'package:provider/provider.dart'; -import 'package:tasks/controllers/user_session_controller.dart'; /// The Landing Screen of the Application class LandingScreen extends StatelessWidget { @@ -24,7 +25,8 @@ class LandingScreen extends StatelessWidget { } return OutlinedButton( - style: buttonStyleBig.copyWith(side: const MaterialStatePropertyAll(buttonBorderSideBig)), + style: buttonStyleBig.copyWith(side: MaterialStatePropertyAll(buttonBorderSideBig.copyWith(color: context + .theme.colorScheme.onBackground))), child: Text( context.localization!.loginSlogan, style: Theme.of(context).textTheme.labelLarge, diff --git a/apps/tasks/lib/screens/login_screen.dart b/apps/tasks/lib/screens/login_screen.dart index 8d963cb4..89ac9a6d 100644 --- a/apps/tasks/lib/screens/login_screen.dart +++ b/apps/tasks/lib/screens/login_screen.dart @@ -1,8 +1,8 @@ import 'package:flutter/cupertino.dart'; +import 'package:helpwave_service/auth.dart'; import 'package:provider/provider.dart'; import 'package:tasks/screens/landing_screen.dart'; import 'package:tasks/screens/main_screen.dart'; -import '../controllers/user_session_controller.dart'; /// A Screen for forcing the User to login or be logged in /// diff --git a/apps/tasks/lib/screens/main_screen.dart b/apps/tasks/lib/screens/main_screen.dart index 690e5d2b..60797a42 100644 --- a/apps/tasks/lib/screens/main_screen.dart +++ b/apps/tasks/lib/screens/main_screen.dart @@ -1,19 +1,19 @@ import 'dart:async'; import 'package:flutter/material.dart'; import 'package:helpwave_localization/localization.dart'; +import 'package:helpwave_service/auth.dart'; +import 'package:helpwave_service/tasks.dart'; import 'package:helpwave_theme/constants.dart'; import 'package:helpwave_theme/theme.dart'; import 'package:helpwave_widget/bottom_sheets.dart'; import 'package:helpwave_widget/animation.dart'; import 'package:provider/provider.dart'; import 'package:tasks/components/patient_bottom_sheet.dart'; +import 'package:tasks/components/task_bottom_sheet.dart'; import 'package:tasks/components/user_header.dart'; import 'package:tasks/screens/main_screen_subscreens/my_tasks_screen.dart'; import 'package:tasks/screens/main_screen_subscreens/patient_screen.dart'; import 'package:tasks/screens/ward_select_screen.dart'; -import 'package:tasks/services/current_ward_svc.dart'; -import '../components/task_bottom_sheet.dart'; -import '../dataclasses/task.dart'; /// The main screen of the app /// @@ -142,7 +142,7 @@ class _TaskPatientFloatingActionButton extends StatelessWidget { onPressed: () { context.pushModal( context: context, - builder: (context) => TaskBottomSheet(task: Task.empty), + builder: (context) => TaskBottomSheet(task: Task.empty("")), ); }, ), diff --git a/apps/tasks/lib/screens/main_screen_subscreens/my_tasks_screen.dart b/apps/tasks/lib/screens/main_screen_subscreens/my_tasks_screen.dart index 47a93010..54b2ca37 100644 --- a/apps/tasks/lib/screens/main_screen_subscreens/my_tasks_screen.dart +++ b/apps/tasks/lib/screens/main_screen_subscreens/my_tasks_screen.dart @@ -1,11 +1,10 @@ import 'package:flutter/material.dart'; +import 'package:helpwave_service/tasks.dart'; import 'package:helpwave_theme/constants.dart'; import 'package:provider/provider.dart'; import 'package:helpwave_localization/localization.dart'; import 'package:tasks/components/task_expansion_tile.dart'; import 'package:helpwave_widget/loading.dart'; -import '../../controllers/my_tasks_controller.dart'; -import '../../dataclasses/task.dart'; /// The Screen for showing all [Task]'s the [User] has assigned to them class MyTasksScreen extends StatefulWidget { @@ -19,9 +18,9 @@ class _MyTasksScreenState extends State { @override Widget build(BuildContext context) { return ChangeNotifierProvider( - create: (_) => MyTasksController(), - child: Consumer( - builder: (BuildContext context, MyTasksController tasksController, Widget? child) { + create: (_) => AssignedTasksController(), + child: Consumer( + builder: (BuildContext context, AssignedTasksController tasksController, Widget? child) { return LoadingAndErrorWidget( state: tasksController.state, child: ListView(children: [ diff --git a/apps/tasks/lib/screens/main_screen_subscreens/patient_screen.dart b/apps/tasks/lib/screens/main_screen_subscreens/patient_screen.dart index 511028a9..2278a361 100644 --- a/apps/tasks/lib/screens/main_screen_subscreens/patient_screen.dart +++ b/apps/tasks/lib/screens/main_screen_subscreens/patient_screen.dart @@ -8,9 +8,7 @@ import 'package:tasks/components/patient_bottom_sheet.dart'; import 'package:tasks/components/patient_card.dart'; import 'package:tasks/components/patient_status_chip_select.dart'; import 'package:tasks/components/task_bottom_sheet.dart'; -import 'package:tasks/controllers/ward_patients_controller.dart'; -import 'package:tasks/dataclasses/patient.dart'; -import 'package:tasks/dataclasses/task.dart'; +import 'package:helpwave_service/tasks.dart'; /// A screen for showing a all [Patient]s by certain user-selectable filter properties /// @@ -120,7 +118,7 @@ class _PatientScreenState extends State { context.pushModal( context: context, builder: (context) => TaskBottomSheet( - task: Task.empty, + task: Task.empty(patient.id), patient: patient, ), ).then((value) => patientController.load()); diff --git a/apps/tasks/lib/screens/settings_screen.dart b/apps/tasks/lib/screens/settings_screen.dart index 6258c1e7..e943d53b 100644 --- a/apps/tasks/lib/screens/settings_screen.dart +++ b/apps/tasks/lib/screens/settings_screen.dart @@ -1,12 +1,11 @@ import 'package:flutter/material.dart'; import 'package:helpwave_localization/localization.dart'; import 'package:helpwave_localization/localization_model.dart'; +import 'package:helpwave_service/auth.dart'; import 'package:helpwave_theme/constants.dart'; import 'package:helpwave_theme/theme.dart'; import 'package:provider/provider.dart'; import 'package:tasks/screens/login_screen.dart'; -import 'package:tasks/services/user_session_service.dart'; -import 'package:tasks/services/current_ward_svc.dart'; /// Screen for settings and other app options class SettingsScreen extends StatefulWidget { diff --git a/apps/tasks/lib/screens/ward_select_screen.dart b/apps/tasks/lib/screens/ward_select_screen.dart index f0bda449..41c06071 100644 --- a/apps/tasks/lib/screens/ward_select_screen.dart +++ b/apps/tasks/lib/screens/ward_select_screen.dart @@ -1,16 +1,14 @@ import 'package:flutter/material.dart'; import 'package:helpwave_localization/localization.dart'; +import 'package:helpwave_service/auth.dart'; +import 'package:helpwave_service/tasks.dart'; +import 'package:helpwave_service/user.dart'; import 'package:helpwave_theme/constants.dart'; import 'package:helpwave_widget/content_selection.dart'; import 'package:provider/provider.dart'; -import 'package:tasks/dataclasses/organization.dart'; import 'package:tasks/screens/settings_screen.dart'; -import 'package:tasks/services/current_ward_svc.dart'; -import 'package:tasks/services/organization_svc.dart'; -import 'package:tasks/services/ward_service.dart'; -import '../dataclasses/ward.dart'; -/// A Screen to select the current [Organization] and [Ward] +/// A Screen to select the current [OrganizationService] and [Ward] class WardSelectScreen extends StatefulWidget { const WardSelectScreen({super.key}); @@ -44,8 +42,7 @@ class _WardSelectScreen extends State { body: Column( children: [ ListTile( - // TODO change to organization name - title: Text(organization?.name ?? context.localization!.none), + title: Text(organization?.longName ?? context.localization!.none), subtitle: Text(context.localization!.organization), trailing: const Icon(Icons.arrow_forward), onTap: () => Navigator.push( @@ -57,7 +54,7 @@ class _WardSelectScreen extends State { List organizations = await OrganizationService().getOrganizationsForUser(); return organizations; }, - elementToString: (Organization t) => t.name, + elementToString: (Organization t) => t.longName, ), ), ).then((value) { @@ -73,7 +70,6 @@ class _WardSelectScreen extends State { }), ), ListTile( - // TODO change to organization name title: Text(ward?.name ?? context.localization!.none), subtitle: Text(context.localization!.ward), trailing: const Icon(Icons.arrow_forward), diff --git a/apps/tasks/lib/services/grpc_client_svc.dart b/apps/tasks/lib/services/grpc_client_svc.dart deleted file mode 100644 index 30278c22..00000000 --- a/apps/tasks/lib/services/grpc_client_svc.dart +++ /dev/null @@ -1,76 +0,0 @@ -import 'package:grpc/grpc.dart'; -import 'package:helpwave_proto_dart/proto/services/task_svc/v1/patient_svc.pbgrpc.dart'; -import 'package:helpwave_proto_dart/proto/services/task_svc/v1/room_svc.pbgrpc.dart'; -import 'package:helpwave_proto_dart/proto/services/task_svc/v1/task_svc.pbgrpc.dart'; -import 'package:helpwave_proto_dart/proto/services/task_svc/v1/ward_svc.pbgrpc.dart'; -import 'package:helpwave_proto_dart/proto/services/user_svc/v1/organization_svc.pbgrpc.dart'; -import 'package:helpwave_proto_dart/proto/services/user_svc/v1/user_svc.pbgrpc.dart'; -import 'package:tasks/config/config.dart'; -import 'package:tasks/services/current_ward_svc.dart'; -import 'user_session_service.dart'; - -/// The Underlying GrpcService it provides other clients and the correct metadata for the requests -class GRPCClientService { - static final taskServiceChannel = ClientChannel( - usedAPIURL, - ); - static final userServiceChannel = ClientChannel( - usedAPIURL, - ); - - final UserSessionService authService = UserSessionService(); - - Map get authMetaData { - if (authService.isLoggedIn) { - return { - "Authorization": "Bearer ${UserSessionService().identity?.idToken}", - }; - } - // Maybe throw a error instead - return {}; - } - - String? get fallbackOrganizationId => - // Maybe throw a error instead for the last case - CurrentWardService().currentWard?.organizationId ?? authService.identity?.firstOrganization; - - Map getTaskServiceMetaData({String? organizationId}) { - var metaData = { - ...authMetaData, - "dapr-app-id": "task-svc", - }; - - if (organizationId != null) { - metaData["X-Organization"] = organizationId; - } else { - metaData["X-Organization"] = fallbackOrganizationId!; - } - - return metaData; - } - - Map getUserServiceMetaData({String? organizationId}) { - var metaData = { - ...authMetaData, - "dapr-app-id": "user-svc", - }; - - if (organizationId != null) { - metaData["X-Organization"] = organizationId; - } - - return metaData; - } - - static PatientServiceClient get getPatientServiceClient => PatientServiceClient(taskServiceChannel); - - static WardServiceClient get getWardServiceClient => WardServiceClient(taskServiceChannel); - - static RoomServiceClient get getRoomServiceClient => RoomServiceClient(taskServiceChannel); - - static TaskServiceClient get getTaskServiceClient => TaskServiceClient(taskServiceChannel); - - static UserServiceClient get getUserServiceClient => UserServiceClient(userServiceChannel); - - static OrganizationServiceClient get getOrganizationServiceClient => OrganizationServiceClient(userServiceChannel); -} diff --git a/apps/tasks/lib/services/patient_svc.dart b/apps/tasks/lib/services/patient_svc.dart deleted file mode 100644 index c0c9ede6..00000000 --- a/apps/tasks/lib/services/patient_svc.dart +++ /dev/null @@ -1,295 +0,0 @@ -import 'package:grpc/grpc.dart'; -import 'package:helpwave_proto_dart/proto/services/task_svc/v1/patient_svc.pbgrpc.dart'; -import 'package:tasks/dataclasses/bed.dart'; -import 'package:tasks/dataclasses/patient.dart'; -import 'package:tasks/dataclasses/room.dart'; -import 'package:tasks/dataclasses/subtask.dart'; -import 'package:tasks/dataclasses/ward.dart'; -import 'package:tasks/services/grpc_client_svc.dart'; - -import '../dataclasses/task.dart'; - -/// The GRPC Service for [Patient]s -/// -/// Provides queries and requests that load or alter [Patient] objects on the server -/// The server is defined in the underlying [GRPCClientService] -class PatientService { - /// The GRPC ServiceClient which handles GRPC - PatientServiceClient patientService = GRPCClientService.getPatientServiceClient; - - // TODO consider an enum instead of an string - /// Loads the [Patient]s by [Ward] and sorts them by their assignment status - Future getPatientList({String? wardId}) async { - GetPatientListRequest request = GetPatientListRequest(wardId: wardId); - GetPatientListResponse response = await patientService.getPatientList( - request, - options: CallOptions( - metadata: GRPCClientService().getTaskServiceMetaData(), - ), - ); - - Map taskStatusMapping = { - GetPatientListResponse_TaskStatus.TASK_STATUS_TODO: TaskStatus.todo, - GetPatientListResponse_TaskStatus.TASK_STATUS_IN_PROGRESS: TaskStatus.inProgress, - GetPatientListResponse_TaskStatus.TASK_STATUS_DONE: TaskStatus.done, - GetPatientListResponse_TaskStatus.TASK_STATUS_UNSPECIFIED: TaskStatus.unspecified, - }; - - List active = response.active - .map( - (patient) => Patient( - id: patient.id, - name: patient.humanReadableIdentifier, - isDischarged: response.dischargedPatients.contains(patient), - tasks: patient.tasks - .map((task) => Task( - id: task.id, - name: task.name, - notes: task.description, - status: taskStatusMapping[task.status]!, - isPublicVisible: task.public, - assignee: task.assignedUserId, - subtasks: task.subtasks - .map((subtask) => SubTask( - id: subtask.id, - name: subtask.name, - isDone: subtask.done, - )) - .toList(), - // TODO due and creation date - )) - .toList(), - notes: patient.notes, - bed: BedMinimal(id: patient.bed.id, name: patient.bed.name), - room: RoomMinimal(id: patient.room.id, name: patient.room.name), - ), - ) - .toList(); - - List unassigned = response.unassignedPatients - .map( - (patient) => Patient( - id: patient.id, - name: patient.humanReadableIdentifier, - isDischarged: response.dischargedPatients.contains(patient), - tasks: patient.tasks - .map((task) => Task( - id: task.id, - name: task.name, - notes: task.description, - status: taskStatusMapping[task.status]!, - isPublicVisible: task.public, - assignee: task.assignedUserId, - subtasks: task.subtasks - .map((subtask) => SubTask( - id: subtask.id, - name: subtask.name, - isDone: subtask.done, - )) - .toList(), - // TODO due and creation date - )) - .toList(), - notes: patient.notes, - ), - ) - .toList(); - - List discharged = response.dischargedPatients - .map( - (patient) => Patient( - id: patient.id, - name: patient.humanReadableIdentifier, - isDischarged: true, - tasks: patient.tasks - .map((task) => Task( - id: task.id, - name: task.name, - notes: task.description, - status: taskStatusMapping[task.status]!, - isPublicVisible: task.public, - assignee: task.assignedUserId, - subtasks: task.subtasks - .map((subtask) => SubTask( - id: subtask.id, - name: subtask.name, - isDone: subtask.done, - )) - .toList(), - // TODO due and creation date - )) - .toList(), - notes: patient.notes, - ), - ) - .toList(); - - return PatientsByAssignmentStatus( - all: active + unassigned + discharged, - active: active, - unassigned: unassigned, - discharged: discharged, - ); - } - - /// Loads the [Patient]s by id - Future getPatient({required String patientId}) async { - GetPatientRequest request = GetPatientRequest(id: patientId); - GetPatientResponse response = await patientService.getPatient( - request, - options: CallOptions( - metadata: GRPCClientService().getTaskServiceMetaData(), - ), - ); - - // TODO maybe also use bedId and notes from response - return PatientMinimal( - id: response.id, - name: response.humanReadableIdentifier, - ); - } - - /// Loads the [Patient]s with detailed information - Future getPatientDetails({required String patientId}) async { - GetPatientDetailsRequest request = GetPatientDetailsRequest(id: patientId); - GetPatientDetailsResponse response = await patientService.getPatientDetails( - request, - options: CallOptions( - metadata: GRPCClientService().getTaskServiceMetaData(), - ), - ); - - Map statusMap = { - GetPatientDetailsResponse_TaskStatus.TASK_STATUS_TODO: TaskStatus.todo, - GetPatientDetailsResponse_TaskStatus.TASK_STATUS_IN_PROGRESS: TaskStatus.inProgress, - GetPatientDetailsResponse_TaskStatus.TASK_STATUS_DONE: TaskStatus.done, - GetPatientDetailsResponse_TaskStatus.TASK_STATUS_UNSPECIFIED: TaskStatus.unspecified, - }; - - return Patient( - id: response.id, - name: response.humanReadableIdentifier, - notes: response.notes, - isDischarged: response.isDischarged, - tasks: response.tasks - .map((task) => Task( - id: task.id, - name: task.name, - notes: task.description, - assignee: task.assignedUserId, - status: statusMap[task.status]!, - isPublicVisible: task.public, - subtasks: task.subtasks - .map((subtask) => SubTask( - id: subtask.id, - name: subtask.name, - )) - .toList(), - )) - .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, - ); - } - - /// 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( - request, - options: CallOptions( - metadata: GRPCClientService().getTaskServiceMetaData(), - ), - ); - - return response.rooms.map((room) { - var beds = room.beds; - return RoomWithBedWithMinimalPatient( - id: room.id, - name: room.id, - beds: beds.map((bed) { - var patient = bed.patient; - return BedWithMinimalPatient( - id: bed.id, - name: bed.name, - patient: patient.isInitialized() ? PatientMinimal(id: patient.id, name: patient.name) : null, - ); - }).toList()); - }).toList(); - } - - /// Create a [Patient] - Future createPatient(Patient patient) async { - CreatePatientRequest request = CreatePatientRequest( - notes: patient.notes, - humanReadableIdentifier: patient.name, - ); - CreatePatientResponse response = await patientService.createPatient( - request, - options: CallOptions(metadata: GRPCClientService().getTaskServiceMetaData()), - ); - - return response.id; - } - - /// Update a [Patient] - Future updatePatient(Patient patient) async { - UpdatePatientRequest request = UpdatePatientRequest( - id: patient.id, - notes: patient.notes, - humanReadableIdentifier: patient.name, - ); - UpdatePatientResponse response = await patientService.updatePatient( - request, - options: CallOptions(metadata: GRPCClientService().getTaskServiceMetaData()), - ); - - if (response.isInitialized()) { - return true; - } - return false; - } - - // TODO consider an enum instead of an string - /// Discharges a [Patient] - Future dischargePatient({required String patientId}) async { - DischargePatientRequest request = DischargePatientRequest(id: patientId); - DischargePatientResponse response = await patientService.dischargePatient( - request, - options: CallOptions(metadata: GRPCClientService().getTaskServiceMetaData()), - ); - - if (response.isInitialized()) { - return true; - } - return false; - } - - /// Unassigns a [Patient] from a [Bed] - Future unassignPatient({required String patientId}) async { - UnassignBedRequest request = UnassignBedRequest(id: patientId); - UnassignBedResponse response = await patientService.unassignBed( - request, - options: CallOptions(metadata: GRPCClientService().getTaskServiceMetaData()), - ); - - if (response.isInitialized()) { - return true; - } - return false; - } - - /// Assigns a [Patient] to a [Bed] - Future assignBed({required String patientId, required String bedId}) async { - AssignBedRequest request = AssignBedRequest(id: patientId, bedId: bedId); - AssignBedResponse response = await patientService.assignBed( - request, - options: CallOptions(metadata: GRPCClientService().getTaskServiceMetaData()), - ); - - if (response.isInitialized()) { - return true; - } - return false; - } -} diff --git a/apps/tasks/lib/services/task_svc.dart b/apps/tasks/lib/services/task_svc.dart deleted file mode 100644 index 642e45f4..00000000 --- a/apps/tasks/lib/services/task_svc.dart +++ /dev/null @@ -1,197 +0,0 @@ -import 'package:grpc/grpc.dart'; -import 'package:helpwave_proto_dart/google/protobuf/timestamp.pb.dart'; -import 'package:helpwave_proto_dart/proto/services/task_svc/v1/task_svc.pbgrpc.dart'; -import 'package:tasks/dataclasses/patient.dart'; -import 'package:tasks/dataclasses/subtask.dart'; -import 'package:tasks/services/grpc_client_svc.dart'; -import 'package:tasks/util/task_status_mapping.dart'; -import '../dataclasses/task.dart'; - -/// The GRPC Service for [Task]s -/// -/// Provides queries and requests that load or alter [Task] objects on the server -/// The server is defined in the underlying [GRPCClientService] -class TaskService { - /// The GRPC ServiceClient which handles GRPC - TaskServiceClient taskService = GRPCClientService.getTaskServiceClient; - - /// Loads the [Task]s by a [Patient] identifier - Future> getTasksByPatient({String? patientId}) async { - GetTasksByPatientRequest request = GetTasksByPatientRequest(patientId: patientId); - GetTasksByPatientResponse response = await taskService.getTasksByPatient( - request, - options: CallOptions(metadata: GRPCClientService().getTaskServiceMetaData()), - ); - - return response.tasks - .map((task) => Task( - id: task.id, - name: task.name, - notes: task.description, - isPublicVisible: task.public, - status: taskStatusMappingFromProto[task.status]!, - assignee: task.assignedUserId, - dueDate: task.dueAt.toDateTime(), - subtasks: task.subtasks - .map((subtask) => SubTask( - id: subtask.id, - name: subtask.name, - isDone: subtask.done, - )) - .toList(), - )) - .toList(); - } - - /// Loads the [Task]s by it's identifier - Future getTask({String? id}) async { - GetTaskRequest request = GetTaskRequest(id: id); - GetTaskResponse response = await taskService.getTask( - request, - options: CallOptions(metadata: GRPCClientService().getTaskServiceMetaData()), - ); - - return TaskWithPatient( - id: response.id, - name: response.name, - notes: response.description, - isPublicVisible: response.public, - status: taskStatusMappingFromProto[response.status]!, - assignee: response.assignedUserId, - dueDate: response.dueAt.toDateTime(), - patient: PatientMinimal(id: response.patient.id, name: response.patient.name), - subtasks: response.subtasks - .map((subtask) => SubTask( - id: subtask.id, - name: subtask.name, - isDone: subtask.done, - )) - .toList(), - ); - } - - Future createTask(TaskWithPatient task) async { - CreateTaskRequest request = CreateTaskRequest( - name: task.name, - description: task.notes, - initialStatus: taskStatusMappingToProto[task.status], - dueAt: task.dueDate != null ? Timestamp.fromDateTime(task.dueDate!) : null, - patientId: !task.patient.isCreating ? task.patient.id : null, - public: task.isPublicVisible, - ); - CreateTaskResponse response = await taskService.createTask( - request, - options: CallOptions(metadata: GRPCClientService().getTaskServiceMetaData()), - ); - - return response.id; - } - - /// Assign a [Task] to a [User] - Future assignToUser({required String taskId, required String userId}) async { - AssignTaskToUserRequest request = AssignTaskToUserRequest(id: taskId, userId: userId); - AssignTaskToUserResponse response = await taskService.assignTaskToUser( - request, - options: CallOptions(metadata: GRPCClientService().getTaskServiceMetaData()), - ); - - if (!response.isInitialized()) { - // Handle error - } - } - - /// Add a [SubTask] to a [Task] - Future addSubTask({required String taskId, required SubTask subTask}) async { - AddSubTaskRequest request = AddSubTaskRequest( - taskId: taskId, - name: subTask.name, - done: subTask.isDone, - ); - AddSubTaskResponse response = await taskService.addSubTask( - request, - options: CallOptions(metadata: GRPCClientService().getTaskServiceMetaData()), - ); - - return SubTask( - id: response.id, - name: subTask.name, - isDone: subTask.isDone, - ); - } - - /// Delete a [SubTask] by its identifier - Future deleteSubTask({required String id}) async { - RemoveSubTaskRequest request = RemoveSubTaskRequest(id: id); - RemoveSubTaskResponse response = await taskService.removeSubTask( - request, - options: CallOptions(metadata: GRPCClientService().getTaskServiceMetaData()), - ); - - return response.isInitialized(); - } - - /// Change a [SubTask]'s status to done by its identifier - Future subtaskToDone({required String id}) async { - SubTaskToDoneRequest request = SubTaskToDoneRequest(id: id); - SubTaskToDoneResponse response = await taskService.subTaskToDone( - request, - options: CallOptions(metadata: GRPCClientService().getTaskServiceMetaData()), - ); - - return response.isInitialized(); - } - - /// Change a [SubTask]'s status to todo by its identifier - Future subtaskToToDo({required String id}) async { - SubTaskToToDoRequest request = SubTaskToToDoRequest(id: id); - SubTaskToToDoResponse response = await taskService.subTaskToToDo( - request, - options: CallOptions(metadata: GRPCClientService().getTaskServiceMetaData()), - ); - - return response.isInitialized(); - } - - /// Change a [SubTask]'s status by its identifier - Future changeSubtaskStatus({ - required String id, - required isDone, - }) async { - if (isDone) { - return subtaskToDone(id: id); - } else { - return subtaskToToDo(id: id); - } - } - - /// Update a [SubTask]'s - Future updateSubTask({required SubTask subTask}) async { - UpdateSubTaskRequest request = UpdateSubTaskRequest( - id: subTask.id, - name: subTask.name, - ); - UpdateSubTaskResponse response = await taskService.updateSubTask( - request, - options: CallOptions(metadata: GRPCClientService().getTaskServiceMetaData()), - ); - - return response.isInitialized(); - } - - Future updateTask(Task task) async { - UpdateTaskRequest request = UpdateTaskRequest( - id: task.id, - name: task.name, - description: task.notes, - dueAt: task.dueDate != null ? Timestamp.fromDateTime(task.dueDate!) : null, - public: task.isPublicVisible, - ); - - UpdateTaskResponse response = await taskService.updateTask( - request, - options: CallOptions(metadata: GRPCClientService().getTaskServiceMetaData()), - ); - - return response.isInitialized(); - } -} diff --git a/apps/tasks/lib/util/task_status_mapping.dart b/apps/tasks/lib/util/task_status_mapping.dart deleted file mode 100644 index 6ee32cdf..00000000 --- a/apps/tasks/lib/util/task_status_mapping.dart +++ /dev/null @@ -1,16 +0,0 @@ -import 'package:tasks/dataclasses/task.dart' as task_lib; -import 'package:helpwave_proto_dart/proto/services/task_svc/v1/task_svc.pbenum.dart' as proto; - -Map taskStatusMappingFromProto = { - proto.TaskStatus.TASK_STATUS_TODO: task_lib.TaskStatus.todo, - proto.TaskStatus.TASK_STATUS_IN_PROGRESS: task_lib.TaskStatus.inProgress, - proto.TaskStatus.TASK_STATUS_DONE: task_lib.TaskStatus.done, - proto.TaskStatus.TASK_STATUS_UNSPECIFIED: task_lib.TaskStatus.unspecified, -}; - -Map taskStatusMappingToProto = { - task_lib.TaskStatus.todo: proto.TaskStatus.TASK_STATUS_TODO, - task_lib.TaskStatus.inProgress: proto.TaskStatus.TASK_STATUS_IN_PROGRESS, - task_lib.TaskStatus.done: proto.TaskStatus.TASK_STATUS_DONE, - task_lib.TaskStatus.unspecified: proto.TaskStatus.TASK_STATUS_UNSPECIFIED, -}; diff --git a/apps/tasks/pubspec.lock b/apps/tasks/pubspec.lock index fa0366a5..bbc15e5d 100644 --- a/apps/tasks/pubspec.lock +++ b/apps/tasks/pubspec.lock @@ -254,7 +254,7 @@ packages: source: hosted version: "1.4.1" grpc: - dependency: "direct main" + dependency: transitive description: name: grpc sha256: e93ee3bce45c134bf44e9728119102358c7cd69de7832d9a874e2e74eb8cab40 @@ -269,13 +269,13 @@ packages: source: path version: "0.0.1" helpwave_proto_dart: - dependency: "direct main" + dependency: transitive description: name: helpwave_proto_dart - sha256: "60e912fcb781e16b9b5bd6b5c27585d9481ae554be2bfc2e58c76dcfa4735d60" + sha256: "4d4b2b6d0129c7c1b678164688e2bd0a99029cc178794fd655e7c4e7aa505d58" url: "https://pub.dev" source: hosted - version: "0.39.0-aa8fd45" + version: "0.46.0-336429a" helpwave_service: dependency: "direct main" description: @@ -291,7 +291,7 @@ packages: source: path version: "0.0.1" helpwave_util: - dependency: "direct overridden" + dependency: "direct main" description: path: "../../packages/helpwave_util" relative: true @@ -376,30 +376,6 @@ packages: url: "https://pub.dev" source: hosted version: "4.8.1" - leak_tracker: - dependency: transitive - description: - name: leak_tracker - sha256: "78eb209deea09858f5269f5a5b02be4049535f568c07b275096836f01ea323fa" - url: "https://pub.dev" - source: hosted - version: "10.0.0" - leak_tracker_flutter_testing: - dependency: transitive - description: - name: leak_tracker_flutter_testing - sha256: b46c5e37c19120a8a01918cfaf293547f47269f7cb4b0058f21531c2465d6ef0 - url: "https://pub.dev" - source: hosted - version: "2.0.1" - leak_tracker_testing: - dependency: transitive - description: - name: leak_tracker_testing - sha256: a597f72a664dbd293f3bfc51f9ba69816f84dcd403cdac7066cb3f6003f3ab47 - url: "https://pub.dev" - source: hosted - version: "2.0.1" lints: dependency: transitive description: @@ -428,26 +404,26 @@ packages: dependency: transitive description: name: matcher - sha256: d2323aa2060500f906aa31a895b4030b6da3ebdcc5619d14ce1aada65cd161cb + sha256: "1803e76e6653768d64ed8ff2e1e67bea3ad4b923eb5c56a295c3e634bad5960e" url: "https://pub.dev" source: hosted - version: "0.12.16+1" + version: "0.12.16" material_color_utilities: dependency: transitive description: name: material_color_utilities - sha256: "0e0a020085b65b6083975e499759762399b4475f766c21668c4ecca34ea74e5a" + sha256: "9528f2f296073ff54cb9fee677df673ace1218163c3bc7628093e7eed5203d41" url: "https://pub.dev" source: hosted - version: "0.8.0" + version: "0.5.0" meta: dependency: transitive description: name: meta - sha256: d584fa6707a52763a52446f02cc621b077888fb63b93bbcb1143a7be5a0c0c04 + sha256: a6e590c838b18133bb482a2745ad77c5bb7715fb0451209e1a7567d416678b8e url: "https://pub.dev" source: hosted - version: "1.11.0" + version: "1.10.0" nested: dependency: transitive description: @@ -468,10 +444,10 @@ packages: dependency: transitive description: name: path - sha256: "087ce49c3f0dc39180befefc60fdb4acd8f8620e5682fe2476afd0b3688bb4af" + sha256: "8829d8a55c13fc0e37127c29fedf290c102f4e40ae94ada574091fe0ff96c917" url: "https://pub.dev" source: hosted - version: "1.9.0" + version: "1.8.3" path_provider: dependency: transitive description: @@ -773,14 +749,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.1.4" - vm_service: + web: dependency: transitive description: - name: vm_service - sha256: b3d56ff4341b8f182b96aceb2fa20e3dcb336b9f867bc0eafc0de10f1048e957 + name: web + sha256: afe077240a270dcfd2aafe77602b4113645af95d0ad31128cc02bce5ac5d5152 url: "https://pub.dev" source: hosted - version: "13.0.0" + version: "0.3.0" win32: dependency: transitive description: @@ -822,5 +798,5 @@ packages: source: hosted version: "3.1.2" sdks: - dart: ">=3.2.0-0 <4.0.0" + dart: ">=3.2.0-194.0.dev <4.0.0" flutter: ">=3.16.0" diff --git a/apps/tasks/pubspec.yaml b/apps/tasks/pubspec.yaml index 068c883b..032d9e18 100644 --- a/apps/tasks/pubspec.yaml +++ b/apps/tasks/pubspec.yaml @@ -45,8 +45,8 @@ dependencies: path: "../../packages/helpwave_widget" helpwave_service: path: "../../packages/helpwave_service" - helpwave_proto_dart: ^0.39.0-aa8fd45 - grpc: ^3.2.4 + helpwave_util: + path: "../../packages/helpwave_util" shared_preferences: ^2.0.15 logger: ^2.0.2+1 diff --git a/packages/helpwave_localization/lib/l10n/app_de.arb b/packages/helpwave_localization/lib/l10n/app_de.arb index 4490bc54..18a2eeef 100644 --- a/packages/helpwave_localization/lib/l10n/app_de.arb +++ b/packages/helpwave_localization/lib/l10n/app_de.arb @@ -168,5 +168,6 @@ "darkMode": "Dunkel", "lightMode": "Hell", "system": "System", - "newTaskOrPatient": "Neu" + "newTaskOrPatient": "Neu", + "remove": "Entfernen" } diff --git a/packages/helpwave_localization/lib/l10n/app_en.arb b/packages/helpwave_localization/lib/l10n/app_en.arb index c516a9ab..0321789b 100644 --- a/packages/helpwave_localization/lib/l10n/app_en.arb +++ b/packages/helpwave_localization/lib/l10n/app_en.arb @@ -168,5 +168,6 @@ "darkMode": "Dark", "lightMode": "Light", "system": "System", - "newTaskOrPatient": "New" + "newTaskOrPatient": "New", + "remove": "Remove" } diff --git a/packages/helpwave_service/lib/auth.dart b/packages/helpwave_service/lib/auth.dart index cba87fbb..497562fd 100644 --- a/packages/helpwave_service/lib/auth.dart +++ b/packages/helpwave_service/lib/auth.dart @@ -1,4 +1 @@ -export 'package:helpwave_service/src/auth/authentication_service.dart' - show AuthenticationService; - -export 'package:helpwave_service/src/auth/identity.dart'; +export 'package:helpwave_service/src/auth/index.dart'; diff --git a/packages/helpwave_service/lib/src/api/offline/offline_client_store.dart b/packages/helpwave_service/lib/src/api/offline/offline_client_store.dart new file mode 100644 index 00000000..5f62cd5a --- /dev/null +++ b/packages/helpwave_service/lib/src/api/offline/offline_client_store.dart @@ -0,0 +1,159 @@ +import 'package:helpwave_service/src/api/tasks/index.dart'; +import 'package:helpwave_service/src/api/tasks/offline_clients/bed_offline_client.dart'; +import 'package:helpwave_service/src/api/tasks/offline_clients/patient_offline_client.dart'; +import 'package:helpwave_service/src/api/tasks/offline_clients/room_offline_client.dart'; +import 'package:helpwave_service/src/api/tasks/offline_clients/task_offline_client.dart'; +import 'package:helpwave_service/src/api/tasks/offline_clients/template_offline_client.dart'; +import 'package:helpwave_service/src/api/tasks/offline_clients/ward_offline_client.dart'; +import 'package:helpwave_service/src/api/user/offline_clients/organization_offline_client.dart'; +import 'package:helpwave_service/src/api/user/offline_clients/user_offline_client.dart'; +import 'package:helpwave_util/lists.dart'; +import '../../../user.dart'; + +const String profileUrl = "https://helpwave.de/favicon.ico"; + +final List initialOrganizations = [ + Organization( + id: "organization1", + shortName: "MK", + longName: "Musterklinikum", + avatarURL: profileUrl, + email: "test@helpwave.de", + isPersonal: false, + isVerified: true), +]; +final List initialUsers = [ + User( + id: "user1", + name: "Testine Test", + nickName: "Testine", + email: "test@helpwave.de", + profileUrl: Uri.parse(profileUrl), + ), + User( + id: "user2", + name: "Peter Pete", + nickName: "Peter", + email: "test@helpwave.de", + profileUrl: Uri.parse(profileUrl), + ), + User( + id: "user3", + name: "John Doe", + nickName: "John", + email: "test@helpwave.de", + profileUrl: Uri.parse(profileUrl), + ), + User( + id: "user4", + name: "Walter White", + nickName: "Walter", + email: "test@helpwave.de", + profileUrl: Uri.parse(profileUrl), + ), + User( + id: "user5", + name: "Peter Parker", + nickName: "Parker", + email: "test@helpwave.de", + profileUrl: Uri.parse(profileUrl), + ), +]; +final List initialWards = initialOrganizations + .map((organization) => range(0, 3).map((index) => + Ward(id: "${organization.id}${index + 1}", name: "Ward ${index + 1}", organizationId: organization.id))) + .expand((element) => element) + .toList(); +final List initialRooms = initialWards + .map((ward) => range(0, 2) + .map((index) => RoomWithWardId(id: "${ward.id}${index + 1}", name: "Room ${index + 1}", wardId: ward.id))) + .expand((element) => element) + .toList(); +final List initialBeds = initialRooms + .map((room) => range(0, 4) + .map((index) => BedWithRoomId(id: "${room.id}${index + 1}", name: "Bed ${index + 1}", roomId: room.id))) + .expand((element) => element) + .toList(); +final List initialPatients = initialBeds.indexed + .map((e) => PatientWithBedId( + id: "patient${e.$1}", + name: "Patient ${e.$1 + 1}", + notes: "", + isDischarged: e.$1 % 6 == 0, + bedId: e.$1 % 2 == 0 ? e.$2.id : null, + )) + .toList(); +final List initialTasks = initialPatients + .map((patient) => range(0, 3).map((index) => Task( + id: "${patient.id}${index + 1}", + name: "Task ${index + 1}", + patientId: patient.id, + notes: '', + assigneeId: [initialUsers[0].id, null, initialUsers[2].id][index], + ))) + .expand((element) => element) + .toList(); +final List initialTaskSubtasks = initialTasks + .map((task) => range(0, 2).map((index) => Subtask( + id: "${task.id}${index + 1}", + name: "Subtask ${index + 1}", + taskId: task.id, + ))) + .expand((element) => element) + .toList(); +final List initialTaskTemplates = range(0, 5) + .map((index) => TaskTemplate( + id: "template$index", + name: "template${index + 1}", + notes: "", + )) + .toList() + + initialWards + .map((ward) => TaskTemplate( + id: "wardTemplate${ward.id}", + name: "Ward ${ward.name} Template", + notes: "", + wardId: ward.id, + )) + .toList(); +final List initialTaskTemplateSubtasks = initialTasks + .map((task) => range(0, 3).map((index) => Subtask( + id: "${task.id}${index + 1}", + name: "Template Subtask ${index + 1}", + taskId: task.id, + ))) + .expand((element) => element) + .toList(); + +class OfflineClientStore { + static final OfflineClientStore _instance = OfflineClientStore._internal()..reset(); + + OfflineClientStore._internal(); + + factory OfflineClientStore() => _instance; + + final OrganizationOfflineClientStore organizationStore = OrganizationOfflineClientStore(); + final UserOfflineService userStore = UserOfflineService(); + + final WardOfflineService wardStore = WardOfflineService(); + final RoomOfflineService roomStore = RoomOfflineService(); + final BedOfflineService bedStore = BedOfflineService(); + final PatientOfflineService patientStore = PatientOfflineService(); + final TaskOfflineService taskStore = TaskOfflineService(); + final SubtaskOfflineService subtaskStore = SubtaskOfflineService(); + final TaskTemplateOfflineService taskTemplateStore = TaskTemplateOfflineService(); + final TaskTemplateSubtaskOfflineService taskTemplateSubtaskStore = TaskTemplateSubtaskOfflineService(); + + void reset() { + organizationStore.organizations = initialOrganizations; + userStore.users = initialUsers; + wardStore.wards = initialWards; + roomStore.rooms = initialRooms; + bedStore.beds = initialBeds; + patientStore.patients = initialPatients; + taskStore.tasks = initialTasks; + subtaskStore.subtasks = initialTaskSubtasks; + taskTemplateStore.taskTemplates = initialTaskTemplates; + taskTemplateSubtaskStore.taskTemplateSubtasks = initialTaskTemplateSubtasks; + } +} diff --git a/packages/helpwave_service/lib/src/api/offline/util.dart b/packages/helpwave_service/lib/src/api/offline/util.dart new file mode 100644 index 00000000..c518556c --- /dev/null +++ b/packages/helpwave_service/lib/src/api/offline/util.dart @@ -0,0 +1,48 @@ +import 'dart:async'; +import 'package:grpc/grpc.dart'; + +class MockResponseFuture implements ResponseFuture { + final Future future; + + MockResponseFuture.value(T value) : future = Future.value(value); + + MockResponseFuture.error(Object error) : future = Future.error(error); + + MockResponseFuture.future(this.future); + + @override + Stream asStream() { + return future.asStream(); + } + + @override + Future cancel() async { + // Mock Futures cannot be canceled + } + + @override + Future catchError(Function onError, {bool Function(Object error)? test}) { + return future.catchError(onError, test: test); + } + + @override + Future> get headers => Future.value({}); + + @override + Future timeout(Duration timeLimit, {FutureOr Function()? onTimeout}) { + return future.timeout(timeLimit, onTimeout: onTimeout); + } + + @override + Future> get trailers => Future.value({}); + + @override + Future whenComplete(FutureOr Function() action) { + return future.whenComplete(action); + } + + @override + Future then(FutureOr Function(T p1) onValue, {Function? onError}) { + return future.then(onValue, onError: onError); + } +} diff --git a/packages/helpwave_service/lib/src/api/tasks/controllers/index.dart b/packages/helpwave_service/lib/src/api/tasks/controllers/index.dart new file mode 100644 index 00000000..2940b0c6 --- /dev/null +++ b/packages/helpwave_service/lib/src/api/tasks/controllers/index.dart @@ -0,0 +1,5 @@ +export 'patient_controller.dart'; +export 'subtask_list_controller.dart'; +export 'task_controller.dart'; +export 'my_tasks_controller.dart'; +export 'ward_patients_controller.dart'; diff --git a/packages/helpwave_service/lib/src/api/tasks/controllers/my_tasks_controller.dart b/packages/helpwave_service/lib/src/api/tasks/controllers/my_tasks_controller.dart new file mode 100644 index 00000000..70b89070 --- /dev/null +++ b/packages/helpwave_service/lib/src/api/tasks/controllers/my_tasks_controller.dart @@ -0,0 +1,33 @@ +import 'package:helpwave_service/tasks.dart'; +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 = []; + + /// The currently loaded [Task]s + List get tasks => _tasks; + + /// The loaded [Task]s which have [TaskStatus.todo] + 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(); + + /// The loaded [Task]s which have [TaskStatus.done] + List get done => _tasks.where((element) => element.status == TaskStatus.done).toList(); + + AssignedTasksController() { + load(); + } + + /// Loads the [Task]s + Future load() async { + loadTasksFuture() async { + _tasks = await TaskService().getAssignedTasks(); + } + + loadHandler(future: loadTasksFuture()); + } +} diff --git a/packages/helpwave_service/lib/src/api/tasks/controllers/patient_controller.dart b/packages/helpwave_service/lib/src/api/tasks/controllers/patient_controller.dart new file mode 100644 index 00000000..9bfa732e --- /dev/null +++ b/packages/helpwave_service/lib/src/api/tasks/controllers/patient_controller.dart @@ -0,0 +1,129 @@ +import 'package:helpwave_util/loading.dart'; +import 'package:helpwave_service/src/api/tasks/index.dart'; +import 'package:logger/logger.dart'; + +/// The Controller for managing [Patient]s in a Ward +class PatientController extends LoadingChangeNotifier { + /// The current [Patient] + Patient _patient; + + /// 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(this._patient) { + if (!_patient.isCreating) { + load(); + } + } + + /// A function to load the [Patient] + Future load() async { + if (isCreating) { + Logger().w("PatientController.load should not be called when the patient has not been created"); + return; + } + loadPatient() async { + patient = await PatientService().getPatientDetails(patientId: patient.id); + } + + loadHandler( + future: loadPatient(), + ); + } + + /// Unassigns the [Patient] from their [Bed] + Future unassign() async { + unassignPatient() async { + await PatientService().unassignPatient(patientId: patient.id).then((value) { + final patientCopy = patient.copyWith(); + patientCopy.bed = null; + patientCopy.room = null; + patient = patientCopy; + }); + } + + loadHandler(future: unassignPatient()); + } + + /// Discharges the [Patient] + Future discharge() async { + dischargePatient() async { + await PatientService().dischargePatient(patientId: patient.id).then((value) { + final patientCopy = patient.copyWith(isDischarged: true); + patientCopy.bed = null; + patientCopy.room = null; + patient = patientCopy; + }); + } + + loadHandler(future: dischargePatient()); + } + + /// Assigns the [Patient] to a [Bed] and [Room] + Future assignToBed(RoomMinimal room, BedMinimal bed) async { + if (isCreating) { + patient.room = room; + patient.bed = bed; + return; + } + assignPatientToBed() async { + await PatientService().assignBed(patientId: patient.id, bedId: bed.id).then((value) { + patient = patient.copyWith(bed: bed, room: room); + }); + } + + loadHandler(future: assignPatientToBed()); + } + + /// Change the name of the [Patient] + 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()); + } + + /// 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!); + } + } + + loadHandler(future: createPatient()); + } +} diff --git a/packages/helpwave_service/lib/src/api/tasks/controllers/subtask_list_controller.dart b/packages/helpwave_service/lib/src/api/tasks/controllers/subtask_list_controller.dart new file mode 100644 index 00000000..a0b8e98c --- /dev/null +++ b/packages/helpwave_service/lib/src/api/tasks/controllers/subtask_list_controller.dart @@ -0,0 +1,110 @@ +import 'dart:async'; +import 'package:helpwave_service/src/api/tasks/index.dart'; +import 'package:helpwave_util/loading.dart'; + +/// The Controller for managing [Subtask]s in a [Task] +/// +/// Providing a [taskId] means loading and synchronising the [Subtask]s with +/// the backend while no [taskId] or a empty [String] means that the subtasks +/// only used locally +class SubtasksController extends LoadingChangeNotifier { + /// The [Subtask]s + List _subtasks = []; + + List get subtasks => [..._subtasks]; + + set subtasks(List value) { + _subtasks = value; + notifyListeners(); + } + + bool get isCreating => taskId == null || taskId!.isEmpty; + + String? taskId; + + SubtasksController({this.taskId = "", List? subtasks}) { + if (!isCreating) { + load(); + } + } + + /// Loads a [Task] + Future load() async { + if (isCreating) { + return; + } + loadTask() async { + final task = await TaskService().getTask(id: taskId); + subtasks = task.subtasks; + } + + loadHandler(future: loadTask()); + } + + /// Delete the [Subtask] by its index int the list + Future deleteByIndex(int index) async { + if (index < 0 || index >= subtasks.length) { + return; + } + if (isCreating) { + _subtasks.removeAt(index); + notifyListeners(); + return; + } + await deleteById(subtasks[index].id); + } + + /// Delete the [Subtask] by the id + Future deleteById(String id) async { + assert(!isCreating, "deleteById should not be used when creating a completely new Subtask list"); + deleteSubtask() async { + await TaskService().deleteSubTask(subtaskId: id, taskId: taskId!).then((value) { + if (value) { + int index = _subtasks.indexWhere((element) => element.id == id); + if (index != -1) { + _subtasks.removeAt(index); + } + } + }); + } + + loadHandler(future: deleteSubtask()); + } + + /// Add the [Subtask] + Future create(Subtask subTask) async { + if (isCreating) { + _subtasks.add(subTask); + notifyListeners(); + return; + } + createSubtask() async { + await TaskService().createSubTask(taskId: taskId!, subTask: subTask).then((value) { + _subtasks.add(value); + }); + } + + loadHandler(future: createSubtask()); + } + + Future update({required Subtask subtask, int? index}) async { + if (isCreating) { + assert( + index != null && index >= 0 && index < subtasks.length, + "When creating a subtask list and updating a subtask, a index for the subtask must be provided", + ); + subtasks[index!] = subtask; + return; + } + updateSubtask() async { + assert(!subtask.isCreating, "To update a subtask on the server the subtask must have an id"); + await TaskService().updateSubtask(subtask: subtask, taskId: taskId!); + int index = subtasks.indexWhere((element) => element.id == subtask.id); + if (index != -1) { + subtasks[index] = subtask; + } + } + + loadHandler(future: updateSubtask()); + } +} diff --git a/packages/helpwave_service/lib/src/api/tasks/controllers/task_controller.dart b/packages/helpwave_service/lib/src/api/tasks/controllers/task_controller.dart new file mode 100644 index 00000000..2eae7924 --- /dev/null +++ b/packages/helpwave_service/lib/src/api/tasks/controllers/task_controller.dart @@ -0,0 +1,149 @@ +import 'package:helpwave_util/loading.dart'; +import 'package:helpwave_service/src/api/tasks/index.dart'; + +import '../../../../user.dart'; + +/// The Controller for managing a [TaskWithPatient] +class TaskController extends LoadingChangeNotifier { + /// The current [Task] + TaskWithPatient _task; + + TaskController(this._task) { + if (!_task.isCreating) { + 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; + + /// 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 { + await TaskService().getTask(id: task.id).then((value) async { + task = value; + if (task.hasAssignee) { + UserService().getUser(id: task.assigneeId!).then((value) => _assignee = value); + } + }); + } + + loadHandler(future: loadTask()); + } + + /// Changes the assigned [User] + Future changeAssignee(User? user) async { + if (isCreating) { + task.assigneeId = user?.id; + _assignee = user; + notifyListeners(); + return; + } + changeAssigneeFuture() async { + await TaskService().changeAssignee(taskId: task.id, userId: user?.id).then((value) { + task.assigneeId = user?.id; + _assignee = user; + }); + } + + await loadHandler(future: changeAssigneeFuture()); + } + + Future changeName(String name) async { + if (isCreating) { + task = TaskWithPatient.fromTaskAndPatient(task: task.copyWith(name: name), patient: task.patient); + notifyListeners(); + return; + } + updateName() async { + await TaskService().updateTask(taskId: task.id, name: name).then( + (_) => task = TaskWithPatient.fromTaskAndPatient(task: task.copyWith(name: name), patient: task.patient)); + } + + loadHandler(future: updateName()); + } + + Future changeIsPublic(bool isPublic) async { + 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()); + } + + 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)); + } + + loadHandler(future: dueDate == null ? removeDueDate() : updateDueDate()); + } + + /// 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); + }); + } + + return loadHandler(future: createTask()); + } +} diff --git a/apps/tasks/lib/controllers/ward_patients_controller.dart b/packages/helpwave_service/lib/src/api/tasks/controllers/ward_patients_controller.dart similarity index 66% rename from apps/tasks/lib/controllers/ward_patients_controller.dart rename to packages/helpwave_service/lib/src/api/tasks/controllers/ward_patients_controller.dart index e0edea42..c1a29d76 100644 --- a/apps/tasks/lib/controllers/ward_patients_controller.dart +++ b/packages/helpwave_service/lib/src/api/tasks/controllers/ward_patients_controller.dart @@ -1,15 +1,9 @@ -import 'package:flutter/cupertino.dart'; -import 'package:helpwave_widget/loading.dart'; -import 'package:tasks/dataclasses/patient.dart'; -import 'package:tasks/services/current_ward_svc.dart'; -import 'package:tasks/services/patient_svc.dart'; -import 'package:tasks/util/search_helpers.dart'; - -/// The Controller for managing [Patient]s in a Ward -class WardPatientsController extends ChangeNotifier { - /// The [LoadingState] of the Controller - LoadingState state = LoadingState.initializing; +import 'package:helpwave_service/src/api/tasks/index.dart'; +import 'package:helpwave_util/loading.dart'; +import 'package:helpwave_util/search.dart'; +/// The Controller for managing [Patient]s in a [Ward] +class WardPatientsController extends LoadingChangeNotifier { /// The [Patient]s mapped by the [PatientsByAssignmentStatus] PatientsByAssignmentStatus _patientsByAssignmentStatus = PatientsByAssignmentStatus(); @@ -72,23 +66,20 @@ class WardPatientsController extends ChangeNotifier { return results; } - /// Loads the [patients] + /// Loads the [Patient]s Future load() async { - state = LoadingState.loading; - notifyListeners(); - - _patientsByAssignmentStatus = - await PatientService().getPatientList(wardId: CurrentWardService().currentWard?.wardId); - state = LoadingState.loaded; - notifyListeners(); + loadPatients() async { + _patientsByAssignmentStatus = await PatientService().getPatientList(); + } + loadHandler(future: loadPatients()); } /// Discharges the patient the [patients] Future discharge(String patientId) async { - state = LoadingState.loading; - notifyListeners(); - await PatientService().dischargePatient(patientId: patientId); - // Here we can maybe use optimistic updates - load(); + dischargePatient() async { + await PatientService().dischargePatient(patientId: patientId); + await load(); + } + loadHandler(future: dischargePatient()); } } diff --git a/apps/tasks/lib/dataclasses/bed.dart b/packages/helpwave_service/lib/src/api/tasks/data_types/bed.dart similarity index 61% rename from apps/tasks/lib/dataclasses/bed.dart rename to packages/helpwave_service/lib/src/api/tasks/data_types/bed.dart index df7d3a89..2bf7f84e 100644 --- a/apps/tasks/lib/dataclasses/bed.dart +++ b/packages/helpwave_service/lib/src/api/tasks/data_types/bed.dart @@ -1,4 +1,4 @@ -import 'package:tasks/dataclasses/patient.dart'; +import 'package:helpwave_service/src/api/tasks/index.dart'; /// data class for [Bed] class BedMinimal { @@ -11,6 +11,12 @@ class BedMinimal { }); } +class BedWithRoomId extends BedMinimal { + String roomId; + + BedWithRoomId({required super.id, required super.name, required this.roomId}); +} + class BedWithMinimalPatient extends BedMinimal{ PatientMinimal? patient; diff --git a/packages/helpwave_service/lib/src/api/tasks/data_types/index.dart b/packages/helpwave_service/lib/src/api/tasks/data_types/index.dart new file mode 100644 index 00000000..7b1fe9cc --- /dev/null +++ b/packages/helpwave_service/lib/src/api/tasks/data_types/index.dart @@ -0,0 +1,8 @@ +export 'ward.dart'; +export 'room.dart'; +export 'bed.dart'; +export 'patient.dart'; +export 'task.dart'; +export 'subtask.dart'; +export 'task_template.dart'; +export 'task_template_subtask.dart'; diff --git a/apps/tasks/lib/dataclasses/patient.dart b/packages/helpwave_service/lib/src/api/tasks/data_types/patient.dart similarity index 62% rename from apps/tasks/lib/dataclasses/patient.dart rename to packages/helpwave_service/lib/src/api/tasks/data_types/patient.dart index 31baa719..9a4adc94 100644 --- a/apps/tasks/lib/dataclasses/patient.dart +++ b/packages/helpwave_service/lib/src/api/tasks/data_types/patient.dart @@ -1,6 +1,4 @@ -import 'package:tasks/dataclasses/bed.dart'; -import 'package:tasks/dataclasses/room.dart'; -import 'package:tasks/dataclasses/task.dart'; +import 'package:helpwave_service/src/api/tasks/index.dart'; enum PatientAssignmentStatus { active, unassigned, discharged, all } @@ -35,13 +33,45 @@ class PatientMinimal { @override String toString() { - if(isCreating){ + if (isCreating) { return "PatientMinimal"; } return "PatientMinimal<$id, $name>"; } } +class PatientWithBedId extends PatientMinimal { + String? bedId; + bool isDischarged; + String notes; + + PatientWithBedId({ + required super.id, + required super.name, + required this.isDischarged, + required this.notes, + this.bedId, + }); + + PatientWithBedId copyWith({ + String? id, + String? name, + String? bedId, + bool? isDischarged, + String? notes, + }) { + return PatientWithBedId( + id: id ?? this.id, + name: name ?? this.name, + isDischarged: isDischarged ?? this.isDischarged, + notes: notes ?? this.notes, + bedId: bedId ?? this.bedId, + ); + } + + bool get hasBed => bedId != null; +} + /// data class for [Patient] with TaskCount class Patient extends PatientMinimal { RoomMinimal? room; @@ -50,14 +80,13 @@ class Patient extends PatientMinimal { List tasks; bool isDischarged; - get isUnassigned => bed == null && room == null; + get isNotAssignedToBed => bed == null && room == null; get isActive => bed != null && room != null; 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(); @@ -80,6 +109,26 @@ class Patient extends PatientMinimal { this.room, this.bed, }); + + Patient copyWith({ + String? id, + String? name, + List? tasks, + String? notes, + bool? isDischarged, + RoomMinimal? room, + BedMinimal? bed, + }) { + return Patient( + id: id ?? this.id, + name: name ?? this.name, + tasks: tasks ?? this.tasks, + notes: notes ?? this.notes, + isDischarged: isDischarged ?? this.isDischarged, + room: room ?? this.room, + bed: bed ?? this.bed, + ); + } } /// A data class which maps all [PatientAssignmentStatus]es to a [List] of [Patient]s @@ -87,13 +136,12 @@ class PatientsByAssignmentStatus { List active; List unassigned; List discharged; - List all; + List get all => active + unassigned + discharged; PatientsByAssignmentStatus({ this.active = const [], this.unassigned = const [], this.discharged = const [], - this.all = const [], }); byAssignmentStatus(PatientAssignmentStatus status) { diff --git a/apps/tasks/lib/dataclasses/room.dart b/packages/helpwave_service/lib/src/api/tasks/data_types/room.dart similarity index 84% rename from apps/tasks/lib/dataclasses/room.dart rename to packages/helpwave_service/lib/src/api/tasks/data_types/room.dart index 06775428..8e943ea0 100644 --- a/apps/tasks/lib/dataclasses/room.dart +++ b/packages/helpwave_service/lib/src/api/tasks/data_types/room.dart @@ -1,4 +1,4 @@ -import 'package:tasks/dataclasses/bed.dart'; +import 'package:helpwave_service/src/api/tasks/data_types/bed.dart'; /// data class for [Room] class RoomMinimal { @@ -11,6 +11,12 @@ class RoomMinimal { }); } +class RoomWithWardId extends RoomMinimal { + String wardId; + + RoomWithWardId({required super.id, required super.name, required this.wardId}); +} + class RoomWithBeds { String id; String name; diff --git a/packages/helpwave_service/lib/src/api/tasks/data_types/subtask.dart b/packages/helpwave_service/lib/src/api/tasks/data_types/subtask.dart new file mode 100644 index 00000000..956d235c --- /dev/null +++ b/packages/helpwave_service/lib/src/api/tasks/data_types/subtask.dart @@ -0,0 +1,26 @@ +/// Data class for a [Subtask] +class Subtask { + final String id; + final String taskId; + String name; + bool isDone; + + bool get isCreating => id == ""; + + Subtask({required this.id, required this.taskId, required this.name, this.isDone = false}); + + /// Create a copy of the [Subtask] + Subtask copyWith({ + String? id, + String? taskId, + String? name, + bool? isDone, + }) { + return Subtask( + id: id ?? this.id, + taskId: taskId ?? this.taskId, + name: name ?? this.name, + isDone: isDone ?? this.isDone, + ); + } +} diff --git a/packages/helpwave_service/lib/src/api/tasks/data_types/task.dart b/packages/helpwave_service/lib/src/api/tasks/data_types/task.dart new file mode 100644 index 00000000..a8b09b82 --- /dev/null +++ b/packages/helpwave_service/lib/src/api/tasks/data_types/task.dart @@ -0,0 +1,139 @@ +import 'package:helpwave_service/src/api/tasks/data_types/patient.dart'; +import 'package:helpwave_service/src/api/tasks/data_types/subtask.dart'; + +enum TaskStatus { + unspecified, + todo, + inProgress, + done, +} + +/// data class for [Task] +class Task { + final String id; + String name; + String? assigneeId; + String notes; + TaskStatus status; + List subtasks; + DateTime? dueDate; + final DateTime? creationDate; + final String? createdBy; + bool isPublicVisible; + final String patientId; + + factory Task.empty(String patientId) => Task(id: "", name: "name", notes: "", patientId: patientId); + + final _nullID = "00000000-0000-0000-0000-000000000000"; + + double get progress => subtasks.isNotEmpty ? subtasks.where((element) => element.isDone).length / subtasks.length : 1; + + /// 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; + + bool get isOverdue => remainingTime.isNegative; + + bool get inNextTwoDays => remainingTime.inDays < 2; + + bool get inNextHour => remainingTime.inHours < 1; + + bool get isCreating => id == ""; + + bool get hasAssignee => assigneeId != null && assigneeId != "" && assigneeId != _nullID; + + Task({ + required this.id, + required this.name, + required this.notes, + this.assigneeId, + this.status = TaskStatus.todo, + this.subtasks = const [], + this.dueDate, + this.creationDate, + this.createdBy, + this.isPublicVisible = false, + required this.patientId, + }); + + Task copyWith({ + String? id, + String? name, + String? assigneeId, + String? notes, + TaskStatus? status, + List? subtasks, + DateTime? dueDate, + DateTime? creationDate, + String? createdBy, + bool? isPublicVisible, + String? patientId, + }) { + return Task( + id: id ?? this.id, + name: name ?? this.name, + assigneeId: assigneeId ?? this.assigneeId, + notes: notes ?? this.notes, + status: status ?? this.status, + subtasks: subtasks ?? this.subtasks, + dueDate: dueDate ?? this.dueDate, + creationDate: creationDate ?? this.creationDate, + createdBy: createdBy ?? this.createdBy, + isPublicVisible: isPublicVisible ?? this.isPublicVisible, + patientId: patientId ?? this.patientId, + ); + } +} + +class TaskWithPatient extends Task { + final PatientMinimal patient; + + factory TaskWithPatient.empty({ + String taskId = "", + PatientMinimal? patient, + }) { + return TaskWithPatient( + id: taskId, + name: "task name", + notes: "", + patient: patient ?? PatientMinimal.empty(), + patientId: patient?.id ?? "", + ); + } + + 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 ?? "", + ); + } + + 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); +} diff --git a/packages/helpwave_service/lib/src/api/tasks/data_types/task_template.dart b/packages/helpwave_service/lib/src/api/tasks/data_types/task_template.dart new file mode 100644 index 00000000..735c1993 --- /dev/null +++ b/packages/helpwave_service/lib/src/api/tasks/data_types/task_template.dart @@ -0,0 +1,44 @@ +import '../index.dart'; + +/// data class for [TaskTemplate] +class TaskTemplate { + String id; + String? wardId; + String name; + String notes; + List subtasks; + bool isPublicVisible; + String? createdBy; + + get isWardTemplate => wardId != null; + + TaskTemplate({ + required this.id, + this.wardId, + required this.name, + required this.notes, + this.subtasks = const [], + this.isPublicVisible = false, + this.createdBy + }); + + TaskTemplate copyWith({ + String? id, + String? wardId, + String? name, + String? notes, + List? subtasks, + bool? isPublicVisible, + String? createdBy, + }) { + return TaskTemplate( + id: id ?? this.id, + wardId: wardId ?? this.wardId, + name: name ?? this.name, + notes: notes ?? this.notes, + subtasks: subtasks ?? this.subtasks, + isPublicVisible: isPublicVisible ?? this.isPublicVisible, + createdBy: createdBy ?? this.createdBy, + ); + } +} diff --git a/apps/tasks/lib/dataclasses/task_template_subtask.dart b/packages/helpwave_service/lib/src/api/tasks/data_types/task_template_subtask.dart similarity index 100% rename from apps/tasks/lib/dataclasses/task_template_subtask.dart rename to packages/helpwave_service/lib/src/api/tasks/data_types/task_template_subtask.dart diff --git a/apps/tasks/lib/dataclasses/ward.dart b/packages/helpwave_service/lib/src/api/tasks/data_types/ward.dart similarity index 100% rename from apps/tasks/lib/dataclasses/ward.dart rename to packages/helpwave_service/lib/src/api/tasks/data_types/ward.dart diff --git a/packages/helpwave_service/lib/src/api/tasks/index.dart b/packages/helpwave_service/lib/src/api/tasks/index.dart new file mode 100644 index 00000000..44b25815 --- /dev/null +++ b/packages/helpwave_service/lib/src/api/tasks/index.dart @@ -0,0 +1,4 @@ +export 'data_types/index.dart'; +export 'services/index.dart'; +export 'controllers/index.dart'; +export 'tasks_api_service_clients.dart'; diff --git a/packages/helpwave_service/lib/src/api/tasks/offline_clients/bed_offline_client.dart b/packages/helpwave_service/lib/src/api/tasks/offline_clients/bed_offline_client.dart new file mode 100644 index 00000000..cadcb891 --- /dev/null +++ b/packages/helpwave_service/lib/src/api/tasks/offline_clients/bed_offline_client.dart @@ -0,0 +1,181 @@ +import 'package:grpc/grpc.dart'; +import 'package:helpwave_proto_dart/services/tasks_svc/v1/bed_svc.pbgrpc.dart'; +import 'package:helpwave_service/src/api/offline/offline_client_store.dart'; +import 'package:helpwave_service/src/api/offline/util.dart'; +import 'package:helpwave_service/src/api/tasks/data_types/bed.dart'; +import 'package:helpwave_util/lists.dart'; + +class BedUpdate { + String id; + String? name; + + BedUpdate({required this.id, required this.name}); +} + +class BedOfflineService { + List beds = []; + + BedWithRoomId? findBed(String id) { + int index = OfflineClientStore().bedStore.beds.indexWhere((value) => value.id == id); + if (index == -1) { + return null; + } + return beds[index]; + } + + List findBeds([String? roomId]) { + final valueStore = OfflineClientStore().bedStore; + if (roomId == null) { + return valueStore.beds; + } + return valueStore.beds.where((value) => value.roomId == roomId).toList(); + } + + void create(BedWithRoomId bed) { + OfflineClientStore().bedStore.beds.add(bed); + } + + void update(BedUpdate bed) { + final valueStore = OfflineClientStore().bedStore; + bool found = false; + + valueStore.beds = valueStore.beds.map((value) { + if (value.id == bed.id) { + found = true; + return BedWithRoomId(id: bed.id, name: bed.name ?? value.name, roomId: value.roomId); + } + return value; + }).toList(); + + if (!found) { + throw Exception('UpdateBed: Could not find bed with id ${bed.id}'); + } + } + + void delete(String bedId) { + final valueStore = OfflineClientStore().bedStore; + valueStore.beds = valueStore.beds.where((value) => value.id != bedId).toList(); + final patient = OfflineClientStore().patientStore.findPatientByBed(bedId); + if(patient != null){ + OfflineClientStore().patientStore.unassignBed(patient.id); + } + } +} + +class BedServicePromiseClient extends BedServiceClient { + BedServicePromiseClient(super.channel); + + @override + ResponseFuture getBed(GetBedRequest request, {CallOptions? options}) { + final bed = OfflineClientStore().bedStore.findBed(request.id); + + if (bed == null) { + throw "Bed with bed id ${request.id} not found"; + } + + final response = GetBedResponse() + ..id = bed.id + ..name = bed.name + ..roomId = bed.roomId; + + return MockResponseFuture.value(response); + } + + @override + ResponseFuture getBeds(GetBedsRequest request, {CallOptions? options}) { + final beds = OfflineClientStore().bedStore.findBeds(); + final bedsList = beds.map((bed) => GetBedsResponse_Bed(id: bed.id, name: bed.name, roomId: bed.roomId)).toList(); + + final response = GetBedsResponse(beds: bedsList); + + return MockResponseFuture.value(response); + } + + @override + ResponseFuture getBedByPatient(GetBedByPatientRequest request, {CallOptions? options}) { + final patient = OfflineClientStore().patientStore.findPatient(request.patientId); + if (patient == null) { + throw "Patient with id ${request.patientId} not found"; + } + if (!patient.hasBed) { + throw "Patient with id ${request.patientId} has no bed"; + } + final bed = OfflineClientStore().bedStore.findBed(patient.bedId!); + if (bed == null) { + throw "Inconsistent Data: Bed with id ${patient.bedId} not found"; + } + final room = OfflineClientStore().roomStore.findRoom(bed.roomId); + if (room == null) { + throw "Inconsistent Data: Room with id ${bed.roomId} not found"; + } + final response = GetBedByPatientResponse( + bed: GetBedByPatientResponse_Bed(id: bed.id, name: bed.name), + room: GetBedByPatientResponse_Room(id: room.id, name: room.name, wardId: room.wardId), + ); + return MockResponseFuture.value(response); + } + + @override + ResponseFuture getBedsByRoom(GetBedsByRoomRequest request, {CallOptions? options}) { + final beds = OfflineClientStore().bedStore.findBeds(request.roomId); + final response = + GetBedsByRoomResponse(beds: beds.map((bed) => GetBedsByRoomResponse_Bed(id: bed.id, name: bed.name))); + return MockResponseFuture.value(response); + } + + @override + ResponseFuture createBed(CreateBedRequest request, {CallOptions? options}) { + final newBed = BedWithRoomId( + id: DateTime.now().millisecondsSinceEpoch.toString(), + name: request.name, + roomId: request.roomId, + ); + + OfflineClientStore().bedStore.create(newBed); + + final response = CreateBedResponse()..id = newBed.id; + + return MockResponseFuture.value(response); + } + + @override + ResponseFuture bulkCreateBeds(BulkCreateBedsRequest request, {CallOptions? options}) { + final beds = range(0, request.amountOfBeds) + .map((index) => BedWithRoomId( + id: DateTime.now().millisecondsSinceEpoch.toString(), + name: "New Bed ${index + 1}", + roomId: request.roomId, + )) + .toList(); + + for (var bed in beds) { + OfflineClientStore().bedStore.create(bed); + } + + final response = + BulkCreateBedsResponse(beds: beds.map((bed) => BulkCreateBedsResponse_Bed(id: bed.id, name: bed.name))); + + return MockResponseFuture.value(response); + } + + @override + ResponseFuture updateBed(UpdateBedRequest request, {CallOptions? options}) { + final update = BedUpdate( + id: request.id, + name: request.name, + ); + + OfflineClientStore().bedStore.update(update); + + final response = UpdateBedResponse(); + return MockResponseFuture.value(response); + } + + @override + ResponseFuture deleteBed(DeleteBedRequest request, {CallOptions? options}) { + OfflineClientStore().bedStore.delete(request.id); + + final response = DeleteBedResponse(); + return MockResponseFuture.value(response); + } +} diff --git a/packages/helpwave_service/lib/src/api/tasks/offline_clients/patient_offline_client.dart b/packages/helpwave_service/lib/src/api/tasks/offline_clients/patient_offline_client.dart new file mode 100644 index 00000000..97e62b46 --- /dev/null +++ b/packages/helpwave_service/lib/src/api/tasks/offline_clients/patient_offline_client.dart @@ -0,0 +1,402 @@ +import 'package:grpc/grpc.dart'; +import 'package:helpwave_proto_dart/services/tasks_svc/v1/patient_svc.pbgrpc.dart'; +import 'package:helpwave_service/src/api/offline/offline_client_store.dart'; +import 'package:helpwave_service/src/api/offline/util.dart'; +import 'package:helpwave_service/src/api/tasks/data_types/patient.dart'; +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}); +} + +class PatientOfflineService { + List patients = []; + + PatientWithBedId? findPatient(String id) { + int index = OfflineClientStore().patientStore.patients.indexWhere((value) => value.id == id); + if (index == -1) { + return null; + } + return patients[index]; + } + + PatientWithBedId? findPatientByBed(String bedId) { + final valueStore = OfflineClientStore().patientStore; + return valueStore.patients.where((value) => value.bedId == bedId).firstOrNull; + } + + void create(PatientWithBedId patient) { + OfflineClientStore().patientStore.patients.add(patient); + } + + void update(PatientUpdate patientUpdate) { + final valueStore = OfflineClientStore().patientStore; + bool found = false; + + valueStore.patients = valueStore.patients.map((value) { + if (value.id == patientUpdate.id) { + found = true; + return value.copyWith( + notes: patientUpdate.notes, + name: patientUpdate.name, + isDischarged: patientUpdate.isDischarged, + ); + } + return value; + }).toList(); + + if (!found) { + throw Exception('UpdatePatient: Could not find patient with id ${patientUpdate.id}'); + } + } + + void assignBed(String patientId, String bedId) { + final valueStore = OfflineClientStore().patientStore; + final bed = OfflineClientStore().bedStore.findBed(bedId); + if (bed != null) { + throw Exception('Could not find bed with id $bedId'); + } + bool found = false; + + valueStore.patients = valueStore.patients.map((value) { + if (value.id == patientId) { + found = true; + return value.copyWith(bedId: bedId); + } + return value; + }).toList(); + + if (!found) { + throw Exception('Could not find patient with id $patientId'); + } + } + + void unassignBed(String patientId) { + final valueStore = OfflineClientStore().patientStore; + bool found = false; + + valueStore.patients = valueStore.patients.map((value) { + if (value.id == patientId) { + found = true; + final copy = value.copyWith(); + copy.bedId = null; + return copy; + } + return value; + }).toList(); + + if (!found) { + throw Exception('Could not find patient with id $patientId'); + } + } + + void delete(String patientId) { + final valueStore = OfflineClientStore().patientStore; + valueStore.patients = valueStore.patients.where((value) => value.id != patientId).toList(); + final tasks = OfflineClientStore().taskStore.findTasks(patientId); + for (var task in tasks) { + OfflineClientStore().taskStore.delete(task.id); + } + } +} + +class PatientOfflineClient extends PatientServiceClient { + PatientOfflineClient(super.channel); + + @override + ResponseFuture getPatient(GetPatientRequest request, {CallOptions? options}) { + final patient = OfflineClientStore().patientStore.findPatient(request.id); + + if (patient == null) { + throw "Patient with patient id ${request.id} not found"; + } + + final response = GetPatientResponse(id: patient.id, humanReadableIdentifier: patient.name, notes: patient.notes); + + if (patient.bedId == null) { + return MockResponseFuture.value(response); + } + final bed = OfflineClientStore().bedStore.findBed(patient.bedId!); + if (bed == null) { + return MockResponseFuture.value(response); + } + final room = OfflineClientStore().roomStore.findRoom(bed.roomId); + if (room == null) { + return MockResponseFuture.value(response); + } + response.bedId = patient.bedId!; + response.bed = GetPatientResponse_Bed(id: bed.id, name: bed.name); + response.room = GetPatientResponse_Room(id: room.id, name: room.name); + return MockResponseFuture.value(response); + } + + @override + ResponseFuture getPatientDetails(GetPatientDetailsRequest request, + {CallOptions? options}) { + final patient = OfflineClientStore().patientStore.findPatient(request.id); + + if (patient == null) { + throw "Patient with patient id ${request.id} not found"; + } + + final response = GetPatientDetailsResponse( + id: patient.id, + humanReadableIdentifier: patient.name, + notes: patient.notes, + isDischarged: patient.isDischarged, + tasks: OfflineClientStore().taskStore.findTasks(patient.id).map((task) => GetPatientDetailsResponse_Task( + id: task.id, + name: task.name, + patientId: patient.id, + assignedUserId: task.assigneeId, + description: task.notes, + public: task.isPublicVisible, + status: GRPCTypeConverter.taskStatusToGRPC(task.status), + subtasks: OfflineClientStore() + .subtaskStore + .findSubtasks(task.id) + .map((subtask) => GetPatientDetailsResponse_Task_SubTask( + id: subtask.id, + name: subtask.name, + done: subtask.isDone, + )), + )), + ); + + if (patient.bedId == null) { + return MockResponseFuture.value(response); + } + final bed = OfflineClientStore().bedStore.findBed(patient.bedId!); + if (bed == null) { + return MockResponseFuture.value(response); + } + final room = OfflineClientStore().roomStore.findRoom(bed.roomId); + if (room == null) { + return MockResponseFuture.value(response); + } + response.bed = GetPatientDetailsResponse_Bed(id: bed.id, name: bed.name); + response.room = GetPatientDetailsResponse_Room(id: room.id, name: room.name); + return MockResponseFuture.value(response); + } + + @override + ResponseFuture getPatientByBed(GetPatientByBedRequest request, {CallOptions? options}) { + final patient = OfflineClientStore().patientStore.findPatientByBed(request.bedId); + + if (patient == null) { + throw "Patient with bed id ${request.bedId} not found"; + } + + final response = GetPatientByBedResponse( + id: patient.id, + humanReadableIdentifier: patient.name, + notes: patient.notes, + bedId: patient.bedId, + ); + return MockResponseFuture.value(response); + } + + @override + ResponseFuture getPatientList(GetPatientListRequest request, {CallOptions? options}) { + mapping(patient) { + final res = GetPatientListResponse_Patient( + id: patient.id, + notes: patient.notes, + humanReadableIdentifier: patient.name, + tasks: OfflineClientStore().taskStore.findTasks(patient.id).map((task) => GetPatientListResponse_Task( + id: task.id, + name: task.name, + patientId: patient.id, + assignedUserId: task.assigneeId, + description: task.notes, + public: task.isPublicVisible, + status: GRPCTypeConverter.taskStatusToGRPC(task.status), + subtasks: OfflineClientStore() + .subtaskStore + .findSubtasks(task.id) + .map((subtask) => GetPatientListResponse_Task_SubTask( + id: subtask.id, + name: subtask.name, + done: subtask.isDone, + )), + )), + ); + if (patient.bedId == null) { + return res; + } + final bed = OfflineClientStore().bedStore.findBed(patient.bedId!); + if (bed == null) { + return res; + } + final room = OfflineClientStore().roomStore.findRoom(bed.roomId); + if (room == null) { + return res; + } + res.bed = GetPatientListResponse_Bed(id: bed.id, name: bed.name); + res.room = GetPatientListResponse_Room(id: room.id, name: room.name); + return res; + } + + final patients = OfflineClientStore().patientStore.patients; + final active = patients.where((element) => element.hasBed).map(mapping); + final discharged = patients.where((element) => element.isDischarged).map(mapping); + final unassigned = patients.where((element) => !element.isDischarged && element.bedId == null).map(mapping); + final response = GetPatientListResponse( + active: active, + dischargedPatients: discharged, + unassignedPatients: unassigned, + ); + + return MockResponseFuture.value(response); + } + + @override + ResponseFuture getRecentPatients(GetRecentPatientsRequest request, + {CallOptions? options}) { + final patients = OfflineClientStore().patientStore.patients.where((element) => element.hasBed).map((patient) { + final res = GetRecentPatientsResponse_PatientWithRoomAndBed( + id: patient.id, + humanReadableIdentifier: patient.name, + ); + if (patient.bedId == null) { + return res; + } + final bed = OfflineClientStore().bedStore.findBed(patient.bedId!); + if (bed == null) { + return res; + } + final room = OfflineClientStore().roomStore.findRoom(bed.roomId); + if (room == null) { + return res; + } + res.bed = GetRecentPatientsResponse_Bed(id: bed.id, name: bed.name); + res.room = GetRecentPatientsResponse_Room(id: room.id, name: room.name); + return res; + }); + final response = GetRecentPatientsResponse(recentPatients: patients); + + return MockResponseFuture.value(response); + } + + @override + ResponseFuture getPatientsByWard(GetPatientsByWardRequest request, + {CallOptions? options}) { + final rooms = OfflineClientStore().roomStore.findRooms(request.wardId); + final beds = rooms.map((room) => OfflineClientStore().bedStore.findBeds(room.id)).expand((element) => element); + List patients = []; + + for (final bed in beds) { + final patient = OfflineClientStore().patientStore.findPatientByBed(bed.id); + if (patient != null) { + patients.add(GetPatientsByWardResponse_Patient( + id: patient.id, notes: patient.notes, humanReadableIdentifier: patient.name, bedId: patient.bedId)); + } + } + final response = GetPatientsByWardResponse(patients: patients); + + return MockResponseFuture.value(response); + } + + @override + ResponseFuture getPatientAssignmentByWard( + GetPatientAssignmentByWardRequest request, + {CallOptions? options}) { + final rooms = + OfflineClientStore().roomStore.findRooms(request.wardId).map((room) => GetPatientAssignmentByWardResponse_Room( + id: room.id, + name: room.name, + beds: OfflineClientStore().bedStore.findBeds(room.id).map((bed) { + final res = GetPatientAssignmentByWardResponse_Room_Bed(id: bed.id, name: bed.name); + final patient = OfflineClientStore().patientStore.findPatientByBed(bed.id); + if (patient != null) { + res.patient = GetPatientAssignmentByWardResponse_Room_Bed_Patient( + id: patient.id, + name: patient.name, + ); + } + return res; + }), + )); + final response = GetPatientAssignmentByWardResponse(rooms: rooms); + + return MockResponseFuture.value(response); + } + + @override + ResponseFuture createPatient(CreatePatientRequest request, {CallOptions? options}) { + final newPatient = PatientWithBedId( + id: DateTime.now().millisecondsSinceEpoch.toString(), + name: request.humanReadableIdentifier, + notes: request.notes, + isDischarged: false, + ); + + OfflineClientStore().patientStore.create(newPatient); + + final response = CreatePatientResponse()..id = newPatient.id; + + return MockResponseFuture.value(response); + } + + @override + ResponseFuture updatePatient(UpdatePatientRequest request, {CallOptions? options}) { + final update = PatientUpdate( + id: request.id, + name: request.hasHumanReadableIdentifier() ? request.humanReadableIdentifier : null, + notes: request.hasNotes() ? request.notes : null, + ); + + OfflineClientStore().patientStore.update(update); + + final response = UpdatePatientResponse(); + return MockResponseFuture.value(response); + } + + @override + ResponseFuture assignBed(AssignBedRequest request, {CallOptions? options}) { + OfflineClientStore().patientStore.assignBed(request.id, request.bedId); + final response = AssignBedResponse(); + return MockResponseFuture.value(response); + } + + @override + ResponseFuture unassignBed(UnassignBedRequest request, {CallOptions? options}) { + OfflineClientStore().patientStore.unassignBed(request.id); + final response = UnassignBedResponse(); + return MockResponseFuture.value(response); + } + + @override + ResponseFuture dischargePatient(DischargePatientRequest request, {CallOptions? options}) { + final update = PatientUpdate(id: request.id, isDischarged: true); + + OfflineClientStore().patientStore.update(update); + OfflineClientStore().patientStore.unassignBed(request.id); + + final response = DischargePatientResponse(); + return MockResponseFuture.value(response); + } + + @override + ResponseFuture readmitPatient(ReadmitPatientRequest request, {CallOptions? options}) { + final update = PatientUpdate(id: request.patientId, isDischarged: false); + + OfflineClientStore().patientStore.update(update); + + final response = ReadmitPatientResponse(); + return MockResponseFuture.value(response); + } + + @override + ResponseFuture deletePatient(DeletePatientRequest request, {CallOptions? options}) { + OfflineClientStore().patientStore.delete(request.id); + + final response = DeletePatientResponse(); + return MockResponseFuture.value(response); + } +} diff --git a/packages/helpwave_service/lib/src/api/tasks/offline_clients/room_offline_client.dart b/packages/helpwave_service/lib/src/api/tasks/offline_clients/room_offline_client.dart new file mode 100644 index 00000000..57ed4799 --- /dev/null +++ b/packages/helpwave_service/lib/src/api/tasks/offline_clients/room_offline_client.dart @@ -0,0 +1,161 @@ +import 'package:grpc/grpc.dart'; +import 'package:helpwave_proto_dart/services/tasks_svc/v1/room_svc.pbgrpc.dart'; +import 'package:helpwave_service/src/api/offline/offline_client_store.dart'; +import 'package:helpwave_service/src/api/offline/util.dart'; +import 'package:helpwave_service/src/api/tasks/data_types/index.dart'; + +class RoomUpdate { + String id; + String? name; + + RoomUpdate({required this.id, required this.name}); +} + +class RoomOfflineService { + List rooms = []; + + RoomWithWardId? findRoom(String id) { + int index = OfflineClientStore().roomStore.rooms.indexWhere((value) => value.id == id); + if (index == -1) { + return null; + } + return rooms[index]; + } + + List findRooms([String? wardId]) { + final valueStore = OfflineClientStore().roomStore; + if (wardId == null) { + return valueStore.rooms; + } + return valueStore.rooms.where((value) => value.wardId == wardId).toList(); + } + + void create(RoomWithWardId room) { + OfflineClientStore().roomStore.rooms.add(room); + } + + void update(RoomUpdate room) { + final valueStore = OfflineClientStore().roomStore; + bool found = false; + + valueStore.rooms = valueStore.rooms.map((value) { + if (value.id == room.id) { + found = true; + return RoomWithWardId(id: room.id, name: room.name ?? value.name, wardId: value.wardId); + } + return value; + }).toList(); + + if (!found) { + throw Exception('UpdateRoom: Could not find room with id ${room.id}'); + } + } + + void delete(String roomId) { + final valueStore = OfflineClientStore().roomStore; + valueStore.rooms = valueStore.rooms.where((value) => value.id != roomId).toList(); + OfflineClientStore().bedStore.findBeds(roomId).forEach((element) { + OfflineClientStore().bedStore.delete(element.id); + }); + } +} + +class RoomOfflineClient extends RoomServiceClient { + RoomOfflineClient(super.channel); + + @override + ResponseFuture getRoom(GetRoomRequest request, {CallOptions? options}) { + final room = OfflineClientStore().roomStore.findRoom(request.id); + + if (room == null) { + throw "Room with room id ${request.id} not found"; + } + + final response = GetRoomResponse() + ..id = room.id + ..name = room.name + ..wardId = room.wardId; + + return MockResponseFuture.value(response); + } + + @override + ResponseFuture getRooms(GetRoomsRequest request, {CallOptions? options}) { + final rooms = OfflineClientStore().roomStore.findRooms(); + final roomsList = rooms.map((room) => GetRoomsResponse_Room( + id: room.id, + name: room.name, + wardId: room.wardId, + beds: OfflineClientStore().bedStore.findBeds(room.id).map((bed) => GetRoomsResponse_Room_Bed( + id: bed.id, + name: bed.name, + )), + )); + + final response = GetRoomsResponse(rooms: roomsList); + return MockResponseFuture.value(response); + } + + @override + ResponseFuture getRoomOverviewsByWard(GetRoomOverviewsByWardRequest request, + {CallOptions? options}) { + final rooms = OfflineClientStore().roomStore.findRooms(request.id); + + final response = GetRoomOverviewsByWardResponse() + ..rooms.addAll(rooms.map((room) => GetRoomOverviewsByWardResponse_Room() + ..id = room.id + ..name = room.name + ..beds.addAll(OfflineClientStore().bedStore.findBeds(room.id).map((bed) { + final response = GetRoomOverviewsByWardResponse_Room_Bed(id: bed.id, name: bed.name); + final patient = OfflineClientStore().patientStore.findPatientByBed(bed.id); + if (patient != null) { + final tasks = OfflineClientStore().taskStore.findTasks(patient.id); + response.patient = GetRoomOverviewsByWardResponse_Room_Bed_Patient( + id: patient.id, + humanReadableIdentifier: patient.name, + tasksDone: tasks.where((element) => element.status == TaskStatus.done).length, + tasksInProgress: tasks.where((element) => element.status == TaskStatus.inProgress).length, + tasksUnscheduled: tasks.where((element) => element.status == TaskStatus.todo).length, + ); + } + return response; + })))); + return MockResponseFuture.value(response); + } + + @override + ResponseFuture createRoom(CreateRoomRequest request, {CallOptions? options}) { + final newRoom = RoomWithWardId( + id: DateTime.now().millisecondsSinceEpoch.toString(), + name: request.name, + wardId: request.wardId, + ); + + OfflineClientStore().roomStore.create(newRoom); + + final response = CreateRoomResponse()..id = newRoom.id; + + return MockResponseFuture.value(response); + } + + @override + ResponseFuture updateRoom(UpdateRoomRequest request, {CallOptions? options}) { + final update = RoomUpdate( + id: request.id, + name: request.name, + ); + + OfflineClientStore().roomStore.update(update); + + final response = UpdateRoomResponse(); + return MockResponseFuture.value(response); + } + + @override + ResponseFuture deleteRoom(DeleteRoomRequest request, {CallOptions? options}) { + OfflineClientStore().roomStore.delete(request.id); + + final response = DeleteRoomResponse(); + return MockResponseFuture.value(response); + } +} diff --git a/packages/helpwave_service/lib/src/api/tasks/offline_clients/task_offline_client.dart b/packages/helpwave_service/lib/src/api/tasks/offline_clients/task_offline_client.dart new file mode 100644 index 00000000..27b1fae0 --- /dev/null +++ b/packages/helpwave_service/lib/src/api/tasks/offline_clients/task_offline_client.dart @@ -0,0 +1,445 @@ +import 'package:grpc/grpc.dart'; +import 'package:helpwave_proto_dart/google/protobuf/timestamp.pb.dart'; +import 'package:helpwave_proto_dart/services/tasks_svc/v1/task_svc.pbgrpc.dart'; +import 'package:helpwave_service/src/api/offline/offline_client_store.dart'; +import 'package:helpwave_service/src/api/offline/util.dart'; +import 'package:helpwave_service/src/api/tasks/index.dart'; +import 'package:helpwave_service/src/api/tasks/util/task_status_mapping.dart'; + +class TaskUpdate { + String id; + String? name; + String? notes; + TaskStatus? status; + bool? isPublicVisible; + DateTime? dueDate; + + TaskUpdate({required this.id, this.name, this.notes, this.status, this.isPublicVisible, this.dueDate}); +} + +class SubtaskUpdate { + String id; + bool? isDone; + String? name; + + SubtaskUpdate({required this.id, this.name, this.isDone}); +} + +class TaskOfflineService { + List tasks = []; + + Task? findTask(String id) { + int index = OfflineClientStore().taskStore.tasks.indexWhere((value) => value.id == id); + if (index == -1) { + return null; + } + return tasks[index]; + } + + List findTasks([String? patientId]) { + final valueStore = OfflineClientStore().taskStore; + if (patientId == null) { + return valueStore.tasks; + } + return valueStore.tasks.where((value) => value.patientId == patientId).toList(); + } + + void create(Task task) { + OfflineClientStore().taskStore.tasks.add(task); + } + + void update(TaskUpdate taskUpdate) { + final valueStore = OfflineClientStore().taskStore; + bool found = false; + + valueStore.tasks = valueStore.tasks.map((value) { + if (value.id == taskUpdate.id) { + found = true; + return value.copyWith( + name: taskUpdate.name, + notes: taskUpdate.notes, + status: taskUpdate.status, + isPublicVisible: taskUpdate.isPublicVisible, + dueDate: taskUpdate.dueDate, + ); + } + return value; + }).toList(); + + if (!found) { + throw Exception('UpdateTask: Could not find task with id ${taskUpdate.id}'); + } + } + + void removeDueDate(String taskId) { + final valueStore = OfflineClientStore().taskStore; + bool found = false; + + valueStore.tasks = valueStore.tasks.map((value) { + if (value.id == taskId) { + found = true; + final copy = value.copyWith(); + copy.dueDate = null; + return copy; + } + return value; + }).toList(); + + if (!found) { + throw Exception('Could not find task with id $taskId'); + } + } + + assignUser(String taskId, String assigneeId) { + final user = OfflineClientStore().userStore.find(assigneeId); + if (user == null) { + throw "Could not find user with id $assigneeId"; + } + final valueStore = OfflineClientStore().taskStore; + bool found = false; + + valueStore.tasks = valueStore.tasks.map((value) { + if (value.id == taskId) { + found = true; + return value.copyWith(assigneeId: assigneeId); + } + return value; + }).toList(); + + if (!found) { + throw Exception('Could not find task with id $taskId'); + } + } + + unassignUser(String taskId, String assigneeId) { + final valueStore = OfflineClientStore().taskStore; + bool found = false; + + valueStore.tasks = valueStore.tasks.map((value) { + if (value.id == taskId) { + found = true; + final copy = value.copyWith(); + copy.assigneeId = null; + return copy; + } + return value; + }).toList(); + + if (!found) { + throw Exception('Could not find task with id $taskId'); + } + } + + void delete(String taskId) { + final valueStore = OfflineClientStore().taskStore; + valueStore.tasks = valueStore.tasks.where((value) => value.id != taskId).toList(); + OfflineClientStore().subtaskStore.findSubtasks(taskId).forEach((subtask) { + OfflineClientStore().subtaskStore.delete(subtask.id); + }); + } +} + +class SubtaskOfflineService { + List subtasks = []; + + Subtask? findSubtask(String id) { + int index = OfflineClientStore().subtaskStore.subtasks.indexWhere((value) => value.id == id); + if (index == -1) { + return null; + } + return subtasks[index]; + } + + List findSubtasks([String? taskId]) { + final valueStore = OfflineClientStore().subtaskStore; + if (taskId == null) { + return valueStore.subtasks; + } + return valueStore.subtasks.where((value) => value.taskId == taskId).toList(); + } + + void create(Subtask subtask) { + OfflineClientStore().subtaskStore.subtasks.add(subtask); + } + + void update(SubtaskUpdate subtaskUpdate) { + final valueStore = OfflineClientStore().subtaskStore; + bool found = false; + + valueStore.subtasks = valueStore.subtasks.map((value) { + if (value.id == subtaskUpdate.id) { + found = true; + return value.copyWith(name: subtaskUpdate.name, isDone: subtaskUpdate.isDone); + } + return value; + }).toList(); + + if (!found) { + throw Exception('UpdateSubtask: Could not find subtask with id ${subtaskUpdate.id}'); + } + } + + void delete(String subtaskId) { + final valueStore = OfflineClientStore().subtaskStore; + valueStore.subtasks = valueStore.subtasks.where((value) => value.id != subtaskId).toList(); + } +} + +class TaskOfflineClient extends TaskServiceClient { + TaskOfflineClient(super.channel); + + @override + ResponseFuture getTask(GetTaskRequest request, {CallOptions? options}) { + final task = OfflineClientStore().taskStore.findTask(request.id); + + if (task == null) { + throw "Task with task id ${request.id} not found"; + } + + final patient = OfflineClientStore().patientStore.findPatient(task.patientId); + if (patient == null) { + throw "Inconsistency error: Patient with patient id ${task.patientId} not found"; + } + + final subtasks = OfflineClientStore() + .subtaskStore + .findSubtasks(task.id) + .map((subtask) => GetTaskResponse_SubTask(id: subtask.id, name: subtask.name, done: subtask.isDone)); + + final response = GetTaskResponse( + id: task.id, + name: task.name, + description: task.notes, + status: GRPCTypeConverter.taskStatusToGRPC(task.status), + dueAt: task.dueDate == null ? null : Timestamp.fromDateTime(task.dueDate!), + createdBy: task.createdBy, + createdAt: task.creationDate == null ? null : Timestamp.fromDateTime(task.creationDate!), + public: task.isPublicVisible, + assignedUserId: task.assigneeId, + patient: GetTaskResponse_Patient(id: patient.id, humanReadableIdentifier: patient.name), + subtasks: subtasks); + + return MockResponseFuture.value(response); + } + + @override + ResponseFuture getTasksByPatient(GetTasksByPatientRequest request, + {CallOptions? options}) { + final tasks = + OfflineClientStore().taskStore.findTasks(request.patientId).map((task) => GetTasksByPatientResponse_Task( + id: task.id, + name: task.name, + description: task.notes, + status: GRPCTypeConverter.taskStatusToGRPC(task.status), + dueAt: task.dueDate == null ? null : Timestamp.fromDateTime(task.dueDate!), + createdBy: task.createdBy, + createdAt: task.creationDate == null ? null : Timestamp.fromDateTime(task.creationDate!), + public: task.isPublicVisible, + assignedUserId: task.assigneeId, + patientId: request.patientId, + subtasks: OfflineClientStore() + .subtaskStore + .findSubtasks(task.id) + .map((subtask) => GetTasksByPatientResponse_Task_SubTask( + id: subtask.id, + name: subtask.name, + done: subtask.isDone, + )), + )); + + final response = GetTasksByPatientResponse(tasks: tasks); + + return MockResponseFuture.value(response); + } + + @override + ResponseFuture getAssignedTasks(GetAssignedTasksRequest request, {CallOptions? options}) { + final user = OfflineClientStore().userStore.users[0]; + final tasks = OfflineClientStore().taskStore.findTasks().where((task) => task.assigneeId == user.id).map((task) { + final res = GetAssignedTasksResponse_Task( + id: task.id, + name: task.name, + description: task.notes, + status: GRPCTypeConverter.taskStatusToGRPC(task.status), + dueAt: task.dueDate == null ? null : Timestamp.fromDateTime(task.dueDate!), + createdBy: task.createdBy, + createdAt: task.creationDate == null ? null : Timestamp.fromDateTime(task.creationDate!), + public: task.isPublicVisible, + assignedUserId: task.assigneeId, + subtasks: OfflineClientStore() + .subtaskStore + .findSubtasks(task.id) + .map((subtask) => GetAssignedTasksResponse_Task_SubTask( + id: subtask.id, + name: subtask.name, + done: subtask.isDone, + )), + ); + final patient = OfflineClientStore().patientStore.findPatient(task.patientId); + if (patient == null) { + throw "Inconsistency error: patient with id ${task.patientId} not found"; + } + res.patient = GetAssignedTasksResponse_Task_Patient(id: patient.id, humanReadableIdentifier: patient.name); + return res; + }); + + final response = GetAssignedTasksResponse(tasks: tasks); + + return MockResponseFuture.value(response); + } + + @override + ResponseFuture getTasksByPatientSortedByStatus( + GetTasksByPatientSortedByStatusRequest request, + {CallOptions? options}) { + mapping(task) => GetTasksByPatientSortedByStatusResponse_Task( + id: task.id, + name: task.name, + description: task.notes, + dueAt: task.dueDate == null ? null : Timestamp.fromDateTime(task.dueDate!), + createdBy: task.createdBy, + createdAt: task.creationDate == null ? null : Timestamp.fromDateTime(task.creationDate!), + public: task.isPublicVisible, + assignedUserId: task.assigneeId, + patientId: request.patientId, + subtasks: OfflineClientStore() + .subtaskStore + .findSubtasks(task.id) + .map((subtask) => GetTasksByPatientSortedByStatusResponse_Task_SubTask( + id: subtask.id, + name: subtask.name, + done: subtask.isDone, + )), + ); + + final tasks = OfflineClientStore().taskStore.findTasks(request.patientId); + + final response = GetTasksByPatientSortedByStatusResponse( + done: tasks.where((element) => element.status == TaskStatus.done).map(mapping), + inProgress: tasks.where((element) => element.status == TaskStatus.inProgress).map(mapping), + todo: tasks.where((element) => element.status == TaskStatus.todo).map(mapping), + ); + + return MockResponseFuture.value(response); + } + + @override + ResponseFuture createTask(CreateTaskRequest request, {CallOptions? options}) { + final patient = OfflineClientStore().patientStore.findPatient(request.patientId); + if (patient == null) { + throw "Patient with id ${request.patientId} not found"; + } + final newTask = Task( + id: DateTime.now().millisecondsSinceEpoch.toString(), + name: request.name, + notes: request.description, + patientId: request.patientId, + creationDate: DateTime.now(), + createdBy: OfflineClientStore().userStore.users[0].id, + dueDate: request.hasDueAt() ? request.dueAt.toDateTime() : null, + status: GRPCTypeConverter.taskStatusFromGRPC(request.initialStatus), + isPublicVisible: request.public, + assigneeId: request.assignedUserId, + ); + + OfflineClientStore().taskStore.create(newTask); + for (var subtask in request.subtasks) { + OfflineClientStore().subtaskStore.create(Subtask( + id: DateTime.now().millisecondsSinceEpoch.toString(), + taskId: newTask.id, + name: subtask.name, + isDone: subtask.done, + )); + } + + final response = CreateTaskResponse()..id = newTask.id; + + return MockResponseFuture.value(response); + } + + @override + ResponseFuture updateTask(UpdateTaskRequest request, {CallOptions? options}) { + final update = TaskUpdate( + id: request.id, + name: request.name, + status: GRPCTypeConverter.taskStatusFromGRPC(request.status), + isPublicVisible: request.public, + notes: request.description, + dueDate: request.hasDueAt() ? request.dueAt.toDateTime() : null, + ); + + OfflineClientStore().taskStore.update(update); + + final response = UpdateTaskResponse(); + return MockResponseFuture.value(response); + } + + @override + ResponseFuture assignTask(AssignTaskRequest request, {CallOptions? options}) { + OfflineClientStore().taskStore.assignUser(request.taskId, request.userId); + final response = AssignTaskResponse(); + return MockResponseFuture.value(response); + } + + @override + ResponseFuture unassignTask(UnassignTaskRequest request, {CallOptions? options}) { + OfflineClientStore().taskStore.unassignUser(request.taskId, request.userId); + final response = UnassignTaskResponse(); + return MockResponseFuture.value(response); + } + + @override + ResponseFuture removeTaskDueDate(RemoveTaskDueDateRequest request, + {CallOptions? options}) { + OfflineClientStore().taskStore.removeDueDate(request.taskId); + final response = RemoveTaskDueDateResponse(); + return MockResponseFuture.value(response); + } + + @override + ResponseFuture deleteTask(DeleteTaskRequest request, {CallOptions? options}) { + OfflineClientStore().taskStore.delete(request.id); + + final response = DeleteTaskResponse(); + return MockResponseFuture.value(response); + } + + @override + ResponseFuture createSubtask(CreateSubtaskRequest request, {CallOptions? options}) { + final task = OfflineClientStore().taskStore.findTask(request.taskId); + if (task == null) { + throw "Task with id ${request.taskId} not found"; + } + final subtask = Subtask( + id: DateTime.now().millisecondsSinceEpoch.toString(), + taskId: request.taskId, + name: request.subtask.name, + isDone: request.subtask.done); + + OfflineClientStore().subtaskStore.create(subtask); + final response = CreateSubtaskResponse()..subtaskId = subtask.id; + return MockResponseFuture.value(response); + } + + @override + ResponseFuture updateSubtask(UpdateSubtaskRequest request, {CallOptions? options}) { + final requestSubtask = request.subtask; + final update = SubtaskUpdate( + id: request.subtaskId, + isDone: requestSubtask.hasDone() ? requestSubtask.done : null, + name: requestSubtask.hasName() ? requestSubtask.name : null, + ); + OfflineClientStore().subtaskStore.update(update); + + final response = UpdateSubtaskResponse(); + return MockResponseFuture.value(response); + } + + @override + ResponseFuture deleteSubtask(DeleteSubtaskRequest request, {CallOptions? options}) { + OfflineClientStore().subtaskStore.delete(request.subtaskId); + + final response = DeleteSubtaskResponse(); + return MockResponseFuture.value(response); + } +} diff --git a/packages/helpwave_service/lib/src/api/tasks/offline_clients/template_offline_client.dart b/packages/helpwave_service/lib/src/api/tasks/offline_clients/template_offline_client.dart new file mode 100644 index 00000000..3479128a --- /dev/null +++ b/packages/helpwave_service/lib/src/api/tasks/offline_clients/template_offline_client.dart @@ -0,0 +1,241 @@ +import 'package:grpc/grpc.dart'; +import 'package:helpwave_proto_dart/services/tasks_svc/v1/task_template_svc.pbgrpc.dart'; +import 'package:helpwave_service/src/api/offline/offline_client_store.dart'; +import 'package:helpwave_service/src/api/offline/util.dart'; +import 'package:helpwave_service/src/api/tasks/data_types/index.dart'; + +class TaskTemplateUpdate { + String id; + String? name; + String? notes; + + TaskTemplateUpdate({required this.id, this.name, this.notes}); +} + +class TaskSubtaskTemplateUpdate { + String id; + String? name; + + TaskSubtaskTemplateUpdate({required this.id, this.name}); +} + +class TaskTemplateOfflineService { + List taskTemplates = []; + + TaskTemplate? findTaskTemplate(String id) { + int index = taskTemplates.indexWhere((value) => value.id == id); + if (index == -1) { + return null; + } + return taskTemplates[index]; + } + + List findTaskTemplates([String? wardId]) { + if (wardId == null) { + return taskTemplates; + } + return taskTemplates.where((value) => value.wardId == wardId).toList(); + } + + void create(TaskTemplate taskTemplate) { + taskTemplates.add(taskTemplate); + } + + void update(TaskTemplateUpdate taskTemplateUpdate) { + bool found = false; + + taskTemplates = taskTemplates.map((value) { + if (value.id == taskTemplateUpdate.id) { + found = true; + return value.copyWith( + name: taskTemplateUpdate.name, + notes: taskTemplateUpdate.notes, + ); + } + return value; + }).toList(); + + if (!found) { + throw Exception('UpdateTaskTemplate: Could not find task with id ${taskTemplateUpdate.id}'); + } + } + + void delete(String taskId) { + taskTemplates = taskTemplates.where((value) => value.id != taskId).toList(); + OfflineClientStore().taskTemplateSubtaskStore.findTemplateSubtasks(taskId).forEach((templateSubtask) { + OfflineClientStore().taskTemplateSubtaskStore.delete(templateSubtask.id); + }); + } +} + +class TaskTemplateSubtaskOfflineService { + List taskTemplateSubtasks = []; + + Subtask? findTemplateSubtask(String id) { + int index = taskTemplateSubtasks.indexWhere((value) => value.id == id); + if (index == -1) { + return null; + } + return taskTemplateSubtasks[index]; + } + + List findTemplateSubtasks([String? taskTemplateId]) { + if (taskTemplateId == null) { + return taskTemplateSubtasks; + } + return taskTemplateSubtasks.where((value) => value.taskId == taskTemplateId).toList(); + } + + void create(Subtask templateSubtask) { + taskTemplateSubtasks.add(templateSubtask); + } + + void update(TaskSubtaskTemplateUpdate templateSubtaskUpdate) { + bool found = false; + + taskTemplateSubtasks = taskTemplateSubtasks.map((value) { + if (value.id == templateSubtaskUpdate.id) { + found = true; + return value.copyWith(name: templateSubtaskUpdate.name); + } + return value; + }).toList(); + + if (!found) { + throw Exception('UpdateTemplateSubtask: Could not find templateSubtask with id ${templateSubtaskUpdate.id}'); + } + } + + void delete(String templateSubtaskId) { + taskTemplateSubtasks = taskTemplateSubtasks.where((value) => value.id != templateSubtaskId).toList(); + } +} + +class TaskTemplateServicePromiseClient extends TaskTemplateServiceClient { + TaskTemplateServicePromiseClient(super.channel); + + @override + ResponseFuture getAllTaskTemplates(GetAllTaskTemplatesRequest request, + {CallOptions? options}) { + final user = OfflineClientStore().userStore.users[0]; + final templates = OfflineClientStore().taskTemplateStore.taskTemplates.where((template) { + if (request.hasWardId() && template.wardId != request.wardId) { + return false; + } + if (request.hasCreatedBy() && template.createdBy != request.createdBy) { + return false; + } + if (request.privateOnly && template.createdBy != user.id) { + return false; + } + return true; + }); + + final response = GetAllTaskTemplatesResponse( + templates: templates.map( + (template) => GetAllTaskTemplatesResponse_TaskTemplate( + id: template.id, + name: template.name, + createdBy: template.createdBy, + description: template.notes, + isPublic: template.isPublicVisible, + subtasks: OfflineClientStore() + .taskTemplateSubtaskStore + .findTemplateSubtasks(template.id) + .map((taskTemplateSubtask) => GetAllTaskTemplatesResponse_TaskTemplate_SubTask( + id: taskTemplateSubtask.id, + name: taskTemplateSubtask.name, + taskTemplateId: taskTemplateSubtask.taskId, + ))), + )); + return MockResponseFuture.value(response); + } + + @override + ResponseFuture createTaskTemplate(CreateTaskTemplateRequest request, + {CallOptions? options}) { + final newTaskTemplate = TaskTemplate( + id: DateTime.now().millisecondsSinceEpoch.toString(), + name: request.name, + notes: request.description, + wardId: request.hasWardId() ? request.wardId : null, + createdBy: OfflineClientStore().userStore.users[0].id, + ); + + OfflineClientStore().taskTemplateStore.create(newTaskTemplate); + for (var templateSubtask in request.subtasks) { + OfflineClientStore().taskTemplateSubtaskStore.create(Subtask( + id: DateTime.now().millisecondsSinceEpoch.toString(), + taskId: newTaskTemplate.id, + name: templateSubtask.name, + isDone: false, + )); + } + + final response = CreateTaskTemplateResponse()..id = newTaskTemplate.id; + + return MockResponseFuture.value(response); + } + + @override + ResponseFuture updateTaskTemplate(UpdateTaskTemplateRequest request, + {CallOptions? options}) { + final update = TaskTemplateUpdate( + id: request.id, + name: request.hasName() ? request.name : null, + ); + + OfflineClientStore().taskTemplateStore.update(update); + + final response = UpdateTaskTemplateResponse(); + return MockResponseFuture.value(response); + } + + @override + ResponseFuture deleteTaskTemplate(DeleteTaskTemplateRequest request, + {CallOptions? options}) { + OfflineClientStore().taskStore.delete(request.id); + + final response = DeleteTaskTemplateResponse(); + return MockResponseFuture.value(response); + } + + @override + ResponseFuture createTaskTemplateSubTask(CreateTaskTemplateSubTaskRequest request, + {CallOptions? options}) { + final task = OfflineClientStore().taskTemplateStore.findTaskTemplate(request.taskTemplateId); + if (task == null) { + throw "TaskTemplate with id ${request.taskTemplateId} not found"; + } + final templateSubtask = Subtask( + id: DateTime.now().millisecondsSinceEpoch.toString(), + taskId: request.taskTemplateId, + name: request.name, + isDone: false, + ); + + OfflineClientStore().taskTemplateSubtaskStore.create(templateSubtask); + final response = CreateTaskTemplateSubTaskResponse()..id = templateSubtask.id; + return MockResponseFuture.value(response); + } + + @override + ResponseFuture updateTaskTemplateSubTask(UpdateTaskTemplateSubTaskRequest request, {CallOptions? options}) { + final update = TaskSubtaskTemplateUpdate( + id: request.subtaskId, + name: request.hasName() ? request.name : null, + ); + OfflineClientStore().taskTemplateSubtaskStore.update(update); + + final response = UpdateTaskTemplateSubTaskResponse(); + return MockResponseFuture.value(response); + } + + @override + ResponseFuture deleteTaskTemplateSubTask(DeleteTaskTemplateSubTaskRequest request, {CallOptions? options}) { + OfflineClientStore().taskTemplateSubtaskStore.delete(request.id); + + final response = DeleteTaskTemplateSubTaskResponse(); + return MockResponseFuture.value(response); + } +} diff --git a/packages/helpwave_service/lib/src/api/tasks/offline_clients/ward_offline_client.dart b/packages/helpwave_service/lib/src/api/tasks/offline_clients/ward_offline_client.dart new file mode 100644 index 00000000..aacb28a3 --- /dev/null +++ b/packages/helpwave_service/lib/src/api/tasks/offline_clients/ward_offline_client.dart @@ -0,0 +1,238 @@ +import 'package:grpc/grpc.dart'; +import 'package:helpwave_proto_dart/services/tasks_svc/v1/ward_svc.pbgrpc.dart'; +import 'package:helpwave_service/src/api/offline/offline_client_store.dart'; +import 'package:helpwave_service/src/api/offline/util.dart'; +import 'package:helpwave_service/src/api/tasks/index.dart'; + +class WardUpdate { + String id; + String? name; + + WardUpdate({required this.id, required this.name}); +} + +class WardOfflineService { + List wards = []; + + Ward? findWard(String id) { + int index = OfflineClientStore().wardStore.wards.indexWhere((value) => value.id == id); + if (index == -1) { + return null; + } + return wards[index]; + } + + List findWards([String? organizationId]) { + final valueStore = OfflineClientStore().wardStore; + if (organizationId == null) { + return valueStore.wards; + } + return valueStore.wards.where((value) => value.organizationId == organizationId).toList(); + } + + void create(Ward ward) { + OfflineClientStore().wardStore.wards.add(ward); + } + + void update(WardUpdate ward) { + final valueStore = OfflineClientStore().wardStore; + bool found = false; + + valueStore.wards = valueStore.wards.map((value) { + if (value.id == ward.id) { + found = true; + return Ward(id: ward.id, name: ward.name ?? value.name, organizationId: value.organizationId); + } + return value; + }).toList(); + + if (!found) { + throw Exception('UpdateWard: Could not find ward with id ${ward.id}'); + } + } + + void delete(String wardId) { + final valueStore = OfflineClientStore().wardStore; + valueStore.wards = valueStore.wards.where((value) => value.id != wardId).toList(); + OfflineClientStore().roomStore.findRooms(wardId).forEach((element) { + OfflineClientStore().roomStore.delete(element.id); + }); + final taskTemplates = OfflineClientStore().taskTemplateStore.findTaskTemplates(wardId); + for (var element in taskTemplates) { + OfflineClientStore().taskTemplateStore.delete(element.id); + } + } +} + +class WardOfflineClient extends WardServiceClient { + WardOfflineClient(super.channel); + + @override + ResponseFuture getWard(GetWardRequest request, {CallOptions? options}) { + final ward = OfflineClientStore().wardStore.findWard(request.id); + + if (ward == null) { + throw "Ward with ward id ${request.id} not found"; + } + + final response = GetWardResponse() + ..id = ward.id + ..name = ward.name; + + return MockResponseFuture.value(response); + } + + @override + ResponseFuture getWardDetails(GetWardDetailsRequest request, {CallOptions? options}) { + final ward = OfflineClientStore().wardStore.findWard(request.id); + + if (ward == null) { + throw "Ward with ward id ${request.id} not found"; + } + + final rooms = OfflineClientStore().roomStore.findRooms(ward.id).map((room) { + final beds = OfflineClientStore() + .bedStore + .findBeds(room.id) + .map((bed) => GetWardDetailsResponse_Bed(id: bed.id, name: bed.name)) + .toList(); + + return GetWardDetailsResponse_Room() + ..id = room.id + ..name = room.name + ..beds.addAll(beds); + }).toList(); + + final response = GetWardDetailsResponse( + id: ward.id, + name: ward.name, + rooms: rooms, + taskTemplates: OfflineClientStore() + .taskTemplateStore + .findTaskTemplates(ward.id) + .map((template) => GetWardDetailsResponse_TaskTemplate( + id: template.id, + name: template.name, + subtasks: OfflineClientStore().taskTemplateSubtaskStore.findTemplateSubtasks(template.id).map( + (subtask) => GetWardDetailsResponse_Subtask(id: subtask.id, name: subtask.name), + ), + )), + ); + + return MockResponseFuture.value(response); + } + + @override + ResponseFuture getWards(GetWardsRequest request, {CallOptions? options}) { + final wards = OfflineClientStore().wardStore.findWards(); + final wardsList = wards + .map((ward) => GetWardsResponse_Ward() + ..id = ward.id + ..name = ward.name) + .toList(); + + final response = GetWardsResponse()..wards.addAll(wardsList); + + return MockResponseFuture.value(response); + } + + @override + ResponseFuture getRecentWards(GetRecentWardsRequest request, {CallOptions? options}) { + final wards = OfflineClientStore().wardStore.findWards(); + final wardsList = wards.map((ward) { + final rooms = OfflineClientStore().roomStore.findRooms(ward.id); + final beds = + rooms.map((room) => OfflineClientStore().bedStore.findBeds(room.id)).expand((element) => element).toList(); + List patients = []; + for (var bed in beds) { + final patient = OfflineClientStore().patientStore.findPatient(bed.id); + if (patient != null) { + patients.add(patient); + } + } + List tasks = patients + .map((patient) => OfflineClientStore().taskStore.findTasks(patient.id)) + .expand((element) => element) + .toList(); + return GetRecentWardsResponse_Ward( + id: ward.id, + name: ward.name, + bedCount: beds.length, + tasksDone: tasks.where((element) => element.status == TaskStatus.done).length, + tasksInProgress: tasks.where((element) => element.status == TaskStatus.inProgress).length, + tasksTodo: tasks.where((element) => element.status == TaskStatus.todo).length, + ); + }); + final response = GetRecentWardsResponse()..wards.addAll(wardsList); + + return MockResponseFuture.value(response); + } + + @override + ResponseFuture getWardOverviews(GetWardOverviewsRequest request, {CallOptions? options}) { + final wards = OfflineClientStore().wardStore.findWards(); + final wardsList = wards.map((ward) { + final rooms = OfflineClientStore().roomStore.findRooms(ward.id); + final beds = + rooms.map((room) => OfflineClientStore().bedStore.findBeds(room.id)).expand((element) => element).toList(); + List patients = []; + for (var bed in beds) { + final patient = OfflineClientStore().patientStore.findPatient(bed.id); + if (patient != null) { + patients.add(patient); + } + } + List tasks = patients + .map((patient) => OfflineClientStore().taskStore.findTasks(patient.id)) + .expand((element) => element) + .toList(); + return GetWardOverviewsResponse_Ward( + id: ward.id, + name: ward.name, + bedCount: beds.length, + tasksDone: tasks.where((element) => element.status == TaskStatus.done).length, + tasksInProgress: tasks.where((element) => element.status == TaskStatus.inProgress).length, + tasksTodo: tasks.where((element) => element.status == TaskStatus.todo).length, + ); + }); + final response = GetWardOverviewsResponse(wards: wardsList); + + return MockResponseFuture.value(response); + } + + @override + ResponseFuture createWard(CreateWardRequest request, {CallOptions? options}) { + final newWard = Ward( + id: DateTime.now().millisecondsSinceEpoch.toString(), + name: request.name, + organizationId: 'organization', // TODO: Check organization + ); + + OfflineClientStore().wardStore.create(newWard); + + final response = CreateWardResponse()..id = newWard.id; + + return MockResponseFuture.value(response); + } + + @override + ResponseFuture updateWard(UpdateWardRequest request, {CallOptions? options}) { + final update = WardUpdate( + id: request.id, + name: request.name, + ); + + OfflineClientStore().wardStore.update(update); + + final response = UpdateWardResponse(); + return MockResponseFuture.value(response); + } + + @override + ResponseFuture deleteWard(DeleteWardRequest request, {CallOptions? options}) { + OfflineClientStore().wardStore.delete(request.id); + + final response = DeleteWardResponse(); + return MockResponseFuture.value(response); + } +} diff --git a/packages/helpwave_service/lib/src/api/tasks/services/bed_svc.dart b/packages/helpwave_service/lib/src/api/tasks/services/bed_svc.dart new file mode 100644 index 00000000..e69de29b diff --git a/packages/helpwave_service/lib/src/api/tasks/services/index.dart b/packages/helpwave_service/lib/src/api/tasks/services/index.dart new file mode 100644 index 00000000..78922c72 --- /dev/null +++ b/packages/helpwave_service/lib/src/api/tasks/services/index.dart @@ -0,0 +1,5 @@ +export 'ward_service.dart'; +export 'room_svc.dart'; +export 'bed_svc.dart'; +export 'patient_svc.dart'; +export 'task_svc.dart'; diff --git a/packages/helpwave_service/lib/src/api/tasks/services/patient_svc.dart b/packages/helpwave_service/lib/src/api/tasks/services/patient_svc.dart new file mode 100644 index 00000000..5695ddbe --- /dev/null +++ b/packages/helpwave_service/lib/src/api/tasks/services/patient_svc.dart @@ -0,0 +1,222 @@ +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'; + +/// 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 { + /// The GRPC ServiceClient which handles GRPC + 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 + Future getPatientList({String? wardId}) async { + GetPatientListRequest request = GetPatientListRequest(wardId: wardId); + GetPatientListResponse response = await patientService.getPatientList( + request, + options: CallOptions( + metadata: TasksAPIServiceClients().getMetaData(), + ), + ); + + mapping(GetPatientListResponse_Patient patient) { + final res = Patient( + id: patient.id, + name: patient.humanReadableIdentifier, + isDischarged: response.dischargedPatients.contains(patient), + tasks: patient.tasks + .map((task) => Task( + id: task.id, + name: task.name, + notes: task.description, + status: GRPCTypeConverter.taskStatusFromGRPC(task.status), + isPublicVisible: task.public, + assigneeId: task.assignedUserId, + subtasks: task.subtasks + .map((subtask) => + Subtask(id: subtask.id, name: subtask.name, isDone: subtask.done, taskId: task.id)) + .toList(), + patientId: patient.id, + // TODO due and creation date + )) + .toList(), + notes: patient.notes, + bed: BedMinimal(id: patient.bed.id, name: patient.bed.name), + room: RoomMinimal(id: patient.room.id, name: patient.room.name), + ); + if (patient.hasBed() && patient.hasRoom()) { + res.bed = BedMinimal(id: patient.bed.id, name: patient.bed.name); + res.room = RoomMinimal(id: patient.room.id, name: patient.room.name); + } + return res; + } + + final active = response.active.map(mapping).toList(); + final unassigned = response.unassignedPatients.map(mapping).toList(); + final discharged = response.dischargedPatients.map(mapping).toList(); + + return PatientsByAssignmentStatus( + active: active, + unassigned: unassigned, + discharged: discharged, + ); + } + + /// Loads the [Patient]s by id + Future getPatient({required String patientId}) async { + GetPatientRequest request = GetPatientRequest(id: patientId); + GetPatientResponse response = await patientService.getPatient( + request, + options: CallOptions( + metadata: TasksAPIServiceClients().getMetaData(), + ), + ); + + // TODO maybe also use bedId and notes from response + return PatientMinimal( + id: response.id, + name: response.humanReadableIdentifier, + ); + } + + /// Loads the [Patient]s with detailed information + Future getPatientDetails({required String patientId}) async { + GetPatientDetailsRequest request = GetPatientDetailsRequest(id: patientId); + GetPatientDetailsResponse response = await patientService.getPatientDetails( + request, + options: CallOptions( + metadata: TasksAPIServiceClients().getMetaData(), + ), + ); + + return Patient( + id: response.id, + name: response.humanReadableIdentifier, + notes: response.notes, + isDischarged: response.isDischarged, + tasks: response.tasks + .map((task) => Task( + id: task.id, + name: task.name, + notes: task.description, + assigneeId: task.assignedUserId, + status: GRPCTypeConverter.taskStatusFromGRPC(task.status), + isPublicVisible: task.public, + subtasks: task.subtasks + .map((subtask) => Subtask( + id: subtask.id, + taskId: task.id, + name: subtask.name, + )) + .toList(), + patientId: response.id)) + .toList(), + bed: response.hasBed() ? BedMinimal(id: response.bed.id, name: response.bed.name) : null, + room: response.hasRoom() ? RoomMinimal(id: response.room.id, name: response.room.name) : null, + ); + } + + /// 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( + request, + options: CallOptions( + metadata: TasksAPIServiceClients().getMetaData(), + ), + ); + + return response.rooms.map((room) { + var beds = room.beds; + return RoomWithBedWithMinimalPatient( + id: room.id, + name: room.id, + beds: beds.map((bed) { + var patient = bed.patient; + return BedWithMinimalPatient( + id: bed.id, + name: bed.name, + patient: patient.isInitialized() ? PatientMinimal(id: patient.id, name: patient.name) : null, + ); + }).toList()); + }).toList(); + } + + /// Create a [Patient] + Future createPatient(Patient patient) async { + CreatePatientRequest request = CreatePatientRequest( + notes: patient.notes, + humanReadableIdentifier: patient.name, + ); + CreatePatientResponse response = await patientService.createPatient( + request, + options: CallOptions(metadata: TasksAPIServiceClients().getMetaData()), + ); + + return response.id; + } + + /// Update a [Patient] + Future updatePatient({required String id, String? notes, String? name}) async { + UpdatePatientRequest request = UpdatePatientRequest( + id: id, + notes: notes, + humanReadableIdentifier: name, + ); + UpdatePatientResponse response = await patientService.updatePatient( + request, + options: CallOptions(metadata: TasksAPIServiceClients().getMetaData()), + ); + + if (response.isInitialized()) { + return true; + } + return false; + } + + // TODO consider an enum instead of an string + /// Discharges a [Patient] + Future dischargePatient({required String patientId}) async { + DischargePatientRequest request = DischargePatientRequest(id: patientId); + DischargePatientResponse response = await patientService.dischargePatient( + request, + options: CallOptions(metadata: TasksAPIServiceClients().getMetaData()), + ); + + if (response.isInitialized()) { + return true; + } + return false; + } + + /// Unassigns a [Patient] from a [Bed] + Future unassignPatient({required String patientId}) async { + UnassignBedRequest request = UnassignBedRequest(id: patientId); + UnassignBedResponse response = await patientService.unassignBed( + request, + options: CallOptions(metadata: TasksAPIServiceClients().getMetaData()), + ); + + if (response.isInitialized()) { + return true; + } + return false; + } + + /// Assigns a [Patient] to a [Bed] + Future assignBed({required String patientId, required String bedId}) async { + AssignBedRequest request = AssignBedRequest(id: patientId, bedId: bedId); + AssignBedResponse response = await patientService.assignBed( + request, + options: CallOptions(metadata: TasksAPIServiceClients().getMetaData()), + ); + + if (response.isInitialized()) { + return true; + } + return false; + } +} diff --git a/apps/tasks/lib/services/room_svc.dart b/packages/helpwave_service/lib/src/api/tasks/services/room_svc.dart similarity index 72% rename from apps/tasks/lib/services/room_svc.dart rename to packages/helpwave_service/lib/src/api/tasks/services/room_svc.dart index 48400564..59ae4381 100644 --- a/apps/tasks/lib/services/room_svc.dart +++ b/packages/helpwave_service/lib/src/api/tasks/services/room_svc.dart @@ -1,23 +1,21 @@ import 'package:grpc/grpc.dart'; -import 'package:helpwave_proto_dart/proto/services/task_svc/v1/room_svc.pbgrpc.dart'; -import 'package:tasks/dataclasses/bed.dart'; -import 'package:tasks/dataclasses/patient.dart'; -import 'package:tasks/dataclasses/room.dart'; -import 'package:tasks/services/grpc_client_svc.dart'; +import 'package:helpwave_proto_dart/services/tasks_svc/v1/room_svc.pbgrpc.dart'; +import 'package:helpwave_service/src/api/tasks/data_types/index.dart'; +import 'package:helpwave_service/src/api/tasks/tasks_api_service_clients.dart'; /// The GRPC Service for [Room]s /// /// Provides queries and requests that load or alter [Room] objects on the server -/// The server is defined in the underlying [GRPCClientService] +/// The server is defined in the underlying [TasksAPIServiceClients] class RoomService { /// The GRPC ServiceClient which handles GRPC - RoomServiceClient roomService = GRPCClientService.getRoomServiceClient; + RoomServiceClient roomService = TasksAPIServiceClients().roomServiceClient; Future> getRoomOverviews({required String wardId}) async { GetRoomOverviewsByWardRequest request = GetRoomOverviewsByWardRequest(id: wardId); GetRoomOverviewsByWardResponse response = await roomService.getRoomOverviewsByWard( request, - options: CallOptions(metadata: GRPCClientService().getTaskServiceMetaData()), + options: CallOptions(metadata: TasksAPIServiceClients().getMetaData()), ); List rooms = response.rooms diff --git a/packages/helpwave_service/lib/src/api/tasks/services/task_svc.dart b/packages/helpwave_service/lib/src/api/tasks/services/task_svc.dart new file mode 100644 index 00000000..f32e175f --- /dev/null +++ b/packages/helpwave_service/lib/src/api/tasks/services/task_svc.dart @@ -0,0 +1,225 @@ +import 'package:grpc/grpc.dart'; +import 'package:helpwave_proto_dart/google/protobuf/timestamp.pb.dart'; +import 'package:helpwave_service/src/api/tasks/index.dart'; +import 'package:helpwave_proto_dart/services/tasks_svc/v1/task_svc.pbgrpc.dart'; +import '../util/task_status_mapping.dart'; + +/// The GRPC Service for [Task]s +/// +/// Provides queries and requests that load or alter [Task] objects on the server +/// The server is defined in the underlying [TasksAPIServiceClients] +class TaskService { + /// 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); + GetTasksByPatientResponse response = await taskService.getTasksByPatient( + request, + options: CallOptions(metadata: TasksAPIServiceClients().getMetaData()), + ); + + return response.tasks + .map((task) => Task( + id: task.id, + name: task.name, + notes: task.description, + isPublicVisible: task.public, + status: GRPCTypeConverter.taskStatusFromGRPC(task.status), + assigneeId: task.assignedUserId, + dueDate: task.dueAt.toDateTime(), + subtasks: task.subtasks + .map((subtask) => Subtask( + id: subtask.id, + taskId: task.id, + name: subtask.name, + isDone: subtask.done, + )) + .toList(), + patientId: task.patientId, + createdBy: task.createdBy, + creationDate: task.createdAt.toDateTime())) + .toList(); + } + + /// Loads the [Task]s by it's identifier + Future getTask({String? id}) async { + GetTaskRequest request = GetTaskRequest(id: id); + GetTaskResponse response = await taskService.getTask( + request, + options: CallOptions(metadata: TasksAPIServiceClients().getMetaData()), + ); + + return TaskWithPatient( + id: response.id, + name: response.name, + notes: response.description, + isPublicVisible: response.public, + status: GRPCTypeConverter.taskStatusFromGRPC(response.status), + assigneeId: response.assignedUserId, + dueDate: response.dueAt.toDateTime(), + patient: PatientMinimal(id: response.patient.id, name: response.patient.humanReadableIdentifier), + subtasks: response.subtasks + .map((subtask) => Subtask( + id: subtask.id, + taskId: response.id, + name: subtask.name, + isDone: subtask.done, + )) + .toList(), + patientId: response.patient.id, + createdBy: response.createdBy, + creationDate: response.createdAt.toDateTime(), + ); + } + + /// Loads the [Task]s assigned to the current [User] + Future> getAssignedTasks({String? id}) async { + GetAssignedTasksRequest request = GetAssignedTasksRequest(); + GetAssignedTasksResponse response = await taskService.getAssignedTasks( + request, + options: CallOptions(metadata: TasksAPIServiceClients().getMetaData()), + ); + + return response.tasks + .map((task) => TaskWithPatient( + id: task.id, + name: task.name, + notes: task.description, + isPublicVisible: task.public, + status: GRPCTypeConverter.taskStatusFromGRPC(task.status), + assigneeId: task.assignedUserId, + dueDate: task.dueAt.toDateTime(), + patient: PatientMinimal(id: task.patient.id, name: task.patient.humanReadableIdentifier), + subtasks: task.subtasks + .map((subtask) => Subtask( + id: subtask.id, + taskId: task.id, + name: subtask.name, + isDone: subtask.done, + )) + .toList(), + patientId: task.patient.id, + createdBy: task.createdBy, + creationDate: task.createdAt.toDateTime(), + )) + .toList(); + } + + Future createTask(TaskWithPatient task) async { + CreateTaskRequest request = CreateTaskRequest( + name: task.name, + description: task.notes, + initialStatus: GRPCTypeConverter.taskStatusToGRPC(task.status), + dueAt: task.dueDate != null ? Timestamp.fromDateTime(task.dueDate!) : null, + patientId: !task.patient.isCreating ? task.patient.id : null, + public: task.isPublicVisible, + ); + CreateTaskResponse response = await taskService.createTask( + request, + options: CallOptions(metadata: TasksAPIServiceClients().getMetaData()), + ); + + return response.id; + } + + /// Assign a [Task] to a [User] or unassign the [User] + Future changeAssignee({required String taskId, required String? userId}) async { + if(userId != null){ + AssignTaskRequest request = AssignTaskRequest(taskId: taskId, userId: userId); + await taskService.assignTask( + request, + options: CallOptions(metadata: TasksAPIServiceClients().getMetaData()), + ); + } else { + UnassignTaskRequest request = UnassignTaskRequest(taskId: taskId, userId: userId); + await taskService.unassignTask( + request, + options: CallOptions(metadata: TasksAPIServiceClients().getMetaData()), + ); + } + } + + /// Add a [Subtask] to a [Task] + Future createSubTask({required String taskId, required Subtask subTask}) async { + CreateSubtaskRequest request = CreateSubtaskRequest( + taskId: taskId, + subtask: CreateSubtaskRequest_Subtask( + name: subTask.name, + done: subTask.isDone, + )); + CreateSubtaskResponse response = await taskService.createSubtask( + request, + options: CallOptions(metadata: TasksAPIServiceClients().getMetaData()), + ); + + return Subtask( + id: response.subtaskId, + taskId: taskId, + name: subTask.name, + isDone: subTask.isDone, + ); + } + + /// Delete a [Subtask] by its identifier + Future deleteSubTask({required String subtaskId, required String taskId}) async { + DeleteSubtaskRequest request = DeleteSubtaskRequest(subtaskId: subtaskId, taskId: taskId); + DeleteSubtaskResponse response = await taskService.deleteSubtask( + request, + options: CallOptions(metadata: TasksAPIServiceClients().getMetaData()), + ); + + return response.isInitialized(); + } + + /// Update a [Subtask]'s + Future updateSubtask({required Subtask subtask, required taskId}) async { + UpdateSubtaskRequest request = UpdateSubtaskRequest( + taskId: taskId, + subtaskId: subtask.id, + subtask: UpdateSubtaskRequest_Subtask(done: subtask.isDone, name: subtask.name), + ); + UpdateSubtaskResponse response = await taskService.updateSubtask( + request, + options: CallOptions(metadata: TasksAPIServiceClients().getMetaData()), + ); + + return response.isInitialized(); + } + + Future updateTask({ + required String taskId, + String? name, + String? notes, + DateTime? dueDate, + bool? isPublic, + TaskStatus? status, + }) async { + UpdateTaskRequest request = UpdateTaskRequest( + id: taskId, + name: name, + description: notes, + dueAt: dueDate != null ? Timestamp.fromDateTime(dueDate) : null, + public: isPublic, + status: status != null ? GRPCTypeConverter.taskStatusToGRPC(status) : null, + ); + + UpdateTaskResponse response = await taskService.updateTask( + request, + options: CallOptions(metadata: TasksAPIServiceClients().getMetaData()), + ); + + return response.isInitialized(); + } + + Future removeDueDate({ + required String taskId, + }) async { + RemoveTaskDueDateRequest request = RemoveTaskDueDateRequest(taskId: taskId); + await taskService.removeTaskDueDate( + request, + options: CallOptions(metadata: TasksAPIServiceClients().getMetaData()), + ); + } +} diff --git a/apps/tasks/lib/services/ward_service.dart b/packages/helpwave_service/lib/src/api/tasks/services/ward_service.dart similarity index 59% rename from apps/tasks/lib/services/ward_service.dart rename to packages/helpwave_service/lib/src/api/tasks/services/ward_service.dart index 73ddcc9c..3d28ca97 100644 --- a/apps/tasks/lib/services/ward_service.dart +++ b/packages/helpwave_service/lib/src/api/tasks/services/ward_service.dart @@ -1,28 +1,27 @@ import 'package:grpc/grpc.dart'; -import 'package:helpwave_proto_dart/proto/services/task_svc/v1/ward_svc.pbgrpc.dart'; -import 'package:tasks/dataclasses/ward.dart'; -import 'package:tasks/services/grpc_client_svc.dart'; +import 'package:helpwave_proto_dart/services/tasks_svc/v1/ward_svc.pbgrpc.dart'; +import 'package:helpwave_service/src/api/tasks/data_types/index.dart'; +import 'package:helpwave_service/src/api/tasks/tasks_api_service_clients.dart'; -/// The GRPC Service for [Ward]s +/// The Service for [Ward]s /// /// Provides queries and requests that load or alter [Ward] objects on the server -/// The server is defined in the underlying [GRPCClientService] +/// The server is defined in the underlying [TasksAPIServiceClients] class WardService { /// The GRPC ServiceClient which handles GRPC - WardServiceClient wardService = GRPCClientService.getWardServiceClient; + WardServiceClient wardService = TasksAPIServiceClients().wardServiceClient; - /// Loads a [Ward] by its identifier - Future getWard({required String id}) async { + /// Loads a [WardMinimal] by its identifier + Future getWard({required String id}) async { GetWardRequest request = GetWardRequest(id: id); GetWardResponse response = await wardService.getWard( request, - options: CallOptions(metadata: GRPCClientService().getTaskServiceMetaData()), + options: CallOptions(metadata: TasksAPIServiceClients().getMetaData()), ); - return Ward( + return WardMinimal( id: response.id, name: response.name, - organizationId: response.organizationId, ); } @@ -31,7 +30,7 @@ class WardService { GetWardOverviewsRequest request = GetWardOverviewsRequest(); GetWardOverviewsResponse response = await wardService.getWardOverviews( request, - options: CallOptions(metadata: GRPCClientService().getTaskServiceMetaData(organizationId: organizationId)), + options: CallOptions(metadata: TasksAPIServiceClients().getMetaData(organizationId: organizationId)), ); return response.wards diff --git a/packages/helpwave_service/lib/src/api/tasks/tasks_api_service_clients.dart b/packages/helpwave_service/lib/src/api/tasks/tasks_api_service_clients.dart new file mode 100644 index 00000000..b2e06333 --- /dev/null +++ b/packages/helpwave_service/lib/src/api/tasks/tasks_api_service_clients.dart @@ -0,0 +1,57 @@ +import 'package:grpc/grpc.dart'; +import 'package:helpwave_proto_dart/services/tasks_svc/v1/ward_svc.pbgrpc.dart'; +import 'package:helpwave_proto_dart/services/tasks_svc/v1/patient_svc.pbgrpc.dart'; +import 'package:helpwave_proto_dart/services/tasks_svc/v1/room_svc.pbgrpc.dart'; +import 'package:helpwave_proto_dart/services/tasks_svc/v1/task_svc.pbgrpc.dart'; +import 'package:helpwave_service/src/api/tasks/offline_clients/patient_offline_client.dart'; +import 'package:helpwave_service/src/api/tasks/offline_clients/ward_offline_client.dart'; +import 'package:helpwave_service/src/auth/index.dart'; + +import 'offline_clients/room_offline_client.dart'; +import 'offline_clients/task_offline_client.dart'; + +/// The Underlying GrpcService it provides other clients and the correct metadata for the requests +class TasksAPIServiceClients { + TasksAPIServiceClients._privateConstructor(); + + static final TasksAPIServiceClients _instance = TasksAPIServiceClients._privateConstructor(); + + factory TasksAPIServiceClients() => _instance; + + /// The api URL used + String? apiUrl; + + bool offlineMode = false; + + ClientChannel get serviceChannel { + assert(apiUrl != null); + return ClientChannel(apiUrl!); + } + + Map getMetaData({String? organizationId}) { + var metaData = { + ...AuthenticationUtility.authMetaData, + "dapr-app-id": "task-svc", + }; + + if (organizationId != null) { + metaData["X-Organization"] = organizationId; + } else { + metaData["X-Organization"] = AuthenticationUtility.fallbackOrganizationId!; + } + + return metaData; + } + + PatientServiceClient get patientServiceClient => + offlineMode ? PatientOfflineClient(serviceChannel) : PatientServiceClient(serviceChannel); + + WardServiceClient get wardServiceClient => + offlineMode ? WardOfflineClient(serviceChannel) : WardServiceClient(serviceChannel); + + RoomServiceClient get roomServiceClient => + offlineMode ? RoomOfflineClient(serviceChannel) : RoomServiceClient(serviceChannel); + + TaskServiceClient get taskServiceClient => + offlineMode ? TaskOfflineClient(serviceChannel) : TaskServiceClient(serviceChannel); +} diff --git a/packages/helpwave_service/lib/src/api/tasks/util/task_status_mapping.dart b/packages/helpwave_service/lib/src/api/tasks/util/task_status_mapping.dart new file mode 100644 index 00000000..97b65819 --- /dev/null +++ b/packages/helpwave_service/lib/src/api/tasks/util/task_status_mapping.dart @@ -0,0 +1,30 @@ +import 'package:helpwave_service/src/api/tasks/data_types/task.dart' as task_lib; +import 'package:helpwave_proto_dart/services/tasks_svc/v1/types.pbenum.dart' as proto; + +class GRPCTypeConverter { + static proto.TaskStatus taskStatusToGRPC(task_lib.TaskStatus status) { + switch (status) { + case task_lib.TaskStatus.todo: + return proto.TaskStatus.TASK_STATUS_TODO; + case task_lib.TaskStatus.inProgress: + return proto.TaskStatus.TASK_STATUS_IN_PROGRESS; + case task_lib.TaskStatus.done: + return proto.TaskStatus.TASK_STATUS_DONE; + case task_lib.TaskStatus.unspecified: + return proto.TaskStatus.TASK_STATUS_UNSPECIFIED; + } + } + + static task_lib.TaskStatus taskStatusFromGRPC(proto.TaskStatus status) { + switch (status) { + case proto.TaskStatus.TASK_STATUS_TODO: + return task_lib.TaskStatus.todo; + case proto.TaskStatus.TASK_STATUS_IN_PROGRESS: + return task_lib.TaskStatus.inProgress; + case proto.TaskStatus.TASK_STATUS_DONE: + return task_lib.TaskStatus.done; + default: + return task_lib.TaskStatus.unspecified; + } + } +} diff --git a/packages/helpwave_service/lib/src/api/user/controllers/index.dart b/packages/helpwave_service/lib/src/api/user/controllers/index.dart new file mode 100644 index 00000000..dcd72ff4 --- /dev/null +++ b/packages/helpwave_service/lib/src/api/user/controllers/index.dart @@ -0,0 +1 @@ +export 'user_controller.dart'; diff --git a/apps/tasks/lib/controllers/user_controller.dart b/packages/helpwave_service/lib/src/api/user/controllers/user_controller.dart similarity index 91% rename from apps/tasks/lib/controllers/user_controller.dart rename to packages/helpwave_service/lib/src/api/user/controllers/user_controller.dart index 518ef281..6d975aa2 100644 --- a/apps/tasks/lib/controllers/user_controller.dart +++ b/packages/helpwave_service/lib/src/api/user/controllers/user_controller.dart @@ -1,7 +1,6 @@ import 'package:flutter/material.dart'; -import 'package:helpwave_widget/loading.dart'; -import 'package:tasks/services/user_service.dart'; -import '../dataclasses/user.dart'; +import 'package:helpwave_util/loading.dart'; +import '../index.dart'; /// The Controller for managing a [User] class UserController extends ChangeNotifier { diff --git a/packages/helpwave_service/lib/src/api/user/data_types/index.dart b/packages/helpwave_service/lib/src/api/user/data_types/index.dart new file mode 100644 index 00000000..7d071f5a --- /dev/null +++ b/packages/helpwave_service/lib/src/api/user/data_types/index.dart @@ -0,0 +1,2 @@ +export 'user.dart'; +export 'organization.dart'; diff --git a/packages/helpwave_service/lib/src/api/user/data_types/organization.dart b/packages/helpwave_service/lib/src/api/user/data_types/organization.dart new file mode 100644 index 00000000..9bc349a6 --- /dev/null +++ b/packages/helpwave_service/lib/src/api/user/data_types/organization.dart @@ -0,0 +1,54 @@ +class OrganizationMinimal { + String id; + String shortName; + String longName; + + OrganizationMinimal({ + required this.id, + required this.shortName, + required this.longName, + }); +} + +/// data class for [Organization] +class Organization extends OrganizationMinimal { + String avatarURL; + String email; + bool isPersonal; + bool isVerified; + + Organization({ + required super.id, + required super.shortName, + required super.longName, + required this.avatarURL, + required this.email, + required this.isPersonal, + required this.isVerified, + }); + + Organization copyWith({ + String? id, + String? shortName, + String? longName, + String? avatarURL, + String? email, + bool? isPersonal, + bool? isVerified, + }) { + return Organization( + id: id ?? this.id, + shortName: shortName ?? this.shortName, + longName: longName ?? this.longName, + avatarURL: avatarURL ?? this.avatarURL, + email: email ?? this.email, + isPersonal: isPersonal ?? this.isPersonal, + isVerified: isVerified ?? this.isVerified, + ); + } + + @override + String toString() { + return "{id: $id, name: $longName, shortName: $shortName}"; + } +} diff --git a/apps/tasks/lib/dataclasses/user.dart b/packages/helpwave_service/lib/src/api/user/data_types/user.dart similarity index 52% rename from apps/tasks/lib/dataclasses/user.dart rename to packages/helpwave_service/lib/src/api/user/data_types/user.dart index 54733fc5..0abd1c09 100644 --- a/apps/tasks/lib/dataclasses/user.dart +++ b/packages/helpwave_service/lib/src/api/user/data_types/user.dart @@ -3,21 +3,29 @@ class User { String id; String name; String nickName; - Uri profile; - - // TODO add email + String email; + Uri profileUrl; User({ required this.id, required this.name, required this.nickName, - required this.profile, + required this.email, + required this.profileUrl, }); - factory User.empty({String id = ""}) => User(id: id, name: "", nickName: "", profile: Uri.base); + factory User.empty({String id = ""}) => User(id: id, name: "", nickName: "", email: "", profileUrl: Uri.base); bool get isCreating => id == ""; + User copyWith({String? name, String? nickName, Uri? profileUrl, String? email}) => User( + id: id, + name: name ?? this.name, + nickName: nickName ?? this.nickName, + profileUrl: profileUrl ?? this.profileUrl, + email: email ?? this.email, + ); + @override bool operator ==(Object other) { if (identical(this, other)) return true; diff --git a/packages/helpwave_service/lib/src/api/user/index.dart b/packages/helpwave_service/lib/src/api/user/index.dart new file mode 100644 index 00000000..8bfb7f1d --- /dev/null +++ b/packages/helpwave_service/lib/src/api/user/index.dart @@ -0,0 +1,4 @@ +export 'data_types/index.dart'; +export 'services/index.dart'; +export 'controllers/index.dart'; +export 'user_api_service_clients.dart'; diff --git a/packages/helpwave_service/lib/src/api/user/offline_clients/organization_offline_client.dart b/packages/helpwave_service/lib/src/api/user/offline_clients/organization_offline_client.dart new file mode 100644 index 00000000..f5173556 --- /dev/null +++ b/packages/helpwave_service/lib/src/api/user/offline_clients/organization_offline_client.dart @@ -0,0 +1,271 @@ +import 'package:grpc/grpc.dart'; +import 'package:helpwave_proto_dart/services/user_svc/v1/organization_svc.pbgrpc.dart'; +import 'package:helpwave_service/src/api/offline/offline_client_store.dart'; +import 'package:helpwave_service/src/api/offline/util.dart'; +import 'package:helpwave_service/user.dart'; + +class OrganizationUpdate { + String id; + String shortName; + String longName; + String email; + bool isPersonal; + String avatarURL; + + OrganizationUpdate({ + required this.id, + required this.shortName, + required this.longName, + required this.email, + required this.isPersonal, + required this.avatarURL, + }); +} + +class OrganizationOfflineClientStore { + List organizations = []; + + Organization? find(String id) { + int index = organizations.indexWhere((org) => org.id == id); + if (index == -1) { + return null; + } + return organizations[index]; + } + + List findOrganizations() { + return organizations; + } + + void create(Organization organization) { + organizations.add(organization); + } + + void update(OrganizationUpdate organizationUpdate) { + bool found = false; + organizations = organizations.map((org) { + if (org.id == organizationUpdate.id) { + found = true; + return org.copyWith( + shortName: organizationUpdate.shortName, + longName: organizationUpdate.longName, + avatarURL: organizationUpdate.avatarURL, + email: organizationUpdate.email, + isPersonal: organizationUpdate.isPersonal); + } + return org; + }).toList(); + + if (!found) { + throw Exception("UpdateOrganization: Could not find organization with id ${organizationUpdate.id}"); + } + } + + void delete(String organizationId) { + organizations.removeWhere((org) => org.id == organizationId); + // Assuming WardOfflineService handles related deletions (Wards, etc.) + // WardOfflineService.deleteByOrganization(organizationId); + } +} + +class OrganizationOfflineClient extends OrganizationServiceClient { + OrganizationOfflineClient(super.channel); + + @override + ResponseFuture createOrganization(CreateOrganizationRequest request, + {CallOptions? options}) { + final newOrganization = Organization( + id: DateTime.now().millisecondsSinceEpoch.toString(), + shortName: request.shortName, + longName: request.longName, + avatarURL: 'https://helpwave.de/favicon.ico', + email: request.contactEmail, + isPersonal: request.isPersonal, + isVerified: true, + ); + + OfflineClientStore().organizationStore.create(newOrganization); + + return MockResponseFuture.value(CreateOrganizationResponse()..id = newOrganization.id); + } + + @override + ResponseFuture createOrganizationForUser(CreateOrganizationForUserRequest request, + {CallOptions? options}) { + final newOrganization = Organization( + id: DateTime.now().millisecondsSinceEpoch.toString(), + shortName: request.shortName, + longName: request.longName, + avatarURL: 'https://helpwave.de/favicon.ico', + email: request.contactEmail, + isPersonal: request.isPersonal, + isVerified: true, + ); + + OfflineClientStore().organizationStore.create(newOrganization); + + return MockResponseFuture.value(CreateOrganizationForUserResponse()..id = newOrganization.id); + } + + @override + ResponseFuture getOrganization(GetOrganizationRequest request, {CallOptions? options}) { + final organization = OfflineClientStore().organizationStore.find(request.id); + + if (organization == null) { + throw Exception("GetOrganization: Could not find organization with id ${request.id}"); + } + + final members = + OfflineClientStore().userStore.findUsers().map((user) => GetOrganizationMember()..userId = user.id).toList(); + + return MockResponseFuture.value(GetOrganizationResponse() + ..id = organization.id + ..shortName = organization.shortName + ..longName = organization.longName + ..avatarUrl = organization.avatarURL + ..contactEmail = organization.email + ..isPersonal = organization.isPersonal + ..members.addAll(members)); + } + + @override + ResponseFuture getOrganizationsByUser(GetOrganizationsByUserRequest request, + {CallOptions? options}) { + final organizations = OfflineClientStore().organizationStore.findOrganizations().map((org) { + final members = OfflineClientStore() + .userStore + .findUsers() + .map((user) => GetOrganizationsByUserResponse_Organization_Member()..userId = user.id) + .toList(); + + return GetOrganizationsByUserResponse_Organization() + ..id = org.id + ..shortName = org.shortName + ..longName = org.longName + ..contactEmail = org.email + ..avatarUrl = org.avatarURL + ..members.addAll(members) + ..isPersonal = org.isPersonal; + }).toList(); + + return MockResponseFuture.value(GetOrganizationsByUserResponse()..organizations.addAll(organizations)); + } + + @override + ResponseFuture getOrganizationsForUser(GetOrganizationsForUserRequest request, {CallOptions? options}) { + final organizations = OfflineClientStore().organizationStore.findOrganizations().map((org) { + final members = OfflineClientStore() + .userStore + .findUsers() + .map((user) => GetOrganizationsForUserResponse_Organization_Member()..userId = user.id) + .toList(); + + return GetOrganizationsForUserResponse_Organization() + ..id = org.id + ..shortName = org.shortName + ..longName = org.longName + ..contactEmail = org.email + ..avatarUrl = org.avatarURL + ..members.addAll(members) + ..isPersonal = org.isPersonal; + }).toList(); + + return MockResponseFuture.value(GetOrganizationsForUserResponse()..organizations.addAll(organizations)); + + } + + @override + ResponseFuture updateOrganization(UpdateOrganizationRequest request, + {CallOptions? options}) { + final update = OrganizationUpdate( + id: request.id, + shortName: request.shortName, + longName: request.longName, + email: request.contactEmail, + avatarURL: request.avatarUrl, + isPersonal: request.isPersonal, + ); + + try { + OfflineClientStore().organizationStore.update(update); + return MockResponseFuture.value(UpdateOrganizationResponse()); + } catch (e) { + return MockResponseFuture.error(e); + } + } + + @override + ResponseFuture deleteOrganization(DeleteOrganizationRequest request, + {CallOptions? options}) { + try { + OfflineClientStore().organizationStore.delete(request.id); + return MockResponseFuture.value(DeleteOrganizationResponse()); + } catch (e) { + return MockResponseFuture.error(e); + } + } + + // Other missing methods + + @override + ResponseFuture getMembersByOrganization(GetMembersByOrganizationRequest request, + {CallOptions? options}) { + final organization = OfflineClientStore().organizationStore.find(request.id); + + if (organization == null) { + throw Exception("GetMembersByOrganization: Could not find organization with id ${request.id}"); + } + + final members = OfflineClientStore() + .userStore + .findUsers() + .map((user) => GetMembersByOrganizationResponse_Member() + ..userId = user.id + ..email = user.email + ..nickname = user.nickName + ..avatarUrl = user.profileUrl.toString()) + .toList(); + + return MockResponseFuture.value(GetMembersByOrganizationResponse()..members.addAll(members)); + } + + @override + ResponseFuture getInvitationsByOrganization(GetInvitationsByOrganizationRequest request, {CallOptions? options}) { + return MockResponseFuture.value(GetInvitationsByOrganizationResponse()); + } + + @override + ResponseFuture getInvitationsByUser(GetInvitationsByUserRequest request, {CallOptions? options}) { + return MockResponseFuture.value(GetInvitationsByUserResponse()); + } + + @override + ResponseFuture inviteMember(InviteMemberRequest request, {CallOptions? options}) { + throw UnimplementedError('Not implemented yet'); + } + + @override + ResponseFuture revokeInvitation(RevokeInvitationRequest request, {CallOptions? options}) { + throw UnimplementedError('Not implemented yet'); + } + + @override + ResponseFuture acceptInvitation(AcceptInvitationRequest request, {CallOptions? options}) { + throw UnimplementedError('Not implemented yet'); + } + + @override + ResponseFuture declineInvitation(DeclineInvitationRequest request, {CallOptions? options}) { + throw UnimplementedError('Not implemented yet'); + } + + @override + ResponseFuture addMember(AddMemberRequest request, {CallOptions? options}) { + throw UnimplementedError('Not implemented yet'); + } + + @override + ResponseFuture removeMember(RemoveMemberRequest request, {CallOptions? options}) { + throw UnimplementedError('Not implemented yet'); + } +} diff --git a/packages/helpwave_service/lib/src/api/user/offline_clients/user_offline_client.dart b/packages/helpwave_service/lib/src/api/user/offline_clients/user_offline_client.dart new file mode 100644 index 00000000..d39c57c6 --- /dev/null +++ b/packages/helpwave_service/lib/src/api/user/offline_clients/user_offline_client.dart @@ -0,0 +1,119 @@ +import 'package:grpc/grpc.dart'; +import 'package:helpwave_service/src/api/offline/offline_client_store.dart'; +import 'package:helpwave_service/src/api/offline/util.dart'; +import 'package:helpwave_service/src/api/user/index.dart'; +import 'package:helpwave_proto_dart/services/user_svc/v1/user_svc.pbgrpc.dart'; + +class UserUpdate { + final String id; + + UserUpdate({required this.id}); +} + +class UserOfflineService { + List users = []; + + User? find(String id) { + int index = users.indexWhere((user) => user.id == id); + if (index == -1) { + return null; + } + return users[index]; + } + + List findUsers() { + return users; + } + + void create(User user) { + users.add(user); + } + + void update(UserUpdate user) { + bool found = false; + + users = users.map((u) { + if (u.id == user.id) { + found = true; + return u.copyWith(); + } + return u; + }).toList(); + + if (!found) { + throw Exception('UpdateUser: Could not find user with id ${user.id}'); + } + } + + void delete(String userId) { + users.removeWhere((u) => u.id == userId); + } +} + +class UserOfflineClient extends UserServiceClient { + UserOfflineClient(super.channel); + + @override + ResponseFuture readPublicProfile(ReadPublicProfileRequest request, + {CallOptions? options}) { + final user = OfflineClientStore().userStore.find(request.id); + if (user == null) { + return MockResponseFuture.error(Exception('ReadPublicProfile: Could not find user with id ${request.id}')); + } + final response = ReadPublicProfileResponse() + ..id = user.id + ..name = user.name + ..nickname = user.nickName + ..avatarUrl = user.profileUrl.toString(); + return MockResponseFuture.value(response); + } + + @override + ResponseFuture readSelf(ReadSelfRequest request, {CallOptions? options}) { + final user = OfflineClientStore().userStore.users[0]; + + final organizations = OfflineClientStore() + .organizationStore + .findOrganizations() + .map((org) => ReadSelfOrganization()..id = org.id) + .toList(); + + final response = ReadSelfResponse() + ..id = user.id + ..name = user.name + ..nickname = user.nickName + ..avatarUrl = user.profileUrl.toString() + ..organizations.addAll(organizations); + + return MockResponseFuture.value(response); + } + + @override + ResponseFuture createUser(CreateUserRequest request, {CallOptions? options}) { + final newUser = User( + id: DateTime.now().millisecondsSinceEpoch.toString(), + name: request.name, + nickName: request.nickname, + email: request.email, + profileUrl: Uri.parse('https://helpwave.de/favicon.ico'), + ); + + OfflineClientStore().userStore.create(newUser); + + final response = CreateUserResponse()..id = newUser.id; + return MockResponseFuture.value(response); + } + + @override + ResponseFuture updateUser(UpdateUserRequest request, {CallOptions? options}) { + final update = UserUpdate(id: request.id); + + try { + OfflineClientStore().userStore.update(update); + final response = UpdateUserResponse(); + return MockResponseFuture.value(response); + } catch (e) { + return MockResponseFuture.error(e); + } + } +} diff --git a/packages/helpwave_service/lib/src/api/user/services/index.dart b/packages/helpwave_service/lib/src/api/user/services/index.dart new file mode 100644 index 00000000..01052c51 --- /dev/null +++ b/packages/helpwave_service/lib/src/api/user/services/index.dart @@ -0,0 +1,2 @@ +export 'organization_svc.dart'; +export 'user_service.dart'; diff --git a/apps/tasks/lib/services/organization_svc.dart b/packages/helpwave_service/lib/src/api/user/services/organization_svc.dart similarity index 56% rename from apps/tasks/lib/services/organization_svc.dart rename to packages/helpwave_service/lib/src/api/user/services/organization_svc.dart index 9db077c9..39c23315 100644 --- a/apps/tasks/lib/services/organization_svc.dart +++ b/packages/helpwave_service/lib/src/api/user/services/organization_svc.dart @@ -1,31 +1,33 @@ import 'package:grpc/grpc.dart'; -import 'package:helpwave_proto_dart/proto/services/user_svc/v1/organization_svc.pbgrpc.dart'; -import 'package:tasks/dataclasses/organization.dart'; -import 'package:tasks/dataclasses/user.dart'; -import 'package:tasks/services/grpc_client_svc.dart'; +import 'package:helpwave_proto_dart/services/user_svc/v1/organization_svc.pbgrpc.dart'; +import 'package:helpwave_service/auth.dart'; +import 'package:helpwave_service/src/api/user/user_api_service_clients.dart'; +import '../data_types/index.dart'; /// The GRPC Service for [Organization]s /// /// Provides queries and requests that load or alter [Organization] objects on the server -/// The server is defined in the underlying [GRPCClientService] +/// The server is defined in the underlying [UserAPIServiceClients] class OrganizationService { /// The GRPC ServiceClient which handles GRPC - OrganizationServiceClient organizationService = GRPCClientService.getOrganizationServiceClient; + OrganizationServiceClient organizationService = UserAPIServiceClients().organizationServiceClient; /// Load a Organization by its identifier Future getOrganization({required String id}) async { GetOrganizationRequest request = GetOrganizationRequest(id: id); GetOrganizationResponse response = await organizationService.getOrganization( request, - options: CallOptions(metadata: GRPCClientService().getUserServiceMetaData(organizationId: id)), + options: CallOptions(metadata: UserAPIServiceClients().getMetaData(organizationId: id)), ); - // TODO use full information of request Organization organization = Organization( - id: response.id, - name: response.longName, - shortName: response.shortName, - ); + id: response.id, + longName: response.longName, + shortName: response.shortName, + avatarURL: response.avatarUrl, + email: response.contactEmail, + isVerified: true, + isPersonal: response.isPersonal); return organization; } @@ -35,19 +37,21 @@ class OrganizationService { GetOrganizationsForUserResponse response = await organizationService.getOrganizationsForUser( request, options: CallOptions( - metadata: GRPCClientService().getUserServiceMetaData( - organizationId: GRPCClientService().fallbackOrganizationId, + metadata: UserAPIServiceClients().getMetaData( + organizationId: AuthenticationUtility.fallbackOrganizationId, ), ), ); List organizations = response.organizations - // TODO use full information of request .map((organization) => Organization( - id: organization.id, - name: organization.longName, - shortName: organization.shortName, - )) + id: organization.id, + longName: organization.longName, + shortName: organization.shortName, + avatarURL: organization.avatarUrl, + email: organization.contactEmail, + isVerified: true, + isPersonal: organization.isPersonal)) .toList(); return organizations; } @@ -58,7 +62,7 @@ class OrganizationService { GetMembersByOrganizationResponse response = await organizationService.getMembersByOrganization( request, options: CallOptions( - metadata: GRPCClientService().getUserServiceMetaData(organizationId: organizationId), + metadata: UserAPIServiceClients().getMetaData(organizationId: organizationId), ), ); @@ -67,7 +71,8 @@ class OrganizationService { id: member.userId, name: member.nickname, // TODO replace this nickName: member.nickname, - profile: Uri.parse(member.avatarUrl), + email: member.email, + profileUrl: Uri.parse(member.avatarUrl), )) .toList(); return users; diff --git a/apps/tasks/lib/services/user_service.dart b/packages/helpwave_service/lib/src/api/user/services/user_service.dart similarity index 51% rename from apps/tasks/lib/services/user_service.dart rename to packages/helpwave_service/lib/src/api/user/services/user_service.dart index fedb8899..943bba4a 100644 --- a/apps/tasks/lib/services/user_service.dart +++ b/packages/helpwave_service/lib/src/api/user/services/user_service.dart @@ -1,15 +1,16 @@ import 'package:grpc/grpc.dart'; -import 'package:helpwave_proto_dart/proto/services/user_svc/v1/user_svc.pbgrpc.dart'; -import 'package:tasks/services/grpc_client_svc.dart'; -import '../dataclasses/user.dart'; +import 'package:helpwave_proto_dart/services/user_svc/v1/user_svc.pbgrpc.dart'; +import 'package:helpwave_service/auth.dart'; +import 'package:helpwave_service/src/api/user/user_api_service_clients.dart'; +import '../data_types/index.dart'; /// The GRPC Service for [User]s /// /// Provides queries and requests that load or alter [User] objects on the server -/// The server is defined in the underlying [GRPCClientService] +/// The server is defined in the underlying [UserAPIServiceClients] class UserService { /// The GRPC ServiceClient which handles GRPC - UserServiceClient userService = GRPCClientService.getUserServiceClient; + UserServiceClient userService = UserAPIServiceClients().userServiceClient; /// Loads the [User]s by it's identifier Future getUser({String? id}) async { @@ -17,8 +18,8 @@ class UserService { ReadPublicProfileResponse response = await userService.readPublicProfile( request, options: CallOptions( - metadata: GRPCClientService().getUserServiceMetaData( - organizationId: GRPCClientService().fallbackOrganizationId, + metadata: UserAPIServiceClients().getMetaData( + organizationId: AuthenticationUtility.fallbackOrganizationId, ), ), ); @@ -27,7 +28,8 @@ class UserService { id: response.id, name: response.name, nickName: response.nickname, - profile: Uri.parse(response.avatarUrl), + email: "no-email", // TODO replace this + profileUrl: Uri.parse(response.avatarUrl), ); } } diff --git a/packages/helpwave_service/lib/src/api/user/user_api_service_clients.dart b/packages/helpwave_service/lib/src/api/user/user_api_service_clients.dart new file mode 100644 index 00000000..e41b01f6 --- /dev/null +++ b/packages/helpwave_service/lib/src/api/user/user_api_service_clients.dart @@ -0,0 +1,46 @@ +import 'package:grpc/grpc.dart'; +import 'package:helpwave_proto_dart/services/user_svc/v1/user_svc.pbgrpc.dart'; +import 'package:helpwave_proto_dart/services/user_svc/v1/organization_svc.pbgrpc.dart'; +import 'package:helpwave_service/src/api/user/offline_clients/organization_offline_client.dart'; +import 'package:helpwave_service/src/api/user/offline_clients/user_offline_client.dart'; +import 'package:helpwave_service/src/auth/index.dart'; + +/// A bundling of all User API services which can be used and are configured +/// +/// Make sure to set the [apiURL] to use the services +class UserAPIServiceClients { + UserAPIServiceClients._privateConstructor(); + + static final UserAPIServiceClients _instance = UserAPIServiceClients._privateConstructor(); + + factory UserAPIServiceClients() => _instance; + + /// The api URL used + String? apiUrl; + + bool offlineMode = false; + + ClientChannel get serviceChannel { + assert(apiUrl != null); + return ClientChannel(apiUrl!); + } + + Map getMetaData({String? organizationId}) { + var metaData = { + ...AuthenticationUtility.authMetaData, + "dapr-app-id": "user-svc", + }; + + if (organizationId != null) { + metaData["X-Organization"] = organizationId; + } + + return metaData; + } + + UserServiceClient get userServiceClient => + offlineMode ? UserOfflineClient(serviceChannel) : UserServiceClient(serviceChannel); + + OrganizationServiceClient get organizationServiceClient => + offlineMode ? OrganizationOfflineClient(serviceChannel) : OrganizationServiceClient(serviceChannel); +} diff --git a/packages/helpwave_service/lib/src/auth/authentication_utility.dart b/packages/helpwave_service/lib/src/auth/authentication_utility.dart new file mode 100644 index 00000000..488e4558 --- /dev/null +++ b/packages/helpwave_service/lib/src/auth/authentication_utility.dart @@ -0,0 +1,18 @@ +import 'package:helpwave_service/auth.dart'; + +class AuthenticationUtility { + static Map get authMetaData { + UserSessionService sessionService = UserSessionService(); + if (sessionService.isLoggedIn) { + return { + "Authorization": "Bearer ${sessionService.identity?.idToken}", + }; + } + // Maybe throw a error instead + return {}; + } + + static String? get fallbackOrganizationId => + // Maybe throw a error instead for the last case + CurrentWardService().currentWard?.organizationId ?? UserSessionService().identity?.firstOrganization; +} diff --git a/apps/tasks/lib/services/current_ward_svc.dart b/packages/helpwave_service/lib/src/auth/current_ward_svc.dart similarity index 92% rename from apps/tasks/lib/services/current_ward_svc.dart rename to packages/helpwave_service/lib/src/auth/current_ward_svc.dart index 8711d471..bca9ab3f 100644 --- a/apps/tasks/lib/services/current_ward_svc.dart +++ b/packages/helpwave_service/lib/src/auth/current_ward_svc.dart @@ -1,10 +1,7 @@ import 'package:flutter/foundation.dart'; import 'package:shared_preferences/shared_preferences.dart'; -import 'package:tasks/config/config.dart'; -import 'package:tasks/dataclasses/organization.dart'; -import 'package:tasks/dataclasses/ward.dart'; -import 'package:tasks/services/organization_svc.dart'; -import 'package:tasks/services/ward_service.dart'; +import 'package:helpwave_service/src/api/tasks/index.dart'; +import 'package:helpwave_service/src/api/user/index.dart'; /// A readonly class for getting the CurrentWard information class CurrentWardInformation { @@ -12,7 +9,7 @@ class CurrentWardInformation { final WardMinimal ward; /// The identifier of the organization - final Organization organization; + final OrganizationMinimal organization; String get wardId => ward.id; @@ -20,11 +17,11 @@ class CurrentWardInformation { String get organizationId => organization.id; - String get organizationName => "${organization.name} (${organization.shortName})"; + String get organizationName => "${organization.longName} (${organization.shortName})"; bool get isEmpty => wardId == "" || organizationId == ""; - bool get hasFullInformation => ward.name != "" && organization.name != ""; + bool get hasFullInformation => ward.name != "" && organization.longName != ""; CurrentWardInformation(this.ward, this.organization); @@ -65,7 +62,7 @@ class _CurrentWardPreferences { if (wardId != null && organizationId != null) { return CurrentWardInformation( WardMinimal(id: wardId, name: ""), - Organization(id: organizationId, name: "", shortName: ""), + OrganizationMinimal(id: organizationId, longName: "", shortName: ""), ); } return null; @@ -76,6 +73,8 @@ class _CurrentWardPreferences { /// /// Changes the [CurrentWardInformation] globally class CurrentWardService extends Listenable { + bool devMode = false; // TODO remove + /// A storage for the current ward final _CurrentWardPreferences _preferences = _CurrentWardPreferences(); @@ -169,7 +168,8 @@ class CurrentWardController extends ChangeNotifier { /// Whether this Controller has been initialized bool get isInitialized => service.isInitialized; - CurrentWardController() { + CurrentWardController({bool devMode = false}) { + service.devMode = devMode; service.addListener(notifyListeners); if (!service.isInitialized) { load(); diff --git a/packages/helpwave_service/lib/src/auth/index.dart b/packages/helpwave_service/lib/src/auth/index.dart new file mode 100644 index 00000000..27d7f07d --- /dev/null +++ b/packages/helpwave_service/lib/src/auth/index.dart @@ -0,0 +1,6 @@ +export 'authentication_service.dart' show AuthenticationService; +export 'identity.dart'; +export 'user_session_controller.dart'; +export 'user_session_service.dart'; +export 'authentication_utility.dart'; +export 'current_ward_svc.dart'; diff --git a/apps/tasks/lib/controllers/user_session_controller.dart b/packages/helpwave_service/lib/src/auth/user_session_controller.dart similarity index 94% rename from apps/tasks/lib/controllers/user_session_controller.dart rename to packages/helpwave_service/lib/src/auth/user_session_controller.dart index 1d44ddf0..f7238230 100644 --- a/apps/tasks/lib/controllers/user_session_controller.dart +++ b/packages/helpwave_service/lib/src/auth/user_session_controller.dart @@ -1,5 +1,5 @@ import 'package:flutter/foundation.dart'; -import 'package:tasks/services/user_session_service.dart'; +import 'package:helpwave_service/src/auth/user_session_service.dart'; /// A Controller for providing and updating the current user session class UserSessionController extends ChangeNotifier { diff --git a/apps/tasks/lib/services/user_session_service.dart b/packages/helpwave_service/lib/src/auth/user_session_service.dart similarity index 87% rename from apps/tasks/lib/services/user_session_service.dart rename to packages/helpwave_service/lib/src/auth/user_session_service.dart index 6df1ee87..1e4a40a7 100644 --- a/apps/tasks/lib/services/user_session_service.dart +++ b/packages/helpwave_service/lib/src/auth/user_session_service.dart @@ -1,6 +1,4 @@ import 'package:helpwave_service/auth.dart'; -import 'package:tasks/config/config.dart'; -import 'package:tasks/services/current_ward_svc.dart'; /// The class for storing an managing the user session class UserSessionService { @@ -10,6 +8,9 @@ class UserSessionService { /// Whether the stored tokens have already been used for authentication bool _hasTriedTokens = false; + /// Whether this service should run in development mode + bool _devMode = false; + final AuthenticationService _authService = AuthenticationService(); static final UserSessionService _userSessionService = UserSessionService._ensureInitialized(); @@ -24,6 +25,11 @@ class UserSessionService { bool get hasTriedTokens => _hasTriedTokens; + bool get devMode => _devMode; + + /// **Only use this** once before using the service + changeMode(bool isDevMode) => _devMode = isDevMode; + /// Logs a User in by using the stored tokens /// /// Sets the [hasTriedTokens] to true diff --git a/packages/helpwave_service/lib/tasks.dart b/packages/helpwave_service/lib/tasks.dart new file mode 100644 index 00000000..fabf21ad --- /dev/null +++ b/packages/helpwave_service/lib/tasks.dart @@ -0,0 +1 @@ +export 'package:helpwave_service/src/api/tasks/index.dart'; diff --git a/packages/helpwave_service/lib/user.dart b/packages/helpwave_service/lib/user.dart new file mode 100644 index 00000000..24d6abf3 --- /dev/null +++ b/packages/helpwave_service/lib/user.dart @@ -0,0 +1 @@ +export 'package:helpwave_service/src/api/user/index.dart'; diff --git a/packages/helpwave_service/pubspec.yaml b/packages/helpwave_service/pubspec.yaml index 1639ab95..cce05101 100644 --- a/packages/helpwave_service/pubspec.yaml +++ b/packages/helpwave_service/pubspec.yaml @@ -2,6 +2,7 @@ name: helpwave_service description: A package for the helpwave services version: 0.0.1 homepage: https://github.com/helpwave/mobile-app +publish_to: none environment: sdk: '>=3.1.0' @@ -16,6 +17,10 @@ dependencies: flutter_secure_storage: 9.0.0 jose: ^0.3.4 logger: ^2.0.2+1 + helpwave_proto_dart: ^0.46.0-336429a + grpc: ^3.2.4 + helpwave_util: + path: "../helpwave_util" dev_dependencies: flutter_test: @@ -24,4 +29,4 @@ dev_dependencies: flutter: - + # flutter config diff --git a/packages/helpwave_theme/lib/src/dark_theme.dart b/packages/helpwave_theme/lib/src/theme/dark_theme.dart similarity index 97% rename from packages/helpwave_theme/lib/src/dark_theme.dart rename to packages/helpwave_theme/lib/src/theme/dark_theme.dart index 423474e1..5002ff66 100644 --- a/packages/helpwave_theme/lib/src/dark_theme.dart +++ b/packages/helpwave_theme/lib/src/theme/dark_theme.dart @@ -1,6 +1,6 @@ import 'package:flutter/material.dart'; -import 'package:helpwave_theme/src/theme.dart'; -import 'constants.dart'; +import 'package:helpwave_theme/src/theme/theme.dart'; +import '../constants.dart'; const primaryColor = Color.fromARGB(255, 255, 255, 255); const onPrimaryColor = Color.fromARGB(255, 0, 0, 0); diff --git a/packages/helpwave_theme/lib/src/light_theme.dart b/packages/helpwave_theme/lib/src/theme/light_theme.dart similarity index 97% rename from packages/helpwave_theme/lib/src/light_theme.dart rename to packages/helpwave_theme/lib/src/theme/light_theme.dart index fc57e12d..72e708cf 100644 --- a/packages/helpwave_theme/lib/src/light_theme.dart +++ b/packages/helpwave_theme/lib/src/theme/light_theme.dart @@ -1,6 +1,6 @@ import 'package:flutter/material.dart'; -import 'package:helpwave_theme/src/theme.dart'; -import 'constants.dart'; +import 'package:helpwave_theme/src/theme/theme.dart'; +import '../constants.dart'; const primaryColor = Color.fromARGB(255, 0, 0, 0); const onPrimaryColor = Color.fromARGB(255, 255, 255, 255); diff --git a/packages/helpwave_theme/lib/src/theme.dart b/packages/helpwave_theme/lib/src/theme/theme.dart similarity index 99% rename from packages/helpwave_theme/lib/src/theme.dart rename to packages/helpwave_theme/lib/src/theme/theme.dart index bb97803f..a9a27fd7 100644 --- a/packages/helpwave_theme/lib/src/theme.dart +++ b/packages/helpwave_theme/lib/src/theme/theme.dart @@ -1,6 +1,6 @@ import 'package:flutter/material.dart'; import 'package:helpwave_util/material_state.dart'; -import '../constants.dart'; +import '../../constants.dart'; // A function to map incoming colors to a theme ThemeData makeTheme({ diff --git a/packages/helpwave_theme/lib/src/util/context_extension.dart b/packages/helpwave_theme/lib/src/util/context_extension.dart new file mode 100644 index 00000000..c5841c77 --- /dev/null +++ b/packages/helpwave_theme/lib/src/util/context_extension.dart @@ -0,0 +1,5 @@ +import 'package:flutter/material.dart'; + +extension BuildContextThemeExtension on BuildContext { + ThemeData get theme => Theme.of(this); +} diff --git a/packages/helpwave_theme/lib/src/util/index.dart b/packages/helpwave_theme/lib/src/util/index.dart new file mode 100644 index 00000000..a4351565 --- /dev/null +++ b/packages/helpwave_theme/lib/src/util/index.dart @@ -0,0 +1,2 @@ +export 'context_extension.dart'; +export 'material_state_color_resolver.dart'; diff --git a/packages/helpwave_theme/lib/src/material_state_color_resolver.dart b/packages/helpwave_theme/lib/src/util/material_state_color_resolver.dart similarity index 100% rename from packages/helpwave_theme/lib/src/material_state_color_resolver.dart rename to packages/helpwave_theme/lib/src/util/material_state_color_resolver.dart diff --git a/packages/helpwave_theme/lib/theme.dart b/packages/helpwave_theme/lib/theme.dart index af2d8684..65327fb0 100644 --- a/packages/helpwave_theme/lib/theme.dart +++ b/packages/helpwave_theme/lib/theme.dart @@ -1,3 +1,3 @@ -export "package:helpwave_theme/src/light_theme.dart" show lightTheme; -export "package:helpwave_theme/src/dark_theme.dart" show darkTheme; +export 'package:helpwave_theme/src/theme/light_theme.dart' show lightTheme; +export 'package:helpwave_theme/src/theme/dark_theme.dart' show darkTheme; export "package:helpwave_theme/src/theme_model.dart"; diff --git a/packages/helpwave_theme/lib/util.dart b/packages/helpwave_theme/lib/util.dart index 38c23504..4ef38e09 100644 --- a/packages/helpwave_theme/lib/util.dart +++ b/packages/helpwave_theme/lib/util.dart @@ -1 +1 @@ -export "package:helpwave_theme/src/material_state_color_resolver.dart"; +export 'package:helpwave_theme/src/util/index.dart'; diff --git a/packages/helpwave_util/lib/lists.dart b/packages/helpwave_util/lib/lists.dart new file mode 100644 index 00000000..da052351 --- /dev/null +++ b/packages/helpwave_util/lib/lists.dart @@ -0,0 +1 @@ +export 'package:helpwave_util/src/lists/range.dart'; diff --git a/packages/helpwave_util/lib/loading.dart b/packages/helpwave_util/lib/loading.dart new file mode 100644 index 00000000..938db0bd --- /dev/null +++ b/packages/helpwave_util/lib/loading.dart @@ -0,0 +1 @@ +export 'package:helpwave_util/src/loading/index.dart'; diff --git a/packages/helpwave_util/lib/search.dart b/packages/helpwave_util/lib/search.dart new file mode 100644 index 00000000..dc90568d --- /dev/null +++ b/packages/helpwave_util/lib/search.dart @@ -0,0 +1 @@ +export 'package:helpwave_util/src/search/search_helpers.dart'; diff --git a/packages/helpwave_util/lib/src/lists/range.dart b/packages/helpwave_util/lib/src/lists/range.dart new file mode 100644 index 00000000..4119bb3a --- /dev/null +++ b/packages/helpwave_util/lib/src/lists/range.dart @@ -0,0 +1,11 @@ +List range(int start, int end, [int step = 1]) { + if (step == 0) { + throw ArgumentError('Step cannot be zero.'); + } + + List result = []; + for (int i = start; (step > 0 ? i < end : i > end); i += step) { + result.add(i); + } + return result; +} diff --git a/packages/helpwave_util/lib/src/loading/index.dart b/packages/helpwave_util/lib/src/loading/index.dart new file mode 100644 index 00000000..e47463c6 --- /dev/null +++ b/packages/helpwave_util/lib/src/loading/index.dart @@ -0,0 +1,2 @@ +export 'type.dart'; +export 'loading_change_notifier.dart'; diff --git a/packages/helpwave_util/lib/src/loading/loading_change_notifier.dart b/packages/helpwave_util/lib/src/loading/loading_change_notifier.dart new file mode 100644 index 00000000..a3fa3782 --- /dev/null +++ b/packages/helpwave_util/lib/src/loading/loading_change_notifier.dart @@ -0,0 +1,53 @@ +import 'package:flutter/cupertino.dart'; +import '../../loading.dart'; + +/// A [ChangeNotifier] that manages a [LoadingState] to indicate to components what state they should show +class LoadingChangeNotifier extends ChangeNotifier { + /// The [LoadingState] of the Controller + LoadingState _state = LoadingState.initializing; + + /// The [LoadingState] of the Controller + LoadingState get state => _state; + + /// The Error, when the Controller is [LoadingState.error] + Object? error; + + /// Function + changeState(LoadingState value) { + _state = value; + notifyListeners(); + } + + LoadingChangeNotifier(); + + Future loadHandler({ + required Future future, + Future Function(Object? error, StackTrace stackTrace)? errorHandler, + }) async { + bool success = false; + defaultErrorHandler(errorObj, _) async { + error = errorObj.toString(); + return false; + } + + changeState(LoadingState.loading); + await future.then((_) { + changeState(LoadingState.loaded); + success = true; + }).onError((error, stackTrace) async { + if (errorHandler != null) { + try { + bool isHandled = await errorHandler(error, stackTrace); + if (isHandled) { + return; + } + } catch (_) {} + } else { + defaultErrorHandler(error, stackTrace); + } + + changeState(LoadingState.error); + }); + return success; + } +} diff --git a/packages/helpwave_util/lib/src/loading/type.dart b/packages/helpwave_util/lib/src/loading/type.dart new file mode 100644 index 00000000..137a5f9b --- /dev/null +++ b/packages/helpwave_util/lib/src/loading/type.dart @@ -0,0 +1,16 @@ +enum LoadingState { + /// The data is initializing + initializing, + + /// The data is loaded + loaded, + + /// The data is currently loading + loading, + + /// Loading the data produced an error + error, + + /// There is no loading state, meaning ignore the LoadingState + unspecified, +} diff --git a/apps/tasks/lib/util/search_helpers.dart b/packages/helpwave_util/lib/src/search/search_helpers.dart similarity index 100% rename from apps/tasks/lib/util/search_helpers.dart rename to packages/helpwave_util/lib/src/search/search_helpers.dart diff --git a/packages/helpwave_widget/lib/content_selection.dart b/packages/helpwave_widget/lib/content_selection.dart index dab610d1..ad24aa53 100644 --- a/packages/helpwave_widget/lib/content_selection.dart +++ b/packages/helpwave_widget/lib/content_selection.dart @@ -1,3 +1 @@ -export 'package:helpwave_widget/src/content_selection/list_search.dart' show ListSearch; -export 'package:helpwave_widget/src/content_selection/content_selector.dart' show ContentSelector; -export 'package:helpwave_widget/src/content_selection/chip_select.dart'; +export 'package:helpwave_widget/src/content_selection/index.dart'; diff --git a/packages/helpwave_widget/lib/src/content_selection/index.dart b/packages/helpwave_widget/lib/src/content_selection/index.dart new file mode 100644 index 00000000..6c033924 --- /dev/null +++ b/packages/helpwave_widget/lib/src/content_selection/index.dart @@ -0,0 +1,4 @@ +export 'chip_select.dart'; +export 'content_selector.dart' show ContentSelector; +export 'list_search.dart'; +export 'list_select.dart'; diff --git a/packages/helpwave_widget/lib/src/content_selection/list_select.dart b/packages/helpwave_widget/lib/src/content_selection/list_select.dart new file mode 100644 index 00000000..1f7e9383 --- /dev/null +++ b/packages/helpwave_widget/lib/src/content_selection/list_select.dart @@ -0,0 +1,28 @@ +import 'dart:async'; +import 'package:flutter/cupertino.dart'; +import 'package:helpwave_widget/loading.dart'; + +class ListSelect extends StatelessWidget { + final FutureOr> items; + + final void Function(T item) onSelect; + + final Widget Function(BuildContext context, T item, Function() select) builder; + + const ListSelect({ + super.key, + required this.items, + required this.onSelect, + required this.builder, + }); + + @override + Widget build(BuildContext context) { + return LoadingFutureBuilder( + data: items, + thenWidgetBuilder: (context, data) => Column( + children: data.map((item) => builder(context, item, () => onSelect(item))).toList(), + ), + ); + } +} diff --git a/packages/helpwave_widget/lib/src/loading/loading_and_error_widget.dart b/packages/helpwave_widget/lib/src/loading/loading_and_error_widget.dart index 51281eb2..1f64ddf1 100644 --- a/packages/helpwave_widget/lib/src/loading/loading_and_error_widget.dart +++ b/packages/helpwave_widget/lib/src/loading/loading_and_error_widget.dart @@ -1,24 +1,8 @@ import 'package:flutter/cupertino.dart'; import 'package:helpwave_theme/constants.dart'; +import 'package:helpwave_util/loading.dart'; import 'package:helpwave_widget/loading.dart'; -enum LoadingState { - /// The date is initializing - initializing, - - /// The date is loaded - loaded, - - /// The date is currently loading - loading, - - /// The loading produced an error - error, - - /// There is no loading state, meaning ignore the LoadingState - unspecified, -} - /// A [Widget] to show different [Widget]s depending on the [LoadingState] class LoadingAndErrorWidget extends StatelessWidget { /// The [LoadingState] is used to determine the shown [Widget] diff --git a/packages/helpwave_widget/lib/src/loading/loading_future_builder.dart b/packages/helpwave_widget/lib/src/loading/loading_future_builder.dart index 7b5f7704..fb7439c0 100644 --- a/packages/helpwave_widget/lib/src/loading/loading_future_builder.dart +++ b/packages/helpwave_widget/lib/src/loading/loading_future_builder.dart @@ -1,24 +1,26 @@ +import 'dart:async'; import 'package:flutter/cupertino.dart'; +import 'package:helpwave_util/loading.dart'; import 'package:helpwave_widget/loading.dart'; /// A Wrapper for the standard [FutureBuilder] to easily distinguish the three /// cases error, loading, then class LoadingFutureBuilder extends StatelessWidget { - /// The [Future] to load - final Future future; + /// The [FutureOr] to load + final FutureOr data; - /// The Builder for the [Widget] upon an successful [Future] + /// The Builder for the [Widget] upon an successful [FutureOr] final Widget Function(BuildContext context, T data) thenWidgetBuilder; - /// The Builder for the [Widget] when loading the [Future] + /// The Builder for the [Widget] when loading the [FutureOr] final Widget loadingWidget; - /// The [Widget] for an error containing [Future] + /// The [Widget] for an error containing [FutureOr] final Widget errorWidget; const LoadingFutureBuilder({ super.key, - required this.future, + required this.data, required this.thenWidgetBuilder, this.loadingWidget = const LoadingSpinner(), this.errorWidget = const LoadErrorWidget(), @@ -26,24 +28,27 @@ class LoadingFutureBuilder extends StatelessWidget { @override Widget build(BuildContext context) { - return FutureBuilder( - future: future, - builder: (context, snapshot) { - LoadingState state = LoadingState.loaded; - if (snapshot.hasError) { - state = LoadingState.error; - } - if (!snapshot.hasData || snapshot.data == null) { - state = LoadingState.loading; - } - return LoadingAndErrorWidget( - state: state, - errorWidget: Center(child: errorWidget), - loadingWidget: Center(child: loadingWidget), - // Safety check because typecast may fail otherwise - child: snapshot.data != null ? thenWidgetBuilder(context, snapshot.data as T) : const SizedBox(), - ); - }, - ); + if(data is Future){ + return FutureBuilder( + future: data as Future, + builder: (context, snapshot) { + LoadingState state = LoadingState.loaded; + if (snapshot.hasError) { + state = LoadingState.error; + } + if (!snapshot.hasData || snapshot.data == null) { + state = LoadingState.loading; + } + return LoadingAndErrorWidget( + state: state, + errorWidget: Center(child: errorWidget), + loadingWidget: Center(child: loadingWidget), + // Safety check because typecast may fail otherwise + child: snapshot.data != null ? thenWidgetBuilder(context, snapshot.data as T) : const SizedBox(), + ); + }, + ); + } + return thenWidgetBuilder(context, data as T); } } diff --git a/packages/helpwave_widget/pubspec.yaml b/packages/helpwave_widget/pubspec.yaml index c0e91e36..bf60e6d9 100644 --- a/packages/helpwave_widget/pubspec.yaml +++ b/packages/helpwave_widget/pubspec.yaml @@ -25,3 +25,4 @@ dev_dependencies: flutter_lints: ^2.0.0 flutter: + # flutter config