diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index a78c7d0..b595079 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -15,7 +15,7 @@ jobs: - name: Install & set Flutter version uses: subosito/flutter-action@v2 with: - flutter-version: '3.10.4' + flutter-version: '3.19.6' channel: 'stable' - name: Get package run: flutter pub get diff --git a/.gitignore b/.gitignore index 0dc729e..32e68d9 100644 --- a/.gitignore +++ b/.gitignore @@ -9,6 +9,7 @@ .history .svn/ migrate_working_dir/ +pubspec.lock # IntelliJ related *.iml diff --git a/assets/translations/en-US.json b/assets/translations/en-US.json index e88ac33..3d0ce7f 100644 --- a/assets/translations/en-US.json +++ b/assets/translations/en-US.json @@ -246,5 +246,69 @@ "new_password": "New Password", "subtitle_new_password": "Please create a new password that you don't use on any other site", "change": "Change", - "create_new_password": "Create new password" + "create_new_password": "Create new password", + "add_manual_track_successfully": "Add manual track successfully", + "add_manual_track": "Add Manual Track", + "project": "Project", + "select_project": "Select project", + "task": "Task", + "select_task": "Select task", + "select_a_project_first": "Select a project first", + "no_data_available": "No data available", + "please_choose_a_task": "Please choose a task", + "start_time": "Start time", + "set_start_time": "Set start time", + "time": "Time", + "date": "Date", + "finish_time": "Finish time", + "set_finish_time": "Set finish time", + "set_start_and_finish_time": "Set start and finish time", + "duration": "Duration", + "please_set_start_time": "Please set start time", + "please_set_finish_time": "Please set finish time", + "title_new_update_available": "New update available", + "description_new_update_available": "Click here to download latest version", + "screenshot_blur": "Screenshot Blur", + "description_screenshot_blur_user": "Set your screenshot to blurred.", + "this_setting_is_override_by_super_admin": "This setting is overrided by super admin.", + "refresh": "Refresh", + "invalid_id_or_user_id": "Invalid ID or user ID", + "subtitle_screenshot_blur": "Control members blur screenshot for security and privacy.", + "enable_all": "Enable all", + "disable_all": "Disable all", + "member_n": { + "zero": "Member", + "one": "Member ({})", + "many": "Members ({})", + "other": "Members ({})" + }, + "user_setting_updated_successfully": "User setting updated successfully", + "override": "Override", + "description_override_member_blur_screenshot": "It will override the individual setting and member cannot change this configuration.", + "start_date": "Start date", + "set_start_date": "Set start date", + "finish_date": "Finish date", + "set_finish_date": "Set finish date", + "please_set_start_date": "Please set start date", + "please_set_finish_date": "Please set finish date", + "finish_date_time_must_be_after_of_start_date_time": "The finish date time must be after the start date time", + "reason": "Reason", + "why_are_you_adding_manual_track": "e.g. Forgot to start timer", + "please_stop_the_timer_if_you_want_to_logout": "Please stop the timer if you want to logout.", + "user_registration": "User Registration", + "subtitle_user_registration": "Configuring approval workflow for user registration.", + "user_registration_workflow_successfully_updated": "User registration workflow successfully updated", + "user_registration_workflow": "User Registration Workflow", + "auto_approval": "Auto Approval", + "description_auto_approval": "New user registration are automatically approved after submitting the registration form.", + "manual_approval": "Manual Approval", + "description_manual_approval": "New user registration are held in a moderation queue and require an super admin to approve them.", + "please_choose_user_registration_workflow": "Please choose user registration workflow", + "url_screenshot_invalid": "URL screenshot invalid", + "screenshot_name_invalid": "Screenshot name invalid", + "download_directory_invalid": "Download directory invalid", + "something_went_wrong_with_message": "Something went wrong with message {}", + "screenshot_downloaded_successfully": "Screenshot downloaded successfully", + "file_screenshot_doesnt_exists": "File screenshot doesn't exists", + "path_file_screenshot_invalid": "Path file screenshot invalid" } \ No newline at end of file diff --git a/devtools_options.yaml b/devtools_options.yaml new file mode 100644 index 0000000..7e7e7f6 --- /dev/null +++ b/devtools_options.yaml @@ -0,0 +1 @@ +extensions: diff --git a/dist/appcast.xml b/dist/appcast.xml index 590b29e..45c3ff4 100644 --- a/dist/appcast.xml +++ b/dist/appcast.xml @@ -5,23 +5,28 @@ en Dipantau - Version 1.2.2 + Version 1.6.0 Hotfix +

Fitur

+

Perbaikan

+ ]]>
- 7 - 1.2.2 + 11 + 1.6.0 - Mon, 07 Aug 2023 10:00:00 +0700 + Sun, 26 Nov 2023 22:00:00 +0700
diff --git a/lib/core/network/network_info.dart b/lib/core/network/network_info.dart index 2223aa1..e004b87 100644 --- a/lib/core/network/network_info.dart +++ b/lib/core/network/network_info.dart @@ -12,6 +12,12 @@ class NetworkInfoImpl implements NetworkInfo { @override Future get isConnected async { final connectivityResult = await connectivity.checkConnectivity(); - return connectivityResult != ConnectivityResult.none; + if (connectivityResult.contains(ConnectivityResult.none)) { + return false; + } else if (connectivityResult.isEmpty) { + return false; + } else { + return true; + } } } diff --git a/lib/core/util/enum/sign_up_method.dart b/lib/core/util/enum/sign_up_method.dart new file mode 100644 index 0000000..a3f9934 --- /dev/null +++ b/lib/core/util/enum/sign_up_method.dart @@ -0,0 +1,26 @@ +enum SignUpMethod { + manual, + auto, +} + +extension SignUpMethodExtension on SignUpMethod { + String toValue() { + switch (this) { + case SignUpMethod.manual: + return 'manual_approval'; + case SignUpMethod.auto: + return 'auto_approval'; + default: + return ''; + } + } + + static SignUpMethod? parseString(String value) { + if (value.contains('manual')) { + return SignUpMethod.manual; + } else if (value.contains('auto')) { + return SignUpMethod.auto; + } + return null; + } +} diff --git a/lib/core/util/helper.dart b/lib/core/util/helper.dart index 78f8d38..a9eab91 100644 --- a/lib/core/util/helper.dart +++ b/lib/core/util/helper.dart @@ -101,4 +101,8 @@ class Helper { return ConstantErrorMessage().failureUnknown; } } + + String removeTrailingSlash(String input) { + return input.replaceAll(RegExp(r'/+$'), ''); + } } diff --git a/lib/core/util/widget_helper.dart b/lib/core/util/widget_helper.dart index e754973..bb1e050 100644 --- a/lib/core/util/widget_helper.dart +++ b/lib/core/util/widget_helper.dart @@ -1,6 +1,7 @@ import 'dart:io'; import 'dart:math'; +import 'package:dio/dio.dart'; import 'package:dipantau_desktop_client/core/util/enum/global_variable.dart'; import 'package:dipantau_desktop_client/core/util/helper.dart'; import 'package:dipantau_desktop_client/core/util/shared_preferences_manager.dart'; @@ -11,6 +12,7 @@ import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:go_router/go_router.dart'; import 'package:path_provider/path_provider.dart'; +import 'package:xml/xml.dart'; class WidgetHelper { void showSnackBar(BuildContext context, String message) { @@ -168,9 +170,9 @@ class WidgetHelper { ], ), style: Theme.of(context).textTheme.bodyMedium?.copyWith( - fontStyle: FontStyle.italic, - fontWeight: FontWeight.w500, - ), + fontStyle: FontStyle.italic, + fontWeight: FontWeight.w500, + ), ), ], ), @@ -209,4 +211,67 @@ class WidgetHelper { return file; } + + Future isNewUpdateAvailable() async { + final response = + await Dio().get('https://raw.githubusercontent.com/CoderJava/dipantau-desktop/main/dist/appcast.xml'); + final data = response.data; + final document = XmlDocument.parse(data); + final sparkleVersion = document.findAllElements('sparkle:version'); + if (sparkleVersion.isNotEmpty) { + final element = sparkleVersion.first; + final versionText = element.innerText; + final newVersion = int.tryParse(versionText); + if (newVersion != null) { + final strBuildNumberLocal = packageInfo.buildNumber; + final buildNumberLocal = int.tryParse(strBuildNumberLocal); + if (buildNumberLocal != null) { + return newVersion > buildNumberLocal; + } + } + } + return false; + } + + Future showDialogConfirmation( + BuildContext context, + String title, + String content, + List actions, + ) { + return showDialog( + context: context, + builder: (context) { + return AlertDialog( + title: Text(title), + content: Text(content), + actions: actions, + ); + }, + ); + } + + Future showDialogMessage( + BuildContext context, + String? title, + String message, + ) { + return showDialog( + context: context, + builder: (context) { + return AlertDialog( + title: Text(title ?? 'info'.tr()), + content: Text(message), + actions: [ + TextButton( + onPressed: () { + context.pop(); + }, + child: Text('dismiss'.tr()), + ), + ], + ); + }, + ); + } } diff --git a/lib/feature/data/datasource/general/general_remote_data_source.dart b/lib/feature/data/datasource/general/general_remote_data_source.dart new file mode 100644 index 0000000..0a98915 --- /dev/null +++ b/lib/feature/data/datasource/general/general_remote_data_source.dart @@ -0,0 +1,36 @@ +import 'package:dio/dio.dart'; +import 'package:dipantau_desktop_client/config/flavor_config.dart'; +import 'package:dipantau_desktop_client/feature/data/model/general/general_response.dart'; + +abstract class GeneralRemoteDataSource { + /// Panggil endpoint [host]/api/ping + /// + /// Throws [DioException] untuk semua error kode + late String pathPing; + + Future ping(String baseUrl); +} + +class GeneralRemoteDataSourceImpl implements GeneralRemoteDataSource { + final Dio dio; + + GeneralRemoteDataSourceImpl({ + required this.dio, + }); + + final baseUrl = FlavorConfig.instance.values.baseUrl; + + @override + String pathPing = ''; + + @override + Future ping(String baseUrl) async { + pathPing = '$baseUrl/api/ping'; + final response = await dio.get(pathPing); + if (response.statusCode.toString().startsWith('2')) { + return GeneralResponse.fromJson(response.data); + } else { + throw DioException(requestOptions: RequestOptions(path: pathPing)); + } + } +} diff --git a/lib/feature/data/datasource/project/project_remote_data_source.dart b/lib/feature/data/datasource/project/project_remote_data_source.dart index a853d4b..749a9f7 100644 --- a/lib/feature/data/datasource/project/project_remote_data_source.dart +++ b/lib/feature/data/datasource/project/project_remote_data_source.dart @@ -2,6 +2,7 @@ import 'package:dio/dio.dart'; import 'package:dipantau_desktop_client/config/base_url_config.dart'; import 'package:dipantau_desktop_client/config/flavor_config.dart'; import 'package:dipantau_desktop_client/feature/data/model/project/project_response.dart'; +import 'package:dipantau_desktop_client/feature/data/model/project_task/project_task_response.dart'; abstract class ProjectRemoteDataSource { /// Panggil endpoint [host]/project/user/:id @@ -12,6 +13,15 @@ abstract class ProjectRemoteDataSource { late String pathGetProject; Future getProject(String userId); + + /// Panggil endpoint [host]/project/user/:id/detail + /// path parameter + /// id - nilai ID user + /// + /// Throws [DioException] untuk semua error kode + late String pathGetProjectTaskByUserId; + + Future getProjectTaskByUserId(String userId); } class ProjectRemoteDataSourceImpl implements ProjectRemoteDataSource { @@ -44,4 +54,25 @@ class ProjectRemoteDataSourceImpl implements ProjectRemoteDataSource { throw DioException(requestOptions: RequestOptions(path: pathGetProject)); } } + + @override + String pathGetProjectTaskByUserId = ''; + + @override + Future getProjectTaskByUserId(String userId) async { + pathGetProjectTaskByUserId = '$baseUrl/user/$userId/detail'; + final response = await dio.get( + pathGetProjectTaskByUserId, + options: Options( + headers: { + baseUrlConfig.requiredToken: true, + }, + ), + ); + if (response.statusCode.toString().startsWith('2')) { + return ProjectTaskResponse.fromJson(response.data); + } else { + throw DioException(requestOptions: RequestOptions(path: pathGetProjectTaskByUserId)); + } + } } diff --git a/lib/feature/data/datasource/setting/setting_remote_data_source.dart b/lib/feature/data/datasource/setting/setting_remote_data_source.dart index fc7b9ca..044fbad 100644 --- a/lib/feature/data/datasource/setting/setting_remote_data_source.dart +++ b/lib/feature/data/datasource/setting/setting_remote_data_source.dart @@ -1,8 +1,11 @@ import 'package:dio/dio.dart'; import 'package:dipantau_desktop_client/config/base_url_config.dart'; import 'package:dipantau_desktop_client/config/flavor_config.dart'; +import 'package:dipantau_desktop_client/feature/data/model/all_user_setting/all_user_setting_response.dart'; import 'package:dipantau_desktop_client/feature/data/model/kv_setting/kv_setting_body.dart'; import 'package:dipantau_desktop_client/feature/data/model/kv_setting/kv_setting_response.dart'; +import 'package:dipantau_desktop_client/feature/data/model/user_setting/user_setting_body.dart'; +import 'package:dipantau_desktop_client/feature/data/model/user_setting/user_setting_response.dart'; abstract class SettingRemoteDataSource { /// Panggil endpoint [host]/setting/key-value @@ -18,6 +21,27 @@ abstract class SettingRemoteDataSource { late String pathSetKvSetting; Future setKvSetting(KvSettingBody body); + + /// Panggil endpoint [host]/setting/user/all + /// + /// Throws [DioException] untuk semua error kode + late String pathGetAllUserSetting; + + Future getAllUserSetting(); + + /// Panggil endpoint [host]/setting/user + /// + /// Throws [DioException] untuk semua error kode + late String pathGetUserSetting; + + Future getUserSetting(); + + /// Panggil endpoint [host]/setting/user + /// + /// Throws [DioException] untuk semua error kode + late String pathUpdateUserSetting; + + Future updateUserSetting(UserSettingBody body); } class SettingRemoteDataSourceImpl implements SettingRemoteDataSource { @@ -72,4 +96,76 @@ class SettingRemoteDataSourceImpl implements SettingRemoteDataSource { throw DioException(requestOptions: RequestOptions(path: pathSetKvSetting)); } } + + @override + String pathGetAllUserSetting = ''; + + @override + Future getAllUserSetting() async { + pathGetAllUserSetting = '$baseUrl/user/all'; + final response = await dio.get( + pathGetAllUserSetting, + options: Options( + headers: { + baseUrlConfig.requiredToken: true, + }, + ), + ); + if (response.statusCode.toString().startsWith('2')) { + return AllUserSettingResponse.fromJson(response.data); + } else { + throw DioException(requestOptions: RequestOptions(path: pathGetAllUserSetting)); + } + } + + @override + String pathGetUserSetting = ''; + + @override + Future getUserSetting() async { + pathGetUserSetting = '$baseUrl/user'; + final response = await dio.get( + pathGetUserSetting, + options: Options( + headers: { + baseUrlConfig.requiredToken: true, + }, + ), + ); + if (response.statusCode.toString().startsWith('2')) { + return UserSettingResponse.fromJson(response.data); + } else { + throw DioException(requestOptions: RequestOptions(path: pathGetUserSetting)); + } + } + + @override + String pathUpdateUserSetting = ''; + + @override + Future updateUserSetting(UserSettingBody body) async { + pathUpdateUserSetting = '$baseUrl/user'; + Map? data; + if (body.isOverrideBlurScreenshot == null) { + data = { + 'data': body.data, + }; + } else { + data = body.toJson(); + } + final response = await dio.post( + pathUpdateUserSetting, + data: data, + options: Options( + headers: { + baseUrlConfig.requiredToken: true, + }, + ), + ); + if (response.statusCode.toString().startsWith('2')) { + return true; + } else { + throw DioException(requestOptions: RequestOptions(path: pathUpdateUserSetting)); + } + } } diff --git a/lib/feature/data/datasource/track/track_remote_data_source.dart b/lib/feature/data/datasource/track/track_remote_data_source.dart index f0cc9e0..62b0143 100644 --- a/lib/feature/data/datasource/track/track_remote_data_source.dart +++ b/lib/feature/data/datasource/track/track_remote_data_source.dart @@ -5,6 +5,7 @@ import 'package:dipantau_desktop_client/feature/data/model/create_track/bulk_cre import 'package:dipantau_desktop_client/feature/data/model/create_track/bulk_create_track_image_body.dart'; import 'package:dipantau_desktop_client/feature/data/model/create_track/create_track_body.dart'; import 'package:dipantau_desktop_client/feature/data/model/general/general_response.dart'; +import 'package:dipantau_desktop_client/feature/data/model/manual_create_track/manual_create_track_body.dart'; import 'package:dipantau_desktop_client/feature/data/model/track_user/track_user_response.dart'; import 'package:dipantau_desktop_client/feature/data/model/track_user_lite/track_user_lite_response.dart'; @@ -50,6 +51,13 @@ abstract class TrackRemoteDataSource { late String pathDeleteTrack; Future deleteTrackUser(int trackId); + + /// Panggil endpoint [host]/track/manual + /// + /// Throws [DioException] untuk semua error kode + late String pathCreateManualTrack; + + Future createManualTrack(ManualCreateTrackBody body); } class TrackRemoteDataSourceImpl implements TrackRemoteDataSource { @@ -222,4 +230,26 @@ class TrackRemoteDataSourceImpl implements TrackRemoteDataSource { throw DioException(requestOptions: RequestOptions(path: pathDeleteTrack)); } } + + @override + String pathCreateManualTrack = ''; + + @override + Future createManualTrack(ManualCreateTrackBody body) async { + pathCreateManualTrack = '$baseUrl/manual'; + final response = await dio.post( + pathCreateManualTrack, + data: body.toJson(), + options: Options( + headers: { + baseUrlConfig.requiredToken: true, + }, + ), + ); + if (response.statusCode.toString().startsWith('2')) { + return GeneralResponse.fromJson(response.data); + } else { + throw DioException(requestOptions: RequestOptions(path: pathCreateManualTrack)); + } + } } diff --git a/lib/feature/data/model/all_user_setting/all_user_setting_response.dart b/lib/feature/data/model/all_user_setting/all_user_setting_response.dart new file mode 100644 index 0000000..e035d3a --- /dev/null +++ b/lib/feature/data/model/all_user_setting/all_user_setting_response.dart @@ -0,0 +1,33 @@ +import 'package:dipantau_desktop_client/feature/data/model/user_setting/user_setting_response.dart'; +import 'package:equatable/equatable.dart'; +import 'package:json_annotation/json_annotation.dart'; + +part 'all_user_setting_response.g.dart'; + +@JsonSerializable() +class AllUserSettingResponse extends Equatable { + @JsonKey(name: 'data') + final List? data; + @JsonKey(name: 'is_override_blur_screenshot') + final bool? isOverrideBlurScreenshot; + + AllUserSettingResponse({ + required this.data, + required this.isOverrideBlurScreenshot, + }); + + factory AllUserSettingResponse.fromJson(Map json) => _$AllUserSettingResponseFromJson(json); + + Map toJson() => _$AllUserSettingResponseToJson(this); + + @override + List get props => [ + data, + isOverrideBlurScreenshot, + ]; + + @override + String toString() { + return 'AllUserSettingResponse{data: $data, isOverrideBlurScreenshot: $isOverrideBlurScreenshot}'; + } +} diff --git a/lib/feature/data/model/kv_setting/kv_setting_body.dart b/lib/feature/data/model/kv_setting/kv_setting_body.dart index 18fc296..1492d9b 100644 --- a/lib/feature/data/model/kv_setting/kv_setting_body.dart +++ b/lib/feature/data/model/kv_setting/kv_setting_body.dart @@ -7,8 +7,13 @@ part 'kv_setting_body.g.dart'; class KvSettingBody extends Equatable { @JsonKey(name: 'discord_channel_id') final String? discordChannelId; + @JsonKey(name: 'sign_up_method') + final String? signUpMethod; - KvSettingBody({required this.discordChannelId}); + KvSettingBody({ + required this.discordChannelId, + required this.signUpMethod, + }); factory KvSettingBody.fromJson(Map json) => _$KvSettingBodyFromJson(json); @@ -16,11 +21,12 @@ class KvSettingBody extends Equatable { @override List get props => [ - discordChannelId, - ]; + discordChannelId, + signUpMethod, + ]; @override String toString() { - return 'KvSettingBody{discordChannelId: $discordChannelId}'; + return 'KvSettingBody{discordChannelId: $discordChannelId, signUpMethod: $signUpMethod}'; } -} \ No newline at end of file +} diff --git a/lib/feature/data/model/kv_setting/kv_setting_response.dart b/lib/feature/data/model/kv_setting/kv_setting_response.dart index b04dd82..d3ae0df 100644 --- a/lib/feature/data/model/kv_setting/kv_setting_response.dart +++ b/lib/feature/data/model/kv_setting/kv_setting_response.dart @@ -7,9 +7,12 @@ part 'kv_setting_response.g.dart'; class KvSettingResponse extends Equatable { @JsonKey(name: 'discord_channel_id') final String? discordChannelId; + @JsonKey(name: 'sign_up_method') + final String? signUpMethod; KvSettingResponse({ required this.discordChannelId, + required this.signUpMethod, }); factory KvSettingResponse.fromJson(Map json) => _$KvSettingResponseFromJson(json); @@ -19,10 +22,11 @@ class KvSettingResponse extends Equatable { @override List get props => [ discordChannelId, + signUpMethod, ]; @override String toString() { - return 'KvSettingResponse{discordChannelId: $discordChannelId}'; + return 'KvSettingResponse{discordChannelId: $discordChannelId, signUpMethod: $signUpMethod}'; } } diff --git a/lib/feature/data/model/manual_create_track/manual_create_track_body.dart b/lib/feature/data/model/manual_create_track/manual_create_track_body.dart new file mode 100644 index 0000000..7a95a19 --- /dev/null +++ b/lib/feature/data/model/manual_create_track/manual_create_track_body.dart @@ -0,0 +1,45 @@ +import 'package:equatable/equatable.dart'; +import 'package:json_annotation/json_annotation.dart'; + +part 'manual_create_track_body.g.dart'; + +@JsonSerializable() +class ManualCreateTrackBody extends Equatable { + @JsonKey(name: 'task_id') + final int taskId; + @JsonKey(name: 'start_date') + final String startDate; + @JsonKey(name: 'finish_date') + final String finishDate; + @JsonKey(name: 'duration') + final int duration; + @JsonKey(name: 'note') + final String? note; + + ManualCreateTrackBody({ + required this.taskId, + required this.startDate, + required this.finishDate, + required this.duration, + required this.note, + }); + + factory ManualCreateTrackBody.fromJson(Map json) => _$ManualCreateTrackBodyFromJson(json); + + Map toJson() => _$ManualCreateTrackBodyToJson(this); + + @override + List get props => [ + taskId, + startDate, + finishDate, + duration, + note, + ]; + + @override + String toString() { + return 'ManualCreateTrackBody{taskId: $taskId, startDate: $startDate, finishDate: $finishDate, duration: $duration, ' + 'note: $note}'; + } +} diff --git a/lib/feature/data/model/project_task/project_task_response.dart b/lib/feature/data/model/project_task/project_task_response.dart new file mode 100644 index 0000000..a0f05bc --- /dev/null +++ b/lib/feature/data/model/project_task/project_task_response.dart @@ -0,0 +1,89 @@ +import 'package:equatable/equatable.dart'; +import 'package:json_annotation/json_annotation.dart'; + +part 'project_task_response.g.dart'; + +@JsonSerializable() +class ProjectTaskResponse extends Equatable { + @JsonKey(name: 'data') + final List? data; + + ProjectTaskResponse({ + required this.data, + }); + + factory ProjectTaskResponse.fromJson(Map json) => _$ProjectTaskResponseFromJson(json); + + Map toJson() => _$ProjectTaskResponseToJson(this); + + @override + List get props => [ + data, + ]; + + @override + String toString() { + return 'ProjectTaskResponse{data: $data}'; + } +} + +@JsonSerializable() +class ItemProjectTaskResponse extends Equatable { + @JsonKey(name: 'project_id') + final int? projectId; + @JsonKey(name: 'project_name') + final String? projectName; + @JsonKey(name: 'tasks') + final List tasks; + + ItemProjectTaskResponse({ + required this.projectId, + required this.projectName, + required this.tasks, + }); + + factory ItemProjectTaskResponse.fromJson(Map json) => _$ItemProjectTaskResponseFromJson(json); + + Map toJson() => _$ItemProjectTaskResponseToJson(this); + + @override + List get props => [ + projectId, + projectName, + tasks, + ]; + + @override + String toString() { + return 'ItemProjectTaskResponse{projectId: $projectId, projectName: $projectName, tasks: $tasks}'; + } +} + +@JsonSerializable() +class ItemTaskLiteProjectTaskResponse extends Equatable { + @JsonKey(name: 'id') + final int? id; + @JsonKey(name: 'name') + final String? name; + + ItemTaskLiteProjectTaskResponse({ + required this.id, + required this.name, + }); + + factory ItemTaskLiteProjectTaskResponse.fromJson(Map json) => + _$ItemTaskLiteProjectTaskResponseFromJson(json); + + Map toJson() => _$ItemTaskLiteProjectTaskResponseToJson(this); + + @override + List get props => [ + id, + name, + ]; + + @override + String toString() { + return 'ItemTaskLiteProjectTaskResponse{id: $id, name: $name}'; + } +} diff --git a/lib/feature/data/model/track_user/track_user_response.dart b/lib/feature/data/model/track_user/track_user_response.dart index ef758c9..318ca0a 100644 --- a/lib/feature/data/model/track_user/track_user_response.dart +++ b/lib/feature/data/model/track_user/track_user_response.dart @@ -53,6 +53,8 @@ class ItemTrackUserResponse extends Equatable { final String? user; @JsonKey(name: 'files') final List? files; + @JsonKey(name: 'note') + final String? note; ItemTrackUserResponse({ required this.id, @@ -67,6 +69,7 @@ class ItemTrackUserResponse extends Equatable { required this.userId, required this.user, required this.files, + required this.note, }); factory ItemTrackUserResponse.fromJson(Map json) => _$ItemTrackUserResponseFromJson(json); @@ -87,6 +90,7 @@ class ItemTrackUserResponse extends Equatable { userId, user, files, + note, ]; @override @@ -94,7 +98,7 @@ class ItemTrackUserResponse extends Equatable { return 'ItemTrackUserResponse{id: $id, taskId: $taskId, taskName: $taskName, projectId: $projectId, ' 'projectName: $projectName, startDate: $startDate, finishDate: $finishDate, ' 'activityInPercent: $activityInPercent, durationInSeconds: $durationInSeconds, userId: $userId, user: $user, ' - 'files: $files}'; + 'files: $files, note: $note}'; } } @@ -106,11 +110,17 @@ class ItemFileTrackUserResponse extends Equatable { final String? url; @JsonKey(name: 'size') final int? sizeInByte; + @JsonKey(name: 'url_blur') + final String? urlBlur; + @JsonKey(name: 'size_blur') + final int? sizeBlurInByte; ItemFileTrackUserResponse({ required this.id, required this.url, required this.sizeInByte, + required this.urlBlur, + required this.sizeBlurInByte, }); factory ItemFileTrackUserResponse.fromJson(Map json) => _$ItemFileTrackUserResponseFromJson(json); @@ -122,10 +132,13 @@ class ItemFileTrackUserResponse extends Equatable { id, url, sizeInByte, + urlBlur, + sizeBlurInByte, ]; @override String toString() { - return 'ItemFileTrackUserResponse{id: $id, url: $url, sizeInByte: $sizeInByte}'; + return 'ItemFileTrackUserResponse{id: $id, url: $url, sizeInByte: $sizeInByte, urlBlur: $urlBlur, ' + 'sizeBlurInByte: $sizeBlurInByte}'; } } diff --git a/lib/feature/data/model/user_setting/user_setting_body.dart b/lib/feature/data/model/user_setting/user_setting_body.dart new file mode 100644 index 0000000..0dd5ad1 --- /dev/null +++ b/lib/feature/data/model/user_setting/user_setting_body.dart @@ -0,0 +1,64 @@ +import 'package:equatable/equatable.dart'; +import 'package:json_annotation/json_annotation.dart'; + +part 'user_setting_body.g.dart'; + +@JsonSerializable() +class UserSettingBody extends Equatable { + @JsonKey(name: 'data') + final List data; + @JsonKey(name: 'is_override_blur_screenshot') + final bool? isOverrideBlurScreenshot; + + UserSettingBody({ + required this.data, + required this.isOverrideBlurScreenshot, + }); + + factory UserSettingBody.fromJson(Map json) => _$UserSettingBodyFromJson(json); + + Map toJson() => _$UserSettingBodyToJson(this); + + @override + List get props => [ + data, + isOverrideBlurScreenshot, + ]; + + @override + String toString() { + return 'UserSettingBody{data: $data, isOverrideBlurScreenshot: $isOverrideBlurScreenshot}'; + } +} + +@JsonSerializable() +class ItemUserSettingBody extends Equatable { + @JsonKey(name: 'id') + final int id; + @JsonKey(name: 'is_enable_blur_screenshot') + final bool isEnableBlurScreenshot; + @JsonKey(name: 'user_id') + final int userId; + + ItemUserSettingBody({ + required this.id, + required this.isEnableBlurScreenshot, + required this.userId, + }); + + factory ItemUserSettingBody.fromJson(Map json) => _$ItemUserSettingBodyFromJson(json); + + Map toJson() => _$ItemUserSettingBodyToJson(this); + + @override + List get props => [ + id, + isEnableBlurScreenshot, + userId, + ]; + + @override + String toString() { + return 'ItemUserSettingBody{id: $id, isEnableBlurScreenshot: $isEnableBlurScreenshot, userId: $userId}'; + } +} diff --git a/lib/feature/data/model/user_setting/user_setting_response.dart b/lib/feature/data/model/user_setting/user_setting_response.dart new file mode 100644 index 0000000..cd8eeb9 --- /dev/null +++ b/lib/feature/data/model/user_setting/user_setting_response.dart @@ -0,0 +1,45 @@ +import 'package:equatable/equatable.dart'; +import 'package:json_annotation/json_annotation.dart'; + +part 'user_setting_response.g.dart'; + +@JsonSerializable() +class UserSettingResponse extends Equatable { + @JsonKey(name: 'id') + final int? id; + @JsonKey(name: 'is_enable_blur_screenshot') + final bool? isEnableBlurScreenshot; + @JsonKey(name: 'user_id') + final int? userId; + @JsonKey(name: 'name') + final String? name; + @JsonKey(name: 'is_override_blur_screenshot') + final bool? isOverrideBlurScreenshot; + + UserSettingResponse({ + required this.id, + required this.isEnableBlurScreenshot, + required this.userId, + required this.name, + required this.isOverrideBlurScreenshot, + }); + + factory UserSettingResponse.fromJson(Map json) => _$UserSettingResponseFromJson(json); + + Map toJson() => _$UserSettingResponseToJson(this); + + @override + List get props => [ + id, + isEnableBlurScreenshot, + userId, + name, + isOverrideBlurScreenshot, + ]; + + @override + String toString() { + return 'UserSettingResponse{id: $id, isEnableBlurScreenshot: $isEnableBlurScreenshot, userId: $userId, name: $name, ' + 'isOverrideBlurScreenshot: $isOverrideBlurScreenshot}'; + } +} diff --git a/lib/feature/data/repository/general/general_repository_impl.dart b/lib/feature/data/repository/general/general_repository_impl.dart new file mode 100644 index 0000000..be2ad2e --- /dev/null +++ b/lib/feature/data/repository/general/general_repository_impl.dart @@ -0,0 +1,56 @@ +import 'package:dio/dio.dart'; +import 'package:dipantau_desktop_client/core/error/failure.dart'; +import 'package:dipantau_desktop_client/core/network/network_info.dart'; +import 'package:dipantau_desktop_client/feature/data/datasource/general/general_remote_data_source.dart'; +import 'package:dipantau_desktop_client/feature/data/model/general/general_response.dart'; +import 'package:dipantau_desktop_client/feature/domain/repository/general/general_repository.dart'; + +class GeneralRepositoryImpl implements GeneralRepository { + final GeneralRemoteDataSource remoteDataSource; + final NetworkInfo networkInfo; + + GeneralRepositoryImpl({ + required this.remoteDataSource, + required this.networkInfo, + }); + + String getErrorMessageFromEndpoint(dynamic dynamicErrorMessage, String httpErrorMessage, int? statusCode) { + if (dynamicErrorMessage is Map && dynamicErrorMessage.containsKey('message')) { + return '$statusCode ${dynamicErrorMessage['message']}'; + } else if (dynamicErrorMessage is String) { + return httpErrorMessage; + } else { + return httpErrorMessage; + } + } + + @override + Future<({Failure? failure, GeneralResponse? response})> ping(String baseUrl) async { + Failure? failure; + GeneralResponse? response; + final isConnected = await networkInfo.isConnected; + if (isConnected) { + try { + response = await remoteDataSource.ping(baseUrl); + } on DioException catch (error) { + final message = error.message ?? error.toString(); + if (error.response == null) { + failure = ServerFailure(message); + } else { + final errorMessage = getErrorMessageFromEndpoint( + error.response?.data, + message, + error.response?.statusCode, + ); + failure = ServerFailure(errorMessage); + } + } on TypeError catch (error) { + final errorMessage = error.toString(); + failure = ParsingFailure(errorMessage); + } + } else { + failure = ConnectionFailure(); + } + return (failure: failure, response: response); + } +} diff --git a/lib/feature/data/repository/project/project_repository_impl.dart b/lib/feature/data/repository/project/project_repository_impl.dart index 0012c5f..32a1f80 100644 --- a/lib/feature/data/repository/project/project_repository_impl.dart +++ b/lib/feature/data/repository/project/project_repository_impl.dart @@ -4,6 +4,7 @@ import 'package:dipantau_desktop_client/core/error/failure.dart'; import 'package:dipantau_desktop_client/core/network/network_info.dart'; import 'package:dipantau_desktop_client/feature/data/datasource/project/project_remote_data_source.dart'; import 'package:dipantau_desktop_client/feature/data/model/project/project_response.dart'; +import 'package:dipantau_desktop_client/feature/data/model/project_task/project_task_response.dart'; import 'package:dipantau_desktop_client/feature/domain/repository/project/project_repository.dart'; class ProjectRepositoryImpl implements ProjectRepository { @@ -51,4 +52,34 @@ class ProjectRepositoryImpl implements ProjectRepository { return Left(ConnectionFailure()); } } + + @override + Future<({Failure? failure, ProjectTaskResponse? response})> getProjectTaskByUserId(String userId) async { + Failure? failure; + ProjectTaskResponse? response; + final isConnected = await networkInfo.isConnected; + if (isConnected) { + try { + response = await remoteDataSource.getProjectTaskByUserId(userId); + } on DioException catch (error) { + final message = error.message ?? error.toString(); + if (error.response == null) { + failure = ServerFailure(message); + } else { + final errorMessage = getErrorMessageFromEndpoint( + error.response?.data, + message, + error.response?.statusCode, + ); + failure = ServerFailure(errorMessage); + } + } on TypeError catch (error) { + final errorMessage = error.toString(); + failure = ParsingFailure(errorMessage); + } + } else { + failure = ConnectionFailure(); + } + return (failure: failure, response: response); + } } diff --git a/lib/feature/data/repository/setting/setting_repository_impl.dart b/lib/feature/data/repository/setting/setting_repository_impl.dart index e63ed03..7f6bea1 100644 --- a/lib/feature/data/repository/setting/setting_repository_impl.dart +++ b/lib/feature/data/repository/setting/setting_repository_impl.dart @@ -2,8 +2,11 @@ import 'package:dio/dio.dart'; import 'package:dipantau_desktop_client/core/error/failure.dart'; import 'package:dipantau_desktop_client/core/network/network_info.dart'; import 'package:dipantau_desktop_client/feature/data/datasource/setting/setting_remote_data_source.dart'; +import 'package:dipantau_desktop_client/feature/data/model/all_user_setting/all_user_setting_response.dart'; import 'package:dipantau_desktop_client/feature/data/model/kv_setting/kv_setting_body.dart'; import 'package:dipantau_desktop_client/feature/data/model/kv_setting/kv_setting_response.dart'; +import 'package:dipantau_desktop_client/feature/data/model/user_setting/user_setting_body.dart'; +import 'package:dipantau_desktop_client/feature/data/model/user_setting/user_setting_response.dart'; import 'package:dipantau_desktop_client/feature/domain/repository/setting/setting_repository.dart'; class SettingRepositoryImpl implements SettingRepository { @@ -84,4 +87,94 @@ class SettingRepositoryImpl implements SettingRepository { } return (failure: failure, response: response); } + + @override + Future<({Failure? failure, AllUserSettingResponse? response})> getAllUserSetting() async { + Failure? failure; + AllUserSettingResponse? response; + final isConnected = await networkInfo.isConnected; + if (isConnected) { + try { + response = await remoteDataSource.getAllUserSetting(); + } on DioException catch (error) { + final message = error.message ?? error.toString(); + if (error.response == null) { + failure = ServerFailure(message); + } else { + final errorMessage = getErrorMessageFromEndpoint( + error.response?.data, + message, + error.response?.statusCode, + ); + failure = ServerFailure(errorMessage); + } + } on TypeError catch (error) { + final errorMessage = error.toString(); + failure = ParsingFailure(errorMessage); + } + } else { + failure = ConnectionFailure(); + } + return (failure: failure, response: response); + } + + @override + Future<({Failure? failure, UserSettingResponse? response})> getUserSetting() async { + Failure? failure; + UserSettingResponse? response; + final isConnected = await networkInfo.isConnected; + if (isConnected) { + try { + response = await remoteDataSource.getUserSetting(); + } on DioException catch (error) { + final message = error.message ?? error.toString(); + if (error.response == null) { + failure = ServerFailure(message); + } else { + final errorMessage = getErrorMessageFromEndpoint( + error.response?.data, + message, + error.response?.statusCode, + ); + failure = ServerFailure(errorMessage); + } + } on TypeError catch (error) { + final errorMessage = error.toString(); + failure = ParsingFailure(errorMessage); + } + } else { + failure = ConnectionFailure(); + } + return (failure: failure, response: response); + } + + @override + Future<({Failure? failure, bool? response})> updateUserSetting(UserSettingBody body) async { + Failure? failure; + bool? response; + final isConnected = await networkInfo.isConnected; + if (isConnected) { + try { + response = await remoteDataSource.updateUserSetting(body); + } on DioException catch (error) { + final message = error.message ?? error.toString(); + if (error.response == null) { + failure = ServerFailure(message); + } else { + final errorMessage = getErrorMessageFromEndpoint( + error.response?.data, + message, + error.response?.statusCode, + ); + failure = ServerFailure(errorMessage); + } + } on TypeError catch (error) { + final errorMessage = error.toString(); + failure = ParsingFailure(errorMessage); + } + } else { + failure = ConnectionFailure(); + } + return (failure: failure, response: response); + } } diff --git a/lib/feature/data/repository/track/track_repository_impl.dart b/lib/feature/data/repository/track/track_repository_impl.dart index 0477605..26aa3d1 100644 --- a/lib/feature/data/repository/track/track_repository_impl.dart +++ b/lib/feature/data/repository/track/track_repository_impl.dart @@ -7,6 +7,7 @@ import 'package:dipantau_desktop_client/feature/data/model/create_track/bulk_cre import 'package:dipantau_desktop_client/feature/data/model/create_track/bulk_create_track_image_body.dart'; import 'package:dipantau_desktop_client/feature/data/model/create_track/create_track_body.dart'; import 'package:dipantau_desktop_client/feature/data/model/general/general_response.dart'; +import 'package:dipantau_desktop_client/feature/data/model/manual_create_track/manual_create_track_body.dart'; import 'package:dipantau_desktop_client/feature/data/model/track_user/track_user_response.dart'; import 'package:dipantau_desktop_client/feature/data/model/track_user_lite/track_user_lite_response.dart'; import 'package:dipantau_desktop_client/feature/domain/repository/track/track_repository.dart'; @@ -206,4 +207,34 @@ class TrackRepositoryImpl implements TrackRepository { } return (failure: failure, response: response); } + + @override + Future<({Failure? failure, GeneralResponse? response})> createManualTrack(ManualCreateTrackBody body) async { + Failure? failure; + GeneralResponse? response; + final isConnected = await networkInfo.isConnected; + if (isConnected) { + try { + response = await remoteDataSource.createManualTrack(body); + } on DioException catch (error) { + final message = error.message ?? error.toString(); + if (error.response == null) { + failure = ServerFailure(message); + } else { + final errorMessage = getErrorMessageFromEndpoint( + error.response?.data, + message, + error.response?.statusCode, + ); + failure = ServerFailure(errorMessage); + } + } on TypeError catch (error) { + final errorMessage = error.toString(); + failure = ParsingFailure(errorMessage); + } + } else { + failure = ConnectionFailure(); + } + return (failure: failure, response: response); + } } diff --git a/lib/feature/domain/repository/general/general_repository.dart b/lib/feature/domain/repository/general/general_repository.dart new file mode 100644 index 0000000..3b26c36 --- /dev/null +++ b/lib/feature/domain/repository/general/general_repository.dart @@ -0,0 +1,6 @@ +import 'package:dipantau_desktop_client/core/error/failure.dart'; +import 'package:dipantau_desktop_client/feature/data/model/general/general_response.dart'; + +abstract class GeneralRepository { + Future<({Failure? failure, GeneralResponse? response})> ping(String baseUrl); +} \ No newline at end of file diff --git a/lib/feature/domain/repository/project/project_repository.dart b/lib/feature/domain/repository/project/project_repository.dart index 2038608..0baa870 100644 --- a/lib/feature/domain/repository/project/project_repository.dart +++ b/lib/feature/domain/repository/project/project_repository.dart @@ -1,7 +1,10 @@ import 'package:dartz/dartz.dart'; import 'package:dipantau_desktop_client/core/error/failure.dart'; import 'package:dipantau_desktop_client/feature/data/model/project/project_response.dart'; +import 'package:dipantau_desktop_client/feature/data/model/project_task/project_task_response.dart'; abstract class ProjectRepository { Future> getProject(String userId); + + Future<({Failure? failure, ProjectTaskResponse? response})> getProjectTaskByUserId(String userId); } \ No newline at end of file diff --git a/lib/feature/domain/repository/setting/setting_repository.dart b/lib/feature/domain/repository/setting/setting_repository.dart index 87472b7..b70225a 100644 --- a/lib/feature/domain/repository/setting/setting_repository.dart +++ b/lib/feature/domain/repository/setting/setting_repository.dart @@ -1,9 +1,18 @@ import 'package:dipantau_desktop_client/core/error/failure.dart'; +import 'package:dipantau_desktop_client/feature/data/model/all_user_setting/all_user_setting_response.dart'; import 'package:dipantau_desktop_client/feature/data/model/kv_setting/kv_setting_body.dart'; import 'package:dipantau_desktop_client/feature/data/model/kv_setting/kv_setting_response.dart'; +import 'package:dipantau_desktop_client/feature/data/model/user_setting/user_setting_body.dart'; +import 'package:dipantau_desktop_client/feature/data/model/user_setting/user_setting_response.dart'; abstract class SettingRepository { Future<({Failure? failure, KvSettingResponse? response})> getKvSetting(); Future<({Failure? failure, bool? response})> setKvSetting(KvSettingBody body); + + Future<({Failure? failure, AllUserSettingResponse? response})> getAllUserSetting(); + + Future<({Failure? failure, UserSettingResponse? response})> getUserSetting(); + + Future<({Failure? failure, bool? response})> updateUserSetting(UserSettingBody body); } \ No newline at end of file diff --git a/lib/feature/domain/repository/track/track_repository.dart b/lib/feature/domain/repository/track/track_repository.dart index 68e2c63..40a22f3 100644 --- a/lib/feature/domain/repository/track/track_repository.dart +++ b/lib/feature/domain/repository/track/track_repository.dart @@ -4,6 +4,7 @@ import 'package:dipantau_desktop_client/feature/data/model/create_track/bulk_cre import 'package:dipantau_desktop_client/feature/data/model/create_track/bulk_create_track_image_body.dart'; import 'package:dipantau_desktop_client/feature/data/model/create_track/create_track_body.dart'; import 'package:dipantau_desktop_client/feature/data/model/general/general_response.dart'; +import 'package:dipantau_desktop_client/feature/data/model/manual_create_track/manual_create_track_body.dart'; import 'package:dipantau_desktop_client/feature/data/model/track_user/track_user_response.dart'; import 'package:dipantau_desktop_client/feature/data/model/track_user_lite/track_user_lite_response.dart'; @@ -19,4 +20,6 @@ abstract class TrackRepository { Future<({Failure? failure, TrackUserResponse? response})> getTrackUser(String userId, String date); Future<({Failure? failure, GeneralResponse? response})> deleteTrackUser(int trackId); + + Future<({Failure? failure, GeneralResponse? response})> createManualTrack(ManualCreateTrackBody body); } \ No newline at end of file diff --git a/lib/feature/domain/usecase/create_manual_track/create_manual_track.dart b/lib/feature/domain/usecase/create_manual_track/create_manual_track.dart new file mode 100644 index 0000000..843ba0a --- /dev/null +++ b/lib/feature/domain/usecase/create_manual_track/create_manual_track.dart @@ -0,0 +1,33 @@ +import 'package:dipantau_desktop_client/core/error/failure.dart'; +import 'package:dipantau_desktop_client/core/usecase/usecase.dart'; +import 'package:dipantau_desktop_client/feature/data/model/general/general_response.dart'; +import 'package:dipantau_desktop_client/feature/data/model/manual_create_track/manual_create_track_body.dart'; +import 'package:dipantau_desktop_client/feature/domain/repository/track/track_repository.dart'; +import 'package:equatable/equatable.dart'; + +class CreateManualTrack implements UseCaseRecords { + final TrackRepository repository; + + CreateManualTrack({required this.repository}); + + @override + Future<({Failure? failure, GeneralResponse? response})> call(ParamsCreateManualTrack params) async { + return repository.createManualTrack(params.body); + } +} + +class ParamsCreateManualTrack extends Equatable { + final ManualCreateTrackBody body; + + ParamsCreateManualTrack({required this.body}); + + @override + List get props => [ + body, + ]; + + @override + String toString() { + return 'ParamsCreateManualTrack{body: $body}'; + } +} diff --git a/lib/feature/domain/usecase/get_all_user_setting/get_all_user_setting.dart b/lib/feature/domain/usecase/get_all_user_setting/get_all_user_setting.dart new file mode 100644 index 0000000..c65894c --- /dev/null +++ b/lib/feature/domain/usecase/get_all_user_setting/get_all_user_setting.dart @@ -0,0 +1,15 @@ +import 'package:dipantau_desktop_client/core/error/failure.dart'; +import 'package:dipantau_desktop_client/core/usecase/usecase.dart'; +import 'package:dipantau_desktop_client/feature/data/model/all_user_setting/all_user_setting_response.dart'; +import 'package:dipantau_desktop_client/feature/domain/repository/setting/setting_repository.dart'; + +class GetAllUserSetting implements UseCaseRecords { + final SettingRepository repository; + + GetAllUserSetting({required this.repository}); + + @override + Future<({Failure? failure, AllUserSettingResponse? response})> call(NoParams params) { + return repository.getAllUserSetting(); + } +} \ No newline at end of file diff --git a/lib/feature/domain/usecase/get_project_task_by_user_id/get_project_task_by_user_id.dart b/lib/feature/domain/usecase/get_project_task_by_user_id/get_project_task_by_user_id.dart new file mode 100644 index 0000000..66c8fd8 --- /dev/null +++ b/lib/feature/domain/usecase/get_project_task_by_user_id/get_project_task_by_user_id.dart @@ -0,0 +1,32 @@ +import 'package:dipantau_desktop_client/core/error/failure.dart'; +import 'package:dipantau_desktop_client/core/usecase/usecase.dart'; +import 'package:dipantau_desktop_client/feature/data/model/project_task/project_task_response.dart'; +import 'package:dipantau_desktop_client/feature/domain/repository/project/project_repository.dart'; +import 'package:equatable/equatable.dart'; + +class GetProjectTaskByUserId implements UseCaseRecords { + final ProjectRepository repository; + + GetProjectTaskByUserId({required this.repository}); + + @override + Future<({Failure? failure, ProjectTaskResponse? response})> call(ParamsGetProjectTaskByUserId params) async { + return repository.getProjectTaskByUserId(params.userId); + } +} + +class ParamsGetProjectTaskByUserId extends Equatable { + final String userId; + + ParamsGetProjectTaskByUserId({required this.userId}); + + @override + List get props => [ + userId, + ]; + + @override + String toString() { + return 'ParamsGetProjectTaskByUserId{userId: $userId}'; + } +} diff --git a/lib/feature/domain/usecase/get_user_setting/get_user_setting.dart b/lib/feature/domain/usecase/get_user_setting/get_user_setting.dart new file mode 100644 index 0000000..e610561 --- /dev/null +++ b/lib/feature/domain/usecase/get_user_setting/get_user_setting.dart @@ -0,0 +1,15 @@ +import 'package:dipantau_desktop_client/core/error/failure.dart'; +import 'package:dipantau_desktop_client/core/usecase/usecase.dart'; +import 'package:dipantau_desktop_client/feature/data/model/user_setting/user_setting_response.dart'; +import 'package:dipantau_desktop_client/feature/domain/repository/setting/setting_repository.dart'; + +class GetUserSetting implements UseCaseRecords { + final SettingRepository repository; + + GetUserSetting({required this.repository}); + + @override + Future<({Failure? failure, UserSettingResponse? response})> call(NoParams params) { + return repository.getUserSetting(); + } +} \ No newline at end of file diff --git a/lib/feature/domain/usecase/ping/ping.dart b/lib/feature/domain/usecase/ping/ping.dart new file mode 100644 index 0000000..3c28a00 --- /dev/null +++ b/lib/feature/domain/usecase/ping/ping.dart @@ -0,0 +1,34 @@ +import 'package:dipantau_desktop_client/core/error/failure.dart'; +import 'package:dipantau_desktop_client/core/usecase/usecase.dart'; +import 'package:dipantau_desktop_client/feature/data/model/general/general_response.dart'; +import 'package:dipantau_desktop_client/feature/domain/repository/general/general_repository.dart'; +import 'package:equatable/equatable.dart'; + +class Ping implements UseCaseRecords { + final GeneralRepository repository; + + Ping({required this.repository}); + + @override + Future<({Failure? failure, GeneralResponse? response})> call(ParamsPing params) { + return repository.ping(params.baseUrl); + } +} + +class ParamsPing extends Equatable { + final String baseUrl; + + ParamsPing({ + required this.baseUrl, + }); + + @override + List get props => [ + baseUrl, + ]; + + @override + String toString() { + return 'ParamsPing{baseUrl: $baseUrl}'; + } +} diff --git a/lib/feature/domain/usecase/update_user_setting/update_user_setting.dart b/lib/feature/domain/usecase/update_user_setting/update_user_setting.dart new file mode 100644 index 0000000..ee775cc --- /dev/null +++ b/lib/feature/domain/usecase/update_user_setting/update_user_setting.dart @@ -0,0 +1,34 @@ +import 'package:dipantau_desktop_client/core/error/failure.dart'; +import 'package:dipantau_desktop_client/core/usecase/usecase.dart'; +import 'package:dipantau_desktop_client/feature/data/model/user_setting/user_setting_body.dart'; +import 'package:dipantau_desktop_client/feature/domain/repository/setting/setting_repository.dart'; +import 'package:equatable/equatable.dart'; + +class UpdateUserSetting implements UseCaseRecords { + final SettingRepository repository; + + UpdateUserSetting({required this.repository}); + + @override + Future<({Failure? failure, bool? response})> call(ParamsUpdateUserSetting params) { + return repository.updateUserSetting(params.body); + } +} + +class ParamsUpdateUserSetting extends Equatable { + final UserSettingBody body; + + ParamsUpdateUserSetting({ + required this.body, + }); + + @override + List get props => [ + body, + ]; + + @override + String toString() { + return 'ParamsUpdateUserSetting{body: $body}'; + } +} diff --git a/lib/feature/presentation/bloc/manual_tracking/manual_tracking_bloc.dart b/lib/feature/presentation/bloc/manual_tracking/manual_tracking_bloc.dart new file mode 100644 index 0000000..f1e7854 --- /dev/null +++ b/lib/feature/presentation/bloc/manual_tracking/manual_tracking_bloc.dart @@ -0,0 +1,70 @@ +import 'dart:async'; + +import 'package:bloc/bloc.dart'; +import 'package:dipantau_desktop_client/core/util/helper.dart'; +import 'package:dipantau_desktop_client/feature/data/model/manual_create_track/manual_create_track_body.dart'; +import 'package:dipantau_desktop_client/feature/data/model/project_task/project_task_response.dart'; +import 'package:dipantau_desktop_client/feature/domain/usecase/create_manual_track/create_manual_track.dart'; +import 'package:dipantau_desktop_client/feature/domain/usecase/get_project_task_by_user_id/get_project_task_by_user_id.dart'; + +part 'manual_tracking_event.dart'; + +part 'manual_tracking_state.dart'; + +class ManualTrackingBloc extends Bloc { + final Helper helper; + final CreateManualTrack createManualTrack; + final GetProjectTaskByUserId getProjectTaskByUserId; + + ManualTrackingBloc({ + required this.helper, + required this.createManualTrack, + required this.getProjectTaskByUserId, + }) : super(InitialManualTrackingState()) { + on(_onCreateManualTrackingEvent); + + on(_onLoadDataProjectTaskManualTrackingEvent); + } + + FutureOr _onCreateManualTrackingEvent( + CreateManualTrackingEvent event, + Emitter emit, + ) async { + emit(LoadingManualTrackingState()); + final result = await createManualTrack( + ParamsCreateManualTrack( + body: event.body, + ), + ); + final response = result.response; + final failure = result.failure; + if (response != null) { + emit(SuccessCreateManualTrackingState()); + return; + } + + final errorMessage = helper.getErrorMessageFromFailure(failure); + emit(FailureManualTrackingState(errorMessage: errorMessage)); + } + + FutureOr _onLoadDataProjectTaskManualTrackingEvent( + LoadDataProjectTaskManualTrackingEvent event, + Emitter emit, + ) async { + emit(LoadingManualTrackingState()); + final result = await getProjectTaskByUserId( + ParamsGetProjectTaskByUserId( + userId: event.userId, + ), + ); + final response = result.response; + final failure = result.failure; + if (response != null) { + emit(SuccessLoadDataProjectTaskManualTrackingState(response: response)); + return; + } + + final errorMessage = helper.getErrorMessageFromFailure(failure); + emit(FailureCenterManualTrackingState(errorMessage: errorMessage)); + } +} diff --git a/lib/feature/presentation/bloc/manual_tracking/manual_tracking_event.dart b/lib/feature/presentation/bloc/manual_tracking/manual_tracking_event.dart new file mode 100644 index 0000000..af2ccdc --- /dev/null +++ b/lib/feature/presentation/bloc/manual_tracking/manual_tracking_event.dart @@ -0,0 +1,29 @@ +part of 'manual_tracking_bloc.dart'; + +abstract class ManualTrackingEvent {} + +class CreateManualTrackingEvent extends ManualTrackingEvent { + final ManualCreateTrackBody body; + + CreateManualTrackingEvent({ + required this.body, + }); + + @override + String toString() { + return 'CreateManualTrackingEvent{body: $body}'; + } +} + +class LoadDataProjectTaskManualTrackingEvent extends ManualTrackingEvent { + final String userId; + + LoadDataProjectTaskManualTrackingEvent({ + required this.userId, + }); + + @override + String toString() { + return 'LoadDataProjectTaskManualTrackingEvent{userId: $userId}'; + } +} diff --git a/lib/feature/presentation/bloc/manual_tracking/manual_tracking_state.dart b/lib/feature/presentation/bloc/manual_tracking/manual_tracking_state.dart new file mode 100644 index 0000000..2294187 --- /dev/null +++ b/lib/feature/presentation/bloc/manual_tracking/manual_tracking_state.dart @@ -0,0 +1,44 @@ +part of 'manual_tracking_bloc.dart'; + +abstract class ManualTrackingState {} + +class InitialManualTrackingState extends ManualTrackingState {} + +class LoadingManualTrackingState extends ManualTrackingState {} + +class FailureManualTrackingState extends ManualTrackingState { + final String errorMessage; + + FailureManualTrackingState({required this.errorMessage}); + + @override + String toString() { + return 'FailureManualTrackingState{errorMessage: $errorMessage}'; + } +} + +class FailureCenterManualTrackingState extends ManualTrackingState { + final String errorMessage; + + FailureCenterManualTrackingState({required this.errorMessage}); + + @override + String toString() { + return 'FailureCenterManualTrackingState{errorMessage: $errorMessage}'; + } +} + +class SuccessCreateManualTrackingState extends ManualTrackingState {} + +class SuccessLoadDataProjectTaskManualTrackingState extends ManualTrackingState { + final ProjectTaskResponse response; + + SuccessLoadDataProjectTaskManualTrackingState({ + required this.response, + }); + + @override + String toString() { + return 'SuccessLoadDataProjectTaskManualTrackingState{response: $response}'; + } +} diff --git a/lib/feature/presentation/bloc/setting/setting_bloc.dart b/lib/feature/presentation/bloc/setting/setting_bloc.dart index 5e30504..c4379a6 100644 --- a/lib/feature/presentation/bloc/setting/setting_bloc.dart +++ b/lib/feature/presentation/bloc/setting/setting_bloc.dart @@ -3,10 +3,16 @@ import 'dart:async'; import 'package:bloc/bloc.dart'; import 'package:dipantau_desktop_client/core/usecase/usecase.dart'; import 'package:dipantau_desktop_client/core/util/helper.dart'; +import 'package:dipantau_desktop_client/feature/data/model/all_user_setting/all_user_setting_response.dart'; import 'package:dipantau_desktop_client/feature/data/model/kv_setting/kv_setting_body.dart'; import 'package:dipantau_desktop_client/feature/data/model/kv_setting/kv_setting_response.dart'; +import 'package:dipantau_desktop_client/feature/data/model/user_setting/user_setting_body.dart'; +import 'package:dipantau_desktop_client/feature/data/model/user_setting/user_setting_response.dart'; +import 'package:dipantau_desktop_client/feature/domain/usecase/get_all_user_setting/get_all_user_setting.dart'; import 'package:dipantau_desktop_client/feature/domain/usecase/get_kv_setting/get_kv_setting.dart'; +import 'package:dipantau_desktop_client/feature/domain/usecase/get_user_setting/get_user_setting.dart'; import 'package:dipantau_desktop_client/feature/domain/usecase/set_kv_setting/set_kv_setting.dart'; +import 'package:dipantau_desktop_client/feature/domain/usecase/update_user_setting/update_user_setting.dart'; part 'setting_event.dart'; @@ -16,15 +22,27 @@ class SettingBloc extends Bloc { final Helper helper; final GetKvSetting getKvSetting; final SetKvSetting setKvSetting; + final GetUserSetting getUserSetting; + final GetAllUserSetting getAllUserSetting; + final UpdateUserSetting updateUserSetting; SettingBloc({ required this.helper, required this.getKvSetting, required this.setKvSetting, + required this.getUserSetting, + required this.getAllUserSetting, + required this.updateUserSetting, }) : super(InitialSettingState()) { on(_onLoadKvSettingEvent); on(_onUpdateKvSettingEvent); + + on(_onLoadUserSettingEvent); + + on(_onLoadAllUserSettingEvent); + + on(_onUpdateUserSettingEvent); } FutureOr _onLoadKvSettingEvent( @@ -57,4 +75,46 @@ class SettingBloc extends Bloc { final errorMessage = helper.getErrorMessageFromFailure(failure); emit(FailureSnackBarSettingState(errorMessage: errorMessage)); } + + FutureOr _onLoadUserSettingEvent(LoadUserSettingEvent event, Emitter emit) async { + emit(LoadingCenterSettingState()); + await Future.delayed(const Duration(milliseconds: 500)); + final (:response, :failure) = await getUserSetting(NoParams()); + if (response != null) { + emit(SuccessLoadUserSettingState(response: response)); + return; + } + + final errorMessage = helper.getErrorMessageFromFailure(failure); + emit(FailureSettingState(errorMessage: errorMessage)); + } + + FutureOr _onLoadAllUserSettingEvent(LoadAllUserSettingEvent event, Emitter emit) async { + emit(LoadingCenterSettingState()); + final (:response, :failure) = await getAllUserSetting(NoParams()); + if (response != null) { + emit(SuccessLoadAllUserSettingState(response: response)); + return; + } + + final errorMessage = helper.getErrorMessageFromFailure(failure); + emit(FailureSettingState(errorMessage: errorMessage)); + } + + FutureOr _onUpdateUserSettingEvent(UpdateUserSettingEvent event, Emitter emit) async { + emit(LoadingButtonSettingState()); + await Future.delayed(const Duration(milliseconds: 500)); + final (:response, :failure) = await updateUserSetting( + ParamsUpdateUserSetting( + body: event.body, + ), + ); + if (response != null) { + emit(SuccessUpdateUserSettingState()); + return; + } + + final errorMessage = helper.getErrorMessageFromFailure(failure); + emit(FailureSnackBarSettingState(errorMessage: errorMessage)); + } } diff --git a/lib/feature/presentation/bloc/setting/setting_event.dart b/lib/feature/presentation/bloc/setting/setting_event.dart index c97aae7..d341975 100644 --- a/lib/feature/presentation/bloc/setting/setting_event.dart +++ b/lib/feature/presentation/bloc/setting/setting_event.dart @@ -13,4 +13,21 @@ class UpdateKvSettingEvent extends SettingEvent { String toString() { return 'UpdateKvSettingEvent{body: $body}'; } -} \ No newline at end of file +} + +class LoadUserSettingEvent extends SettingEvent {} + +class LoadAllUserSettingEvent extends SettingEvent {} + +class UpdateUserSettingEvent extends SettingEvent { + final UserSettingBody body; + + UpdateUserSettingEvent({ + required this.body, + }); + + @override + String toString() { + return 'UpdateUserSettingEvent{body: $body}'; + } +} diff --git a/lib/feature/presentation/bloc/setting/setting_state.dart b/lib/feature/presentation/bloc/setting/setting_state.dart index a9a943f..94090f1 100644 --- a/lib/feature/presentation/bloc/setting/setting_state.dart +++ b/lib/feature/presentation/bloc/setting/setting_state.dart @@ -42,3 +42,31 @@ class SuccessLoadKvSettingState extends SettingState { } class SuccessUpdateKvSettingState extends SettingState {} + +class SuccessLoadUserSettingState extends SettingState { + final UserSettingResponse response; + + SuccessLoadUserSettingState({ + required this.response, + }); + + @override + String toString() { + return 'SuccessLoadUserSettingState{response: $response}'; + } +} + +class SuccessLoadAllUserSettingState extends SettingState { + final AllUserSettingResponse response; + + SuccessLoadAllUserSettingState({ + required this.response, + }); + + @override + String toString() { + return 'SuccessLoadAllUserSettingState{response: $response}'; + } +} + +class SuccessUpdateUserSettingState extends SettingState {} diff --git a/lib/feature/presentation/bloc/setup_credential/setup_credential_bloc.dart b/lib/feature/presentation/bloc/setup_credential/setup_credential_bloc.dart new file mode 100644 index 0000000..e089437 --- /dev/null +++ b/lib/feature/presentation/bloc/setup_credential/setup_credential_bloc.dart @@ -0,0 +1,43 @@ +import 'dart:async'; + +import 'package:bloc/bloc.dart'; +import 'package:dipantau_desktop_client/core/util/helper.dart'; +import 'package:dipantau_desktop_client/feature/domain/usecase/ping/ping.dart'; + +part 'setup_credential_event.dart'; + +part 'setup_credential_state.dart'; + +class SetupCredentialBloc extends Bloc { + final Helper helper; + final Ping ping; + + SetupCredentialBloc({ + required this.helper, + required this.ping, + }) : super(InitialSetupCredentialState()) { + on(_onPingSetupCredentialEvent); + } + + FutureOr _onPingSetupCredentialEvent( + PingSetupCredentialEvent event, + Emitter emit, + ) async { + final baseUrl = event.baseUrl; + emit(LoadingSetupCredentialState()); + final result = await ping( + ParamsPing( + baseUrl: baseUrl, + ), + ); + final response = result.response; + final failure = result.failure; + if (response != null) { + emit(SuccessPingSetupCredentialState(baseUrl: baseUrl)); + return; + } + + final errorMessage = helper.getErrorMessageFromFailure(failure); + emit(FailureSetupCredentialState(errorMessage: errorMessage)); + } +} diff --git a/lib/feature/presentation/bloc/setup_credential/setup_credential_event.dart b/lib/feature/presentation/bloc/setup_credential/setup_credential_event.dart new file mode 100644 index 0000000..07219b2 --- /dev/null +++ b/lib/feature/presentation/bloc/setup_credential/setup_credential_event.dart @@ -0,0 +1,16 @@ +part of 'setup_credential_bloc.dart'; + +abstract class SetupCredentialEvent {} + +class PingSetupCredentialEvent extends SetupCredentialEvent { + final String baseUrl; + + PingSetupCredentialEvent({ + required this.baseUrl, + }); + + @override + String toString() { + return 'PingSetupCredentialEvent{baseUrl: $baseUrl}'; + } +} diff --git a/lib/feature/presentation/bloc/setup_credential/setup_credential_state.dart b/lib/feature/presentation/bloc/setup_credential/setup_credential_state.dart new file mode 100644 index 0000000..f2127f6 --- /dev/null +++ b/lib/feature/presentation/bloc/setup_credential/setup_credential_state.dart @@ -0,0 +1,33 @@ +part of 'setup_credential_bloc.dart'; + +abstract class SetupCredentialState {} + +class InitialSetupCredentialState extends SetupCredentialState {} + +class LoadingSetupCredentialState extends SetupCredentialState {} + +class FailureSetupCredentialState extends SetupCredentialState { + final String errorMessage; + + FailureSetupCredentialState({ + required this.errorMessage, + }); + + @override + String toString() { + return 'FailureSetupCredentialState{errorMessage: $errorMessage}'; + } +} + +class SuccessPingSetupCredentialState extends SetupCredentialState { + final String baseUrl; + + SuccessPingSetupCredentialState({ + required this.baseUrl, + }); + + @override + String toString() { + return 'SuccessPingSetupCredentialState{baseUrl: $baseUrl}'; + } +} diff --git a/lib/feature/presentation/page/add_member/add_edit_member_page.dart b/lib/feature/presentation/page/add_member/add_edit_member_page.dart index d56c50e..f9383df 100644 --- a/lib/feature/presentation/page/add_member/add_edit_member_page.dart +++ b/lib/feature/presentation/page/add_member/add_edit_member_page.dart @@ -24,9 +24,9 @@ class AddEditMemberPage extends StatefulWidget { final UserProfileResponse? defaultValue; const AddEditMemberPage({ - Key? key, + super.key, this.defaultValue, - }) : super(key: key); + }); @override State createState() => _AddEditMemberPageState(); diff --git a/lib/feature/presentation/page/edit_profile/edit_profile_page.dart b/lib/feature/presentation/page/edit_profile/edit_profile_page.dart index bddae23..83a26d9 100644 --- a/lib/feature/presentation/page/edit_profile/edit_profile_page.dart +++ b/lib/feature/presentation/page/edit_profile/edit_profile_page.dart @@ -22,7 +22,7 @@ class EditProfilePage extends StatefulWidget { static const routePath = '/edit-profile'; static const routeName = 'edit-profile'; - const EditProfilePage({Key? key}) : super(key: key); + const EditProfilePage({super.key}); @override State createState() => _EditProfilePageState(); @@ -132,12 +132,12 @@ class _EditProfilePageState extends State { if (state is LoadingCenterUserProfileState) { return const WidgetCustomCircularProgressIndicator(); } else if (state is FailureUserProfileState) { - final errorMessage = state.errorMessage; + final errorMessage = state.errorMessage.convertErrorMessageToHumanMessage(); return Padding( padding: EdgeInsets.symmetric(horizontal: helper.getDefaultPaddingLayout), child: WidgetError( title: 'oops'.tr(), - message: errorMessage, + message: errorMessage.hideResponseCode(), onTryAgain: doLoadData, ), ); diff --git a/lib/feature/presentation/page/error/error_page.dart b/lib/feature/presentation/page/error/error_page.dart index 1e1c4c8..74b5713 100644 --- a/lib/feature/presentation/page/error/error_page.dart +++ b/lib/feature/presentation/page/error/error_page.dart @@ -1,7 +1,7 @@ import 'package:flutter/material.dart'; class ErrorPage extends StatelessWidget { - const ErrorPage({Key? key}) : super(key: key); + const ErrorPage({super.key}); @override Widget build(BuildContext context) { diff --git a/lib/feature/presentation/page/forgot_password/forgot_password_page.dart b/lib/feature/presentation/page/forgot_password/forgot_password_page.dart index 731d4ae..f982afe 100644 --- a/lib/feature/presentation/page/forgot_password/forgot_password_page.dart +++ b/lib/feature/presentation/page/forgot_password/forgot_password_page.dart @@ -19,9 +19,9 @@ class ForgotPasswordPage extends StatefulWidget { final String? email; const ForgotPasswordPage({ - Key? key, + super.key, required this.email, - }) : super(key: key); + }); @override State createState() => _ForgotPasswordPageState(); diff --git a/lib/feature/presentation/page/home/home_page.dart b/lib/feature/presentation/page/home/home_page.dart index a51f043..ce38b18 100644 --- a/lib/feature/presentation/page/home/home_page.dart +++ b/lib/feature/presentation/page/home/home_page.dart @@ -1,6 +1,7 @@ import 'dart:async'; import 'dart:io'; +import 'package:auto_updater/auto_updater.dart'; import 'package:dipantau_desktop_client/core/network/network_info.dart'; import 'package:dipantau_desktop_client/core/util/enum/global_variable.dart'; import 'package:dipantau_desktop_client/core/util/helper.dart'; @@ -8,6 +9,7 @@ import 'package:dipantau_desktop_client/core/util/images.dart'; import 'package:dipantau_desktop_client/core/util/notification_helper.dart'; import 'package:dipantau_desktop_client/core/util/platform_channel_helper.dart'; import 'package:dipantau_desktop_client/core/util/shared_preferences_manager.dart'; +import 'package:dipantau_desktop_client/core/util/string_extension.dart'; import 'package:dipantau_desktop_client/core/util/widget_helper.dart'; import 'package:dipantau_desktop_client/feature/data/model/create_track/bulk_create_track_data_body.dart'; import 'package:dipantau_desktop_client/feature/data/model/create_track/bulk_create_track_image_body.dart'; @@ -23,6 +25,7 @@ import 'package:dipantau_desktop_client/feature/presentation/bloc/home/home_bloc import 'package:dipantau_desktop_client/feature/presentation/bloc/tracking/tracking_bloc.dart'; import 'package:dipantau_desktop_client/feature/presentation/bloc/user_profile/user_profile_bloc.dart'; import 'package:dipantau_desktop_client/feature/presentation/page/edit_profile/edit_profile_page.dart'; +import 'package:dipantau_desktop_client/feature/presentation/page/manual_tracking/manual_tracking_page.dart'; import 'package:dipantau_desktop_client/feature/presentation/page/report_screenshot/report_screenshot_page.dart'; import 'package:dipantau_desktop_client/feature/presentation/page/setting/setting_page.dart'; import 'package:dipantau_desktop_client/feature/presentation/page/sync/sync_page.dart'; @@ -42,12 +45,13 @@ import 'package:tray_manager/tray_manager.dart'; import 'package:window_manager/window_manager.dart'; var countTimeReminderTrackInSeconds = 0; +var isGlobalTimerStart = false; class HomePage extends StatefulWidget { static const routePath = '/home'; static const routeName = 'home'; - const HomePage({Key? key}) : super(key: key); + const HomePage({super.key}); @override State createState() => _HomePageState(); @@ -75,11 +79,11 @@ class _HomePageState extends State with TrayListener, WindowListener { final listTrackLocal = []; final listPathStartScreenshots = []; final networkInfo = sl(); + final valueNotifierShowBannerUpdate = ValueNotifier(false); var isWindowVisible = true; var userId = ''; var email = ''; - var isTimerStart = false; var isTimerStartTemp = false; TrackUserLiteResponse? trackUserLite; ItemProjectResponse? selectedProject; @@ -125,10 +129,14 @@ class _HomePageState extends State with TrayListener, WindowListener { final appDatabase = await sl.getAsync(); trackDao = appDatabase.trackDao; } catch (error) { - widgetHelper.showSnackBar(context, 'error: $error'); + if (mounted) { + widgetHelper.showSnackBar(context, 'error: $error'); + } } setupCronTimer(); doLoadDataTask(); + final isNewUpdateAvailable = await widgetHelper.isNewUpdateAvailable(); + valueNotifierShowBannerUpdate.value = isNewUpdateAvailable; }); super.initState(); } @@ -141,7 +149,7 @@ class _HomePageState extends State with TrayListener, WindowListener { now.day, ); timerDate = Timer.periodic(const Duration(seconds: 1), (_) { - if (!isTimerStart) { + if (!isGlobalTimerStart) { // reminder track var isShowReminderTrack = false; final now = DateTime.now(); @@ -364,144 +372,154 @@ class _HomePageState extends State with TrayListener, WindowListener { @override Widget build(BuildContext context) { return Scaffold( - body: MultiBlocProvider( - providers: [ - BlocProvider( - create: (context) => homeBloc, - ), - BlocProvider( - create: (context) => trackingBloc, - ), - BlocProvider( - create: (context) => userProfileBloc, - ), - BlocProvider( - create: (context) => cronTrackingBloc, - ), - ], - child: MultiBlocListener( - listeners: [ - BlocListener( - listener: (context, state) async { - isLoading = state is LoadingHomeState; - if (state is FailureHomeState) { - final errorMessage = state.errorMessage; - if (errorMessage.contains('401')) { - widgetHelper.showDialog401(context); - return; - } - } else if (state is SuccessLoadDataHomeState) { - isTimerStartTemp = false; - trackUserLite = state.trackUserLiteResponse; - valueNotifierTotalTracked.value = trackUserLite?.trackedInSeconds ?? 0; - - if (listTrackLocal.isNotEmpty) { - var totalTrackedFromLocal = 0; - for (final element in listTrackLocal) { - totalTrackedFromLocal += element.duration; + body: Scaffold( + body: MultiBlocProvider( + providers: [ + BlocProvider( + create: (context) => homeBloc, + ), + BlocProvider( + create: (context) => trackingBloc, + ), + BlocProvider( + create: (context) => userProfileBloc, + ), + BlocProvider( + create: (context) => cronTrackingBloc, + ), + ], + child: MultiBlocListener( + listeners: [ + BlocListener( + listener: (context, state) async { + isLoading = state is LoadingHomeState; + if (state is FailureHomeState) { + final errorMessage = state.errorMessage; + if (errorMessage.contains('401')) { + widgetHelper.showDialog401(context); + return; + } + } else if (state is SuccessLoadDataHomeState) { + isTimerStartTemp = false; + trackUserLite = state.trackUserLiteResponse; + valueNotifierTotalTracked.value = trackUserLite?.trackedInSeconds ?? 0; + + if (listTrackLocal.isNotEmpty) { + var totalTrackedFromLocal = 0; + for (final element in listTrackLocal) { + totalTrackedFromLocal += element.duration; + } + valueNotifierTotalTracked.value += totalTrackedFromLocal; } - valueNotifierTotalTracked.value += totalTrackedFromLocal; - } - - final strTotalTrackingTime = helper.convertTrackingTimeToString(valueNotifierTotalTracked.value); - setTrayTitle(title: strTotalTrackingTime); - - listTrackTask.clear(); - final listTasks = trackUserLite?.listTasks ?? []; - if (listTasks.isNotEmpty) { - listTrackTask.addAll( - listTasks.where((element) { - return element.id != null && element.name != null; - }).map((e) { - return TrackTask( - id: e.id!, - name: e.name!, - trackedInSeconds: 0, - ); - }), - ); - } - final listTracks = trackUserLite?.listTracks ?? []; - for (var index = 0; index < listTrackTask.length; index++) { - final element = listTrackTask[index]; - final id = element.id; - var totalTrackedInSeconds = 0; - final filteredTracks = listTracks.where((e) => e.taskId != null && e.taskId == id); - for (final itemFilteredTrack in filteredTracks) { - totalTrackedInSeconds += itemFilteredTrack.trackedInSeconds ?? 0; + final strTotalTrackingTime = helper.convertTrackingTimeToString(valueNotifierTotalTracked.value); + setTrayTitle(title: strTotalTrackingTime); + + listTrackTask.clear(); + final listTasks = trackUserLite?.listTasks ?? []; + if (listTasks.isNotEmpty) { + listTrackTask.addAll( + listTasks.where((element) { + return element.id != null && element.name != null; + }).map((e) { + return TrackTask( + id: e.id!, + name: e.name!, + trackedInSeconds: 0, + ); + }), + ); } - final filteredTracksLocal = listTrackLocal.where((e) => e.taskId == id); - for (final itemFilteredTrackLocal in filteredTracksLocal) { - totalTrackedInSeconds += itemFilteredTrackLocal.duration; + + final listTracks = trackUserLite?.listTracks ?? []; + for (var index = 0; index < listTrackTask.length; index++) { + final element = listTrackTask[index]; + final id = element.id; + var totalTrackedInSeconds = 0; + final filteredTracks = listTracks.where((e) => e.taskId != null && e.taskId == id); + for (final itemFilteredTrack in filteredTracks) { + totalTrackedInSeconds += itemFilteredTrack.trackedInSeconds ?? 0; + } + final filteredTracksLocal = listTrackLocal.where((e) => e.taskId == id); + for (final itemFilteredTrackLocal in filteredTracksLocal) { + totalTrackedInSeconds += itemFilteredTrackLocal.duration; + } + listTrackTask[index].trackedInSeconds = totalTrackedInSeconds; } - listTrackTask[index].trackedInSeconds = totalTrackedInSeconds; - } - setTrayContextMenu(); + setTrayContextMenu(); - final isAutoStart = state.isAutoStart; - if (isAutoStart) { - autoStartFromSleep(); - } - } - }, - ), - BlocListener( - listener: (context, state) { - if (state is FailureTrackingState) { - /* Nothing to do in here */ - } else if (state is SuccessCreateTimeTrackingState) { - final files = state.files; - for (final path in files) { - final file = File(path); - if (file.existsSync()) { - file.deleteSync(); + final isAutoStart = state.isAutoStart; + if (isAutoStart) { + autoStartFromSleep(); } } - final trackEntityId = state.trackEntityId; - trackDao.deleteTrackById(trackEntityId); - } - }, - ), - BlocListener( - listener: (context, state) { - if (state is SuccessLoadDataUserProfileState) { - final response = state.response; - final name = response.name ?? ''; - final userRole = response.role; - sharedPreferencesManager.putString( - SharedPreferencesManager.keyFullName, - name, - ); - sharedPreferencesManager.putString( - SharedPreferencesManager.keyUserRole, - userRole?.name ?? '', - ); - } - }, - ), - BlocListener( - listener: (context, state) { - if (state is SuccessRunCronTrackingState) { - final ids = state.ids; - final files = state.files; - trackDao.deleteMultipleTrackByIds(ids).then((value) { - for (final itemFile in files) { - final file = File(itemFile); + }, + ), + BlocListener( + listener: (context, state) { + if (state is FailureTrackingState) { + /* Nothing to do in here */ + } else if (state is SuccessCreateTimeTrackingState) { + final files = state.files; + for (final path in files) { + final file = File(path); if (file.existsSync()) { file.deleteSync(); } } - }); - } - }, + final trackEntityId = state.trackEntityId; + trackDao.deleteTrackById(trackEntityId); + } + }, + ), + BlocListener( + listener: (context, state) { + if (state is SuccessLoadDataUserProfileState) { + final response = state.response; + final name = response.name ?? ''; + final userRole = response.role; + sharedPreferencesManager.putString( + SharedPreferencesManager.keyFullName, + name, + ); + sharedPreferencesManager.putString( + SharedPreferencesManager.keyUserRole, + userRole?.name ?? '', + ); + } + }, + ), + BlocListener( + listener: (context, state) { + if (state is SuccessRunCronTrackingState) { + final ids = state.ids; + final files = state.files; + trackDao.deleteMultipleTrackByIds(ids).then((value) { + for (final itemFile in files) { + final file = File(itemFile); + if (file.existsSync()) { + file.deleteSync(); + } + } + }); + } + }, + ), + ], + child: SizedBox( + width: double.infinity, + child: buildWidgetBody(), ), - ], - child: SizedBox( - width: double.infinity, - child: buildWidgetBody(), ), ), + floatingActionButton: FloatingActionButton( + onPressed: () async { + context.pushNamed(ManualTrackingPage.routeName).then((value) { + // TODO: refresh data home jika add manual tracking-nya pada hari ini dan di project yang sama + }); + }, + child: const FaIcon(FontAwesomeIcons.plus), + ), ), ); } @@ -517,7 +535,7 @@ class _HomePageState extends State with TrayListener, WindowListener { final firstTask = filteredTask.first; startTime = DateTime.now(); selectedTask = firstTask; - isTimerStart = true; + isGlobalTimerStart = true; setTrayContextMenu(); valueNotifierTaskTracked.value = firstTask.trackedInSeconds; resetCountTimer(); @@ -544,6 +562,7 @@ class _HomePageState extends State with TrayListener, WindowListener { child: Column( crossAxisAlignment: CrossAxisAlignment.center, children: [ + buildWidgetBannerUpdate(), buildWidgetFieldProject(), const SizedBox(height: 24), buildWidgetTimer(), @@ -554,10 +573,10 @@ class _HomePageState extends State with TrayListener, WindowListener { if (state is LoadingHomeState || isLoading) { return const WidgetCustomCircularProgressIndicator(); } else if (state is FailureHomeState) { - final errorMessage = state.errorMessage; + final errorMessage = state.errorMessage.convertErrorMessageToHumanMessage(); return WidgetError( title: 'oops'.tr(), - message: errorMessage, + message: errorMessage.hideResponseCode(), onTryAgain: doLoadDataTask, ); } @@ -627,7 +646,7 @@ class _HomePageState extends State with TrayListener, WindowListener { onTap: () async { final isPermissionScreenRecordingGranted = await platformChannelHelper.checkPermissionScreenRecording(); - if (mounted && isPermissionScreenRecordingGranted != null && !isPermissionScreenRecordingGranted) { + if (context.mounted && isPermissionScreenRecordingGranted != null && !isPermissionScreenRecordingGranted) { widgetHelper.showDialogPermissionScreenRecording(context); return; } @@ -635,7 +654,7 @@ class _HomePageState extends State with TrayListener, WindowListener { if (isPermissionScreenRecordingGranted!) { final isPermissionAccessibilityGranted = await platformChannelHelper.checkPermissionAccessibility(); - if (mounted && isPermissionAccessibilityGranted != null && !isPermissionAccessibilityGranted) { + if (context.mounted && isPermissionAccessibilityGranted != null && !isPermissionAccessibilityGranted) { widgetHelper.showDialogPermissionAccessibility(context); return; } @@ -649,7 +668,7 @@ class _HomePageState extends State with TrayListener, WindowListener { } startTime = DateTime.now(); selectedTask = itemTask; - isTimerStart = true; + isGlobalTimerStart = true; setTrayContextMenu(); valueNotifierTaskTracked.value = itemTask.trackedInSeconds; resetCountTimer(); @@ -716,7 +735,7 @@ class _HomePageState extends State with TrayListener, WindowListener { } void stopTimerFromButton(TrackTask itemTask) { - isTimerStart = false; + isGlobalTimerStart = false; setTrayContextMenu(); itemTask.trackedInSeconds = valueNotifierTaskTracked.value; stopTimer(); @@ -730,7 +749,7 @@ class _HomePageState extends State with TrayListener, WindowListener { borderRadius: BorderRadius.circular(8), child: InkWell( borderRadius: BorderRadius.circular(8), - onTap: isTimerStart || isTimerStartTemp + onTap: isGlobalTimerStart || isTimerStartTemp ? null : () async { final selectedProjectTemp = await showModalBottomSheet( @@ -789,7 +808,7 @@ class _HomePageState extends State with TrayListener, WindowListener { height: 8, decoration: BoxDecoration( shape: BoxShape.circle, - color: isTimerStart || isTimerStartTemp ? Colors.green : Colors.grey, + color: isGlobalTimerStart || isTimerStartTemp ? Colors.green : Colors.grey, ), ), const SizedBox(width: 4), @@ -805,7 +824,7 @@ class _HomePageState extends State with TrayListener, WindowListener { ), ), const SizedBox(width: 16), - isTimerStart || isTimerStartTemp + isGlobalTimerStart || isTimerStartTemp ? Container() : const Icon( Icons.keyboard_arrow_down, @@ -978,7 +997,7 @@ class _HomePageState extends State with TrayListener, WindowListener { void setTrayContextMenu() { final items = []; if (listTrackTask.isNotEmpty) { - if (!isTimerStart) { + if (!isGlobalTimerStart) { items.add( MenuItem( key: keyTrayStartWorking, @@ -1079,7 +1098,7 @@ class _HomePageState extends State with TrayListener, WindowListener { final task = listTrackTask.first; startTime = DateTime.now(); selectedTask = task; - isTimerStart = true; + isGlobalTimerStart = true; setTrayContextMenu(); valueNotifierTaskTracked.value = task.trackedInSeconds; resetCountTimer(); @@ -1090,7 +1109,7 @@ class _HomePageState extends State with TrayListener, WindowListener { final task = filteredTask.first; startTime = DateTime.now(); selectedTask = task; - isTimerStart = true; + isGlobalTimerStart = true; setTrayContextMenu(); valueNotifierTaskTracked.value = task.trackedInSeconds; resetCountTimer(); @@ -1100,7 +1119,7 @@ class _HomePageState extends State with TrayListener, WindowListener { } void stopTimerFromSystemTray() { - isTimerStart = false; + isGlobalTimerStart = false; setTrayContextMenu(); selectedTask?.trackedInSeconds = valueNotifierTaskTracked.value; stopTimer(); @@ -1141,8 +1160,8 @@ class _HomePageState extends State with TrayListener, WindowListener { isHaveActivity = true; } else if (strEvent == 'screen_is_locked') { // auto stop timer dan ambil screenshot-nya - if (isTimerStart) { - isTimerStart = false; + if (isGlobalTimerStart) { + isGlobalTimerStart = false; setTrayContextMenu(); stopTimer(); finishTime = DateTime.now(); @@ -1189,18 +1208,19 @@ class _HomePageState extends State with TrayListener, WindowListener { }); } - void doTakeScreenshot(DateTime? startTime, DateTime? finishTime, {bool isForceStop = false}) async { + Future doTakeScreenshot(DateTime? startTime, DateTime? finishTime, {bool isForceStop = false}) async { + final selectedTaskTemp = selectedTask; var percentActivity = 0.0; if (counterActivity > 0 && countTimerInSeconds > 0) { percentActivity = (counterActivity / countTimerInSeconds) * 100; } counterActivity = 0; - if (selectedProject == null || selectedTask == null) { + if (selectedProject == null || selectedTaskTemp == null) { return; } - final taskId = selectedTask?.id; + final taskId = selectedTaskTemp.id; if (startTime == null || finishTime == null) { return; @@ -1254,7 +1274,7 @@ class _HomePageState extends State with TrayListener, WindowListener { // stop timer-nya jika permission screen recording-nya tidak diallow-kan atau // gagal ambil screenshot-nya di end time stopTimer(); - isTimerStart = false; + isGlobalTimerStart = false; setTrayContextMenu(); selectedTask = null; setState(() {}); @@ -1264,8 +1284,12 @@ class _HomePageState extends State with TrayListener, WindowListener { if (listPathStartScreenshots.isNotEmpty) { // hapus file list path start screenshot karena tidak pakai file tersebut // jika file screenshot-nya dapat pas di end time - final filtered = - listPathStartScreenshots.where((element) => element != null && element.isNotEmpty).map((e) => e!).toList(); + final filtered = listPathStartScreenshots + .where((element) { + return element != null && element.isNotEmpty; + }) + .map((e) => e!) + .toList(); for (final element in filtered) { final file = File(element); if (file.existsSync()) { @@ -1277,7 +1301,7 @@ class _HomePageState extends State with TrayListener, WindowListener { // stop timer-nya jika isForceStop bernilai true listPathScreenshots.clear(); stopTimer(); - isTimerStart = false; + isGlobalTimerStart = false; setTrayContextMenu(); selectedTask = null; setState(() {}); @@ -1329,14 +1353,14 @@ class _HomePageState extends State with TrayListener, WindowListener { final trackEntity = Track( userId: userId, - taskId: taskId!, + taskId: taskId, startDate: formattedStartDateTime, finishDate: formattedFinishDateTime, activity: activity, files: files, duration: durationInSeconds, projectName: selectedProject?.name ?? '', - taskName: selectedTask?.name ?? '', + taskName: selectedTaskTemp.name, ); final trackEntityId = await trackDao.insertTrack(trackEntity); @@ -1476,4 +1500,68 @@ class _HomePageState extends State with TrayListener, WindowListener { ); } } + + Widget buildWidgetBannerUpdate() { + return ValueListenableBuilder( + valueListenable: valueNotifierShowBannerUpdate, + builder: (BuildContext context, bool isShow, _) { + if (!isShow) { + return Container(); + } + return Column( + children: [ + Material( + borderRadius: BorderRadius.circular(8), + color: Theme.of(context).colorScheme.primaryContainer, + child: InkWell( + borderRadius: BorderRadius.circular(8), + onTap: () { + const feedURL = autoUpdaterUrl; + autoUpdater.setFeedURL(feedURL); + autoUpdater.checkForUpdates(); + valueNotifierShowBannerUpdate.value = false; + }, + child: Container( + width: double.infinity, + padding: const EdgeInsets.all(8), + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(8), + ), + child: Row( + children: [ + Expanded( + child: Padding( + padding: const EdgeInsets.only(left: 8.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'title_new_update_available'.tr(), + style: Theme.of(context).textTheme.bodyLarge, + ), + Text( + 'description_new_update_available'.tr(), + style: Theme.of(context).textTheme.bodySmall, + ), + ], + ), + ), + ), + IconButton( + onPressed: () { + valueNotifierShowBannerUpdate.value = false; + }, + icon: const Icon(Icons.clear), + ), + ], + ), + ), + ), + ), + const SizedBox(height: 16), + ], + ); + }, + ); + } } diff --git a/lib/feature/presentation/page/login/login_page.dart b/lib/feature/presentation/page/login/login_page.dart index 52a2693..3518291 100644 --- a/lib/feature/presentation/page/login/login_page.dart +++ b/lib/feature/presentation/page/login/login_page.dart @@ -17,7 +17,7 @@ class LoginPage extends StatefulWidget { static const routePath = '/login'; static const routeName = 'login'; - const LoginPage({Key? key}) : super(key: key); + const LoginPage({super.key}); @override State createState() => _LoginPageState(); @@ -122,7 +122,7 @@ class _LoginPageState extends State { final email = controllerEmail.text.trim(); context.pushNamed( ForgotPasswordPage.routeName, - queryParams: { + queryParameters: { ForgotPasswordPage.parameterEmail: email, }, ); diff --git a/lib/feature/presentation/page/manual_tracking/manual_tracking_page.dart b/lib/feature/presentation/page/manual_tracking/manual_tracking_page.dart new file mode 100644 index 0000000..7792e40 --- /dev/null +++ b/lib/feature/presentation/page/manual_tracking/manual_tracking_page.dart @@ -0,0 +1,730 @@ +import 'package:dipantau_desktop_client/core/util/helper.dart'; +import 'package:dipantau_desktop_client/core/util/shared_preferences_manager.dart'; +import 'package:dipantau_desktop_client/core/util/string_extension.dart'; +import 'package:dipantau_desktop_client/core/util/widget_helper.dart'; +import 'package:dipantau_desktop_client/feature/data/model/manual_create_track/manual_create_track_body.dart'; +import 'package:dipantau_desktop_client/feature/data/model/project_task/project_task_response.dart'; +import 'package:dipantau_desktop_client/feature/presentation/bloc/manual_tracking/manual_tracking_bloc.dart'; +import 'package:dipantau_desktop_client/feature/presentation/widget/widget_custom_circular_progress_indicator.dart'; +import 'package:dipantau_desktop_client/feature/presentation/widget/widget_error.dart'; +import 'package:dipantau_desktop_client/feature/presentation/widget/widget_primary_button.dart'; +import 'package:dipantau_desktop_client/injection_container.dart'; +import 'package:easy_localization/easy_localization.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:go_router/go_router.dart'; + +class ManualTrackingPage extends StatefulWidget { + static const routePath = '/manual-tracking'; + static const routeName = 'manual-tracking'; + + const ManualTrackingPage({super.key}); + + @override + State createState() => _ManualTrackingPageState(); +} + +class _ManualTrackingPageState extends State { + final manualTrackingBloc = sl(); + final widgetHelper = WidgetHelper(); + final formState = GlobalKey(); + final helper = sl(); + final sharedPreferencesManager = sl(); + final projectItems = <_ItemData>[]; + final taskItems = <_ItemData>[]; + final controllerStartDate = TextEditingController(); + final controllerStartTime = TextEditingController(); + final controllerFinishDate = TextEditingController(); + final controllerFinishTime = TextEditingController(); + final controllerDuration = TextEditingController(); + final controllerNote = TextEditingController(); + final valueNotifierEnableButtonSave = ValueNotifier(false); + + var isLoading = false; + var userId = ''; + ProjectTaskResponse? projectTask; + _ItemData? selectedProject, selectedTask; + DateTime? startDate, finishDate; + TimeOfDay? startTime, finishTime; + int? durationInSeconds; + + @override + void setState(VoidCallback fn) { + if (mounted) { + super.setState(fn); + } + } + + @override + void initState() { + userId = sharedPreferencesManager.getString(SharedPreferencesManager.keyUserId) ?? ''; + doLoadData(); + super.initState(); + } + + @override + Widget build(BuildContext context) { + return IgnorePointer( + ignoring: isLoading, + child: BlocProvider( + create: (context) => manualTrackingBloc, + child: BlocListener( + listener: (context, state) { + setState(() => isLoading = state is LoadingManualTrackingState); + if (state is FailureManualTrackingState) { + final errorMessage = state.errorMessage.convertErrorMessageToHumanMessage(); + if (errorMessage.contains('401')) { + widgetHelper.showDialog401(context); + return; + } + widgetHelper.showSnackBar(context, errorMessage.hideResponseCode()); + } else if (state is SuccessCreateManualTrackingState) { + widgetHelper.showSnackBar(context, 'add_manual_track_successfully'.tr()); + context.pop(); + } else if (state is SuccessLoadDataProjectTaskManualTrackingState) { + projectTask = state.response; + projectItems.clear(); + taskItems.clear(); + for (final element in projectTask?.data ?? []) { + final projectId = element.projectId; + final projectName = element.projectName; + if (projectId == null || projectName == null || projectName.isEmpty) { + continue; + } + projectItems.add( + _ItemData( + id: projectId, + name: projectName, + ), + ); + } + setState(() {}); + } + }, + child: Scaffold( + appBar: AppBar( + title: Text('add_manual_track'.tr()), + centerTitle: false, + ), + body: buildWidgetBody(), + ), + ), + ), + ); + } + + Widget buildWidgetBody() { + if (projectTask == null) { + return BlocBuilder( + builder: (context, state) { + if (state is LoadingManualTrackingState) { + return const WidgetCustomCircularProgressIndicator(); + } else if (state is FailureCenterManualTrackingState) { + final errorMessage = state.errorMessage.convertErrorMessageToHumanMessage(); + return Padding( + padding: EdgeInsets.symmetric(horizontal: helper.getDefaultPaddingLayout), + child: WidgetError( + title: 'oops'.tr(), + message: errorMessage.hideResponseCode(), + onTryAgain: doLoadData, + ), + ); + } + return Container(); + }, + ); + } + + return BlocBuilder( + builder: (context, state) { + return buildWidgetForm(); + }, + ); + } + + Widget buildWidgetForm() { + return Form( + key: formState, + child: ListView( + padding: EdgeInsets.all(helper.getDefaultPaddingLayout), + children: [ + buildWidgetFieldDropdown( + selectedProject, + projectItems, + labelText: 'project'.tr(), + hintText: 'select_project'.tr(), + onChanged: (value) { + if (value != null && selectedProject != null && value.id == selectedProject?.id) { + /* Nothing to do in here */ + return; + } + selectedProject = value; + selectedTask = null; + taskItems.clear(); + final listTasks = projectTask?.data?.where((element) { + final projectId = element.projectId; + return projectId != null && projectId == selectedProject?.id; + }).map((e) => e.tasks); + if (listTasks != null && listTasks.isNotEmpty) { + final tasks = listTasks.first; + for (final task in tasks) { + final taskId = task.id; + final taskName = task.name; + if (taskId == null || taskName == null || taskName.isEmpty) { + continue; + } + taskItems.add( + _ItemData( + id: taskId, + name: taskName, + ), + ); + } + if (taskItems.isNotEmpty) { + selectedTask = taskItems.first; + } + } + doCheckEnableButtonSubmit(); + setState(() {}); + }, + validator: (value) { + return value == null ? 'please_choose_a_project'.tr() : null; + }, + ), + const SizedBox(height: 24), + buildWidgetFieldDropdown( + selectedTask, + taskItems, + labelText: 'task'.tr(), + hintText: selectedProject == null + ? 'select_a_project_first'.tr() + : taskItems.isEmpty + ? 'no_data_available'.tr() + : 'select_task'.tr(), + onChanged: (value) { + selectedTask = value; + doCheckEnableButtonSubmit(); + setState(() {}); + }, + validator: (value) { + return value == null ? 'please_choose_a_task'.tr() : null; + }, + ), + const SizedBox(height: 24), + Row( + children: [ + Expanded( + child: buildWidgetField( + controllerStartDate, + label: 'start_date'.tr(), + hint: 'set_start_date'.tr(), + validator: (value) { + return value == null ? 'please_set_start_date'.tr() : null; + }, + onTap: () async { + final now = DateTime.now(); + final firstDate = now.subtract(const Duration(days: 30)); + final selectedStartDate = await showDatePicker( + context: context, + initialDate: startDate ?? now, + firstDate: firstDate, + lastDate: now, + ); + if (selectedStartDate != null) { + startDate = selectedStartDate; + checkIfStartAfterFinishDateTime(); + controllerStartDate.text = helper.setDateFormat('EEEE dd MMM yyyy').format(startDate!); + calculateDuration(); + doCheckEnableButtonSubmit(); + setState(() {}); + } + }, + ), + ), + const SizedBox(width: 12), + Expanded( + child: buildWidgetField( + controllerStartTime, + label: 'start_time'.tr(), + hint: 'set_start_time'.tr(), + validator: (value) { + return value == null ? 'please_set_start_time'.tr() : null; + }, + onTap: () async { + TimeOfDay initialTime = TimeOfDay.now(); + if (startTime != null) { + initialTime = TimeOfDay( + hour: startTime!.hour, + minute: startTime!.minute, + ); + } + final selectedStartTime = await showTimePicker( + context: context, + initialTime: initialTime, + initialEntryMode: TimePickerEntryMode.input, + ); + if (selectedStartTime != null) { + startTime = TimeOfDay( + hour: selectedStartTime.hour, + minute: selectedStartTime.minute, + ); + final startDateTime = DateTime( + startDate!.year, + startDate!.month, + startDate!.day, + startTime!.hour, + startTime!.minute, + 0, + ); + checkIfStartAfterFinishDateTime(); + controllerStartTime.text = helper.setDateFormat('HH:mm').format(startDateTime); + calculateDuration(); + doCheckEnableButtonSubmit(); + setState(() {}); + } + }, + isEnabled: startDate != null, + ), + ), + ], + ), + const SizedBox(height: 24), + Row( + children: [ + Expanded( + child: buildWidgetField( + controllerFinishDate, + label: 'finish_date'.tr(), + hint: 'set_finish_date'.tr(), + validator: (value) { + return value == null ? 'please_set_finish_date'.tr() : null; + }, + onTap: () async { + final firstDate = DateTime( + startDate!.year, + startDate!.month, + startDate!.day, + startTime!.hour, + startTime!.minute, + 0, + ); + firstDate.add(const Duration(minutes: 1)); + final selectedFinishDate = await showDatePicker( + context: context, + initialDate: finishDate ?? firstDate, + firstDate: firstDate, + lastDate: firstDate.add(const Duration(days: 1)), + ); + if (selectedFinishDate != null) { + finishDate = selectedFinishDate; + controllerFinishDate.text = helper.setDateFormat('EEEE dd MMM yyyy').format(finishDate!); + final isFinishBeforeStart = isFinishBeforeStartDateTime(); + if (isFinishBeforeStart) { + finishTime = null; + controllerFinishTime.text = ''; + if (mounted) { + showDialogValidationTime(); + } + return; + } + + calculateDuration(); + doCheckEnableButtonSubmit(); + setState(() {}); + } + }, + isEnabled: startDate != null && startTime != null, + ), + ), + const SizedBox(width: 12), + Expanded( + child: buildWidgetField( + controllerFinishTime, + label: 'finish_time'.tr(), + hint: 'set_finish_time'.tr(), + validator: (value) { + return value == null ? 'please_set_finish_time'.tr() : null; + }, + onTap: () async { + DateTime startDateTime = DateTime( + startDate!.year, + startDate!.month, + startDate!.day, + startTime!.hour, + startTime!.minute, + 0, + ); + startDateTime = startDateTime.add(const Duration(minutes: 1)); + + TimeOfDay initialTime = TimeOfDay( + hour: startDateTime.hour, + minute: startDateTime.minute, + ); + if (finishTime != null) { + initialTime = TimeOfDay( + hour: finishTime!.hour, + minute: finishTime!.minute, + ); + } + final selectedFinishTime = await showTimePicker( + context: context, + initialTime: initialTime, + initialEntryMode: TimePickerEntryMode.input, + ); + if (selectedFinishTime != null) { + finishTime = TimeOfDay( + hour: selectedFinishTime.hour, + minute: selectedFinishTime.minute, + ); + final finishDateTime = DateTime( + finishDate!.year, + finishDate!.month, + finishDate!.day, + finishTime!.hour, + finishTime!.minute, + 0, + ); + controllerFinishTime.text = helper.setDateFormat('HH:mm').format(finishDateTime); + final isFinishBeforeStart = isFinishBeforeStartDateTime(); + if (isFinishBeforeStart) { + finishTime = null; + controllerFinishTime.text = ''; + if (mounted) { + showDialogValidationTime(); + } + return; + } + + calculateDuration(); + doCheckEnableButtonSubmit(); + setState(() {}); + } + }, + isEnabled: startDate != null && startTime != null && finishDate != null, + ), + ), + ], + ), + const SizedBox(height: 24), + buildWidgetField( + controllerDuration, + label: 'duration'.tr(), + hint: 'set_start_and_finish_time'.tr(), + isEnabled: false, + ), + const SizedBox(height: 24), + buildWidgetField( + controllerNote, + label: 'reason'.tr(), + hint: 'why_are_you_adding_manual_track'.tr(), + isEnabled: true, + readOnly: false, + maxLength: 100, + onChanged: (_) { + doCheckEnableButtonSubmit(); + }, + minLines: 1, + maxLines: 3, + ), + const SizedBox(height: 24), + buildWidgetButtonSave(), + ], + ), + ); + } + + void showDialogValidationTime() { + showDialog( + context: context, + builder: (context) { + return AlertDialog( + title: Text('warning'.tr()), + content: Text( + 'finish_date_time_must_be_after_of_start_date_time'.tr(), + ), + actions: [ + TextButton( + onPressed: () => context.pop(), + child: Text('ok'.tr()), + ), + ], + ); + }, + ); + } + + bool isFinishBeforeStartDateTime() { + if (startDate != null && startTime != null && finishDate != null && finishTime != null) { + final startDateTime = DateTime( + startDate!.year, + startDate!.month, + startDate!.day, + startTime!.hour, + startTime!.minute, + 0, + ); + final finishDateTime = DateTime( + finishDate!.year, + finishDate!.month, + finishDate!.day, + finishTime!.hour, + finishTime!.minute, + 0, + ); + if (finishDateTime.isBefore(startDateTime) || finishDateTime.isAtSameMomentAs(startDateTime)) { + return true; + } + } + return false; + } + + void checkIfStartAfterFinishDateTime() { + if (finishDate != null && finishTime != null && startDate != null && startTime != null) { + final startDateTime = DateTime( + startDate!.year, + startDate!.month, + startDate!.day, + startTime!.hour, + startTime!.minute, + 0, + ); + final finishDateTime = DateTime( + finishDate!.year, + finishDate!.month, + finishDate!.day, + finishTime!.hour, + finishTime!.minute, + 0, + ); + if (startDateTime.isAfter(finishDateTime)) { + finishDate = null; + finishTime = null; + controllerFinishDate.text = ''; + controllerFinishTime.text = ''; + } + } + } + + Widget buildWidgetButtonSave() { + final widgetButton = ValueListenableBuilder( + valueListenable: valueNotifierEnableButtonSave, + builder: (BuildContext context, bool isEnable, _) { + return SizedBox( + width: double.infinity, + child: WidgetPrimaryButton( + onPressed: isEnable ? doSave : null, + isLoading: isLoading, + child: Text( + 'save'.tr(), + ), + ), + ); + }, + ); + + return BlocBuilder( + builder: (context, state) { + return widgetButton; + }, + ); + } + + void doSave() { + final timezoneOffsetInSeconds = startDate!.timeZoneOffset.inSeconds; + final timezoneOffset = helper.convertSecondToHms(timezoneOffsetInSeconds); + var strTimezoneOffset = timezoneOffsetInSeconds >= 0 ? '+' : '-'; + strTimezoneOffset += timezoneOffset.hour < 10 ? '0${timezoneOffset.hour}' : timezoneOffset.hour.toString(); + strTimezoneOffset += ':'; + strTimezoneOffset += timezoneOffset.minute < 10 ? '0${timezoneOffset.minute}' : timezoneOffset.minute.toString(); + + const datePattern = 'yyyy-MM-dd'; + const timePattern = 'HH:mm:ss'; + final startDateTime = DateTime( + startDate!.year, + startDate!.month, + startDate!.day, + startTime!.hour, + startTime!.minute, + 0, + ); + final finishDateTime = DateTime( + finishDate!.year, + finishDate!.month, + finishDate!.day, + finishTime!.hour, + finishTime!.minute, + 0, + ); + final strStartDate = helper.setDateFormat(datePattern).format(startDateTime); + final strStartTime = helper.setDateFormat(timePattern).format(startDateTime); + final strFinishDate = helper.setDateFormat(datePattern).format(finishDateTime); + final strFinishTime = helper.setDateFormat(timePattern).format(finishDateTime); + final formattedStartDateTime = '${strStartDate}T$strStartTime$strTimezoneOffset'; + final formattedFinishDateTime = '${strFinishDate}T$strFinishTime$strTimezoneOffset'; + final body = ManualCreateTrackBody( + taskId: selectedTask!.id, + startDate: formattedStartDateTime, + finishDate: formattedFinishDateTime, + duration: durationInSeconds!, + note: controllerNote.text.trim(), + ); + manualTrackingBloc.add( + CreateManualTrackingEvent( + body: body, + ), + ); + } + + void calculateDuration() { + if (startDate != null && startTime != null && finishDate != null && finishTime != null) { + final startDateTime = DateTime( + startDate!.year, + startDate!.month, + startDate!.day, + startTime!.hour, + startTime!.minute, + 0, + ); + final finishDateTime = DateTime( + finishDate!.year, + finishDate!.month, + finishDate!.day, + finishTime!.hour, + finishTime!.minute, + 0, + ); + durationInSeconds = startDateTime.difference(finishDateTime).inSeconds.abs(); + final mapDuration = helper.convertSecondToHms(durationInSeconds!); + final hour = mapDuration.hour; + final minute = mapDuration.minute; + final second = mapDuration.second; + final listStrDuration = []; + if (hour > 0) { + listStrDuration.add('hour_n'.plural(hour)); + } + if (minute > 0) { + listStrDuration.add('minute_n'.plural(minute)); + } + if (second > 0) { + listStrDuration.add('second_n'.plural(second)); + } + var strDuration = ''; + if (listStrDuration.isNotEmpty) { + strDuration = listStrDuration.join(' '); + } else { + strDuration = ''; + } + controllerDuration.text = strDuration; + } else { + durationInSeconds = null; + controllerDuration.text = ''; + } + } + + Widget buildWidgetField( + TextEditingController controller, { + required String label, + required String hint, + Function()? onTap, + bool isEnabled = true, + FormFieldValidator? validator, + bool readOnly = true, + int? maxLength, + ValueChanged? onChanged, + int? minLines, + int? maxLines, + }) { + return TextFormField( + controller: controller, + decoration: widgetHelper.setDefaultTextFieldDecoration( + labelText: label, + hintText: hint, + ), + readOnly: readOnly, + mouseCursor: MaterialStateMouseCursor.clickable, + onTap: onTap, + validator: validator, + enabled: isEnabled, + maxLength: maxLength, + onChanged: onChanged, + minLines: minLines, + maxLines: maxLines, + ); + } + + Widget buildWidgetFieldDropdown( + _ItemData? value, + List<_ItemData> items, { + required String labelText, + required String hintText, + ValueChanged<_ItemData?>? onChanged, + FormFieldValidator<_ItemData>? validator, + }) { + return DropdownButtonFormField<_ItemData>( + value: value, + items: items.map((element) { + return DropdownMenuItem( + value: element, + child: Text( + element.name, + ), + ); + }).toList(), + onChanged: onChanged, + selectedItemBuilder: (context) { + return items.map((element) { + final name = element.name; + return Text(name); + }).toList(); + }, + validator: validator, + autovalidateMode: AutovalidateMode.onUserInteraction, + padding: EdgeInsets.zero, + hint: Text(hintText), + decoration: widgetHelper.setDefaultTextFieldDecoration( + labelText: labelText, + floatingLabelBehavior: FloatingLabelBehavior.always, + ), + ); + } + + void doCheckEnableButtonSubmit() { + var isEnableTemp = false; + final reason = controllerNote.text.trim(); + if (selectedProject != null && + selectedTask != null && + startDate != null && + finishDate != null && + durationInSeconds != null && + durationInSeconds! > 0 && + reason.isNotEmpty) { + isEnableTemp = true; + } + if (isEnableTemp != valueNotifierEnableButtonSave.value) { + valueNotifierEnableButtonSave.value = isEnableTemp; + } + } + + void doLoadData() { + manualTrackingBloc.add( + LoadDataProjectTaskManualTrackingEvent( + userId: userId, + ), + ); + } +} + +class _ItemData { + final int id; + final String name; + + _ItemData({ + required this.id, + required this.name, + }); + + @override + String toString() { + return '_ItemData{id: $id, name: $name}'; + } +} diff --git a/lib/feature/presentation/page/member_setting/member_setting_page.dart b/lib/feature/presentation/page/member_setting/member_setting_page.dart index bff928a..35dbe88 100644 --- a/lib/feature/presentation/page/member_setting/member_setting_page.dart +++ b/lib/feature/presentation/page/member_setting/member_setting_page.dart @@ -18,7 +18,7 @@ class MemberSettingPage extends StatefulWidget { static const routePath = '/member-setting'; static const routeName = 'member-setting'; - const MemberSettingPage({Key? key}) : super(key: key); + const MemberSettingPage({super.key}); @override State createState() => _MemberSettingPageState(); @@ -159,10 +159,10 @@ class _MemberSettingPageState extends State { child: BlocBuilder( builder: (context, state) { if (state is FailureMemberState) { - final errorMessage = state.errorMessage; + final errorMessage = state.errorMessage.convertErrorMessageToHumanMessage(); return WidgetError( title: 'oops'.tr(), - message: errorMessage, + message: errorMessage.hideResponseCode(), onTryAgain: doLoadData, ); } else if (state is LoadingCenterMemberState) { diff --git a/lib/feature/presentation/page/photo_view/photo_view_page.dart b/lib/feature/presentation/page/photo_view/photo_view_page.dart index 4bf8646..e2a3486 100644 --- a/lib/feature/presentation/page/photo_view/photo_view_page.dart +++ b/lib/feature/presentation/page/photo_view/photo_view_page.dart @@ -1,8 +1,16 @@ import 'dart:io'; +import 'package:dio/dio.dart'; +import 'package:dipantau_desktop_client/core/util/enum/global_variable.dart'; +import 'package:dipantau_desktop_client/core/util/enum/user_role.dart'; import 'package:dipantau_desktop_client/core/util/images.dart'; +import 'package:dipantau_desktop_client/core/util/shared_preferences_manager.dart'; +import 'package:dipantau_desktop_client/core/util/widget_helper.dart'; +import 'package:dipantau_desktop_client/feature/data/model/track_user/track_user_response.dart'; +import 'package:dipantau_desktop_client/feature/presentation/widget/widget_loading_center_full_screen.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; +import 'package:path_provider/path_provider.dart'; import 'package:photo_view/photo_view.dart'; import 'package:photo_view/photo_view_gallery.dart'; @@ -10,13 +18,16 @@ class PhotoViewPage extends StatefulWidget { static const routePath = '/photo-view'; static const routeName = 'photo-view'; static const parameterListPhotos = 'list_photos'; + static const parameterIsShowIconDownload = 'is_show_icon_download'; - final List? listPhotos; + final List? listPhotos; + final bool? isShowIconDownload; PhotoViewPage({ - Key? key, + super.key, required this.listPhotos, - }) : super(key: key); + required this.isShowIconDownload, + }); @override State createState() => _PhotoViewPageState(); @@ -24,92 +35,317 @@ class PhotoViewPage extends StatefulWidget { class _PhotoViewPageState extends State { final pageController = PageController(); - final listPhotos = []; + final listPhotos = []; + final valueNotifierLoadingDownload = ValueNotifier(false); + final widgetHelper = WidgetHelper(); var indexSelectedPhoto = 0; + UserRole? userRole; + var isBlurSettingEnabled = false; + var isBlurPreviewEnabled = false; + var isShowIconDownload = false; @override void initState() { + final strUserRole = sharedPreferencesManager.getString(SharedPreferencesManager.keyUserRole) ?? ''; + userRole = strUserRole.fromStringUserRole; if (widget.listPhotos != null) { listPhotos.addAll(widget.listPhotos ?? []); } + isBlurSettingEnabled = listPhotos.where((element) => (element.urlBlur ?? '').isNotEmpty).isNotEmpty; + isBlurPreviewEnabled = isBlurSettingEnabled; + isShowIconDownload = widget.isShowIconDownload ?? false; super.initState(); } @override Widget build(BuildContext context) { - return listPhotos.isEmpty - ? Center( - child: Text('no_data_to_display'.tr()), - ) - : Stack( - children: [ - PhotoViewGallery.builder( - pageController: pageController, - scrollPhysics: const BouncingScrollPhysics(), - builder: (BuildContext context, int index) { - final photo = listPhotos[index]; - return photo.startsWith('http') - ? PhotoViewGalleryPageOptions( - imageProvider: NetworkImage(photo), - initialScale: PhotoViewComputedScale.contained, - heroAttributes: PhotoViewHeroAttributes( - tag: photo, - ), - ) - : PhotoViewGalleryPageOptions( - imageProvider: FileImage(File(photo)), - initialScale: PhotoViewComputedScale.contained, - heroAttributes: PhotoViewHeroAttributes( - tag: photo, - ), - ); - }, - loadingBuilder: (context, loadingProgress) { - final cumulativeBytesLoaded = loadingProgress?.cumulativeBytesLoaded ?? 0; - return Center( - child: CircularProgressIndicator( - strokeWidth: 1, - value: loadingProgress?.expectedTotalBytes != null - ? cumulativeBytesLoaded / loadingProgress!.expectedTotalBytes! - : null, - ), - ); - }, - itemCount: listPhotos.length, - onPageChanged: (index) { - setState(() => indexSelectedPhoto = index); - }, + return Scaffold( + body: listPhotos.isEmpty + ? Center( + child: Text('no_data_to_display'.tr()), + ) + : Stack( + children: [ + PhotoViewGallery.builder( + pageController: pageController, + scrollPhysics: const BouncingScrollPhysics(), + builder: (BuildContext context, int index) { + var photo = ''; + if (isBlurPreviewEnabled) { + photo = listPhotos[index].urlBlur ?? ''; + } else { + photo = listPhotos[index].url ?? ''; + } + return photo.startsWith('http') + ? PhotoViewGalleryPageOptions( + imageProvider: NetworkImage(photo), + initialScale: PhotoViewComputedScale.contained, + heroAttributes: PhotoViewHeroAttributes( + tag: photo, + ), + ) + : PhotoViewGalleryPageOptions( + imageProvider: FileImage(File(photo)), + initialScale: PhotoViewComputedScale.contained, + heroAttributes: PhotoViewHeroAttributes( + tag: photo, + ), + ); + }, + loadingBuilder: (context, loadingProgress) { + final cumulativeBytesLoaded = loadingProgress?.cumulativeBytesLoaded ?? 0; + return Center( + child: CircularProgressIndicator( + strokeWidth: 1, + value: loadingProgress?.expectedTotalBytes != null + ? cumulativeBytesLoaded / loadingProgress!.expectedTotalBytes! + : null, + ), + ); + }, + itemCount: listPhotos.length, + onPageChanged: (index) { + setState(() => indexSelectedPhoto = index); + }, + ), + buildWidgetIconClose(), + buildWidgetActionTopEnd(), + Align( + alignment: Alignment.bottomCenter, + child: buildWidgetSliderPreviewPhoto(), + ), + buildWidgetLoadingFullScreen(), + ], + ), + ); + } + + Widget buildWidgetLoadingFullScreen() { + return ValueListenableBuilder( + valueListenable: valueNotifierLoadingDownload, + builder: (BuildContext context, bool isShowLoading, _) { + if (isShowLoading) { + return const WidgetLoadingCenterFullScreen(); + } + return Container(); + }, + ); + } + + Widget buildWidgetActionTopEnd() { + return Align( + alignment: Alignment.topRight, + child: Row( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + buildWidgetIconPreviewSetting(), + buildWidgetIconDownload(), + ], + ), + ); + } + + Widget buildWidgetIconDownload() { + if (!isShowIconDownload) { + return Container(); + } + + return Container( + decoration: BoxDecoration( + shape: BoxShape.circle, + color: Colors.black.withOpacity(.5), + ), + margin: const EdgeInsets.only( + right: 8, + top: 8, + ), + child: IconButton( + onPressed: () async { + final selectedPhoto = listPhotos[indexSelectedPhoto]; + final url = selectedPhoto.url ?? ''; + + final downloadDirectory = await getDownloadsDirectory(); + final pathDownloadDirectory = downloadDirectory?.path; + if ((pathDownloadDirectory == null || pathDownloadDirectory.isEmpty) && mounted) { + widgetHelper.showDialogMessage( + context, + 'info'.tr(), + 'download_directory_invalid'.tr(), + ); + return; + } + + if (url.startsWith('http')) { + // download file dari url dan simpan ke directory download + final splitUrl = url.split('/'); + if (splitUrl.isEmpty && mounted) { + widgetHelper.showDialogMessage( + context, + 'info'.tr(), + 'url_screenshot_invalid'.tr(), + ); + return; + } + + final itemUrl = splitUrl.last; + final splitItemUrl = itemUrl.split('?'); + if (splitItemUrl.isEmpty && mounted) { + widgetHelper.showDialogMessage( + context, + 'info'.tr(), + 'screenshot_name_invalid'.tr(), + ); + return; + } + + valueNotifierLoadingDownload.value = true; + final filename = splitItemUrl.first; + final response = await Dio().get( + url, + options: Options( + responseType: ResponseType.bytes, ), - Align( - alignment: Alignment.topLeft, - child: Container( - decoration: BoxDecoration( - shape: BoxShape.circle, - color: Colors.black.withOpacity(.5), - ), - margin: const EdgeInsets.only( - left: 8, - top: 8, + ); + try { + final fileNameAndPath = '$pathDownloadDirectory/$filename'; + final file = File(fileNameAndPath); + await file.writeAsBytes(response.data); + if (mounted) { + widgetHelper.showSnackBar( + context, + 'screenshot_downloaded_successfully'.tr(), + ); + } + } catch (error) { + if (mounted) { + widgetHelper.showDialogMessage( + context, + 'info'.tr(), + 'something_went_wrong_with_message'.tr( + args: [ + '$error', + ], ), - child: IconButton( - onPressed: () { - Navigator.pop(context); - }, - icon: const Icon( - Icons.clear, - color: Colors.white, - ), - padding: const EdgeInsets.all(8), + ); + } + } finally { + valueNotifierLoadingDownload.value = false; + } + } else { + // copy file dari lokal ke download directory + valueNotifierLoadingDownload.value = true; + final originalFile = File(url); + try { + if (!originalFile.existsSync() && mounted) { + widgetHelper.showDialogMessage( + context, + 'info'.tr(), + 'file_screenshot_doesnt_exists'.tr(), + ); + return; + } + + final splitPathOriginalFile = url.split('/'); + if (splitPathOriginalFile.isEmpty && mounted) { + widgetHelper.showDialogMessage( + context, + 'info'.tr(), + 'path_file_screenshot_invalid'.tr(), + ); + return; + } + + final filename = splitPathOriginalFile.last; + final fileNameAndPath = '$pathDownloadDirectory/$filename'; + final newFile = File(fileNameAndPath); + await originalFile.copy(newFile.path); + if (mounted) { + widgetHelper.showSnackBar( + context, + 'screenshot_downloaded_successfully'.tr(), + ); + } + } catch (error) { + if (mounted) { + widgetHelper.showDialogMessage( + context, + 'info'.tr(), + 'something_went_wrong_with_message'.tr( + args: [ + '$error', + ], ), - ), - ), - Align( - alignment: Alignment.bottomCenter, - child: buildWidgetSliderPreviewPhoto(), - ), - ], - ); + ); + } + } finally { + valueNotifierLoadingDownload.value = false; + } + } + }, + icon: const Icon( + Icons.download, + color: Colors.white, + ), + padding: const EdgeInsets.all(8), + ), + ); + } + + Widget buildWidgetIconPreviewSetting() { + if (!isBlurSettingEnabled || (userRole != UserRole.superAdmin)) { + return Container(); + } + + return Container( + decoration: BoxDecoration( + shape: BoxShape.circle, + color: Colors.black.withOpacity(.5), + ), + margin: const EdgeInsets.only( + right: 8, + top: 8, + ), + child: IconButton( + onPressed: () { + setState(() { + isBlurPreviewEnabled = !isBlurPreviewEnabled; + }); + }, + icon: Icon( + isBlurPreviewEnabled ? Icons.visibility_off : Icons.visibility, + color: Colors.white, + ), + padding: const EdgeInsets.all(8), + ), + ); + } + + Widget buildWidgetIconClose() { + return Align( + alignment: Alignment.topLeft, + child: Container( + decoration: BoxDecoration( + shape: BoxShape.circle, + color: Colors.black.withOpacity(.5), + ), + margin: const EdgeInsets.only( + left: 8, + top: 8, + ), + child: IconButton( + onPressed: () { + Navigator.pop(context); + }, + icon: const Icon( + Icons.clear, + color: Colors.white, + ), + padding: const EdgeInsets.all(8), + ), + ), + ); } Widget buildWidgetSliderPreviewPhoto() { @@ -126,37 +362,26 @@ class _PhotoViewPageState extends State { child: Row( mainAxisAlignment: MainAxisAlignment.center, mainAxisSize: MainAxisSize.max, - children: listPhotos.map((photo) { - final index = listPhotos.indexOf(photo); - final isSelected = photo == listPhotos[indexSelectedPhoto]; + children: listPhotos.map((element) { + final index = listPhotos.indexOf(element); + final elementId = element.id; + final selectedId = listPhotos[indexSelectedPhoto].id; + var isSelected = false; + if (elementId != null || selectedId != null) { + isSelected = elementId == selectedId; + } + var photo = ''; + if (isBlurPreviewEnabled) { + photo = listPhotos[index].urlBlur ?? ''; + } else { + photo = listPhotos[index].url ?? ''; + } final widgetImage = SizedBox( width: defaultSize, height: defaultSize, child: photo.startsWith('http') - ? /*CachedNetworkImage( - imageUrl: photo, - fit: BoxFit.cover, - width: defaultSize, - height: defaultSize, - errorWidget: (context, error, stacktrace) { - return Image.asset( - BaseImage.imagePlaceholder, - fit: BoxFit.cover, - width: defaultSize, - height: defaultSize, - ); - }, - progressIndicatorBuilder: (context, url, downloadProgress) { - return Center( - child: CircularProgressIndicator( - strokeWidth: 1, - value: downloadProgress.progress, - ), - ); - }, - )*/ - Image.network( + ? Image.network( photo, fit: BoxFit.cover, width: defaultSize, diff --git a/lib/feature/presentation/page/register/register_page.dart b/lib/feature/presentation/page/register/register_page.dart index fe6fe0c..0048d95 100644 --- a/lib/feature/presentation/page/register/register_page.dart +++ b/lib/feature/presentation/page/register/register_page.dart @@ -17,7 +17,7 @@ class RegisterPage extends StatefulWidget { static const routePath = '/register'; static const routeName = 'register'; - const RegisterPage({Key? key}) : super(key: key); + const RegisterPage({super.key}); @override State createState() => _RegisterPageState(); @@ -51,7 +51,7 @@ class _RegisterPageState extends State { } else if (state is SuccessSubmitSignUpState) { context.goNamed( RegisterSuccessPage.routeName, - queryParams: { + queryParameters: { 'email': state.response.email ?? '-', }, ); diff --git a/lib/feature/presentation/page/register_success/register_success_page.dart b/lib/feature/presentation/page/register_success/register_success_page.dart index 1b1dfe0..b6d2ad4 100644 --- a/lib/feature/presentation/page/register_success/register_success_page.dart +++ b/lib/feature/presentation/page/register_success/register_success_page.dart @@ -14,9 +14,9 @@ class RegisterSuccessPage extends StatelessWidget { final String email; RegisterSuccessPage({ - Key? key, + super.key, required this.email, - }) : super(key: key); + }); final helper = sl(); diff --git a/lib/feature/presentation/page/report_screenshot/report_screenshot_page.dart b/lib/feature/presentation/page/report_screenshot/report_screenshot_page.dart index 50256b1..e7be2f1 100644 --- a/lib/feature/presentation/page/report_screenshot/report_screenshot_page.dart +++ b/lib/feature/presentation/page/report_screenshot/report_screenshot_page.dart @@ -27,7 +27,7 @@ class ReportScreenshotPage extends StatefulWidget { static const routePath = '/report-screenshot'; static const routeName = 'report-screenshot'; - const ReportScreenshotPage({Key? key}) : super(key: key); + const ReportScreenshotPage({super.key}); @override State createState() => _ReportScreenshotPageState(); @@ -91,10 +91,13 @@ class _ReportScreenshotPageState extends State { @override Widget build(BuildContext context) { - return WillPopScope( - onWillPop: () async { + return PopScope( + canPop: false, + onPopInvoked: (didPop) { + if (didPop) { + return; + } context.pop(isRefreshPreviousPage); - return false; }, child: GestureDetector( onTap: () => widgetHelper.unfocus(context), @@ -275,10 +278,10 @@ class _ReportScreenshotPageState extends State { if (state is LoadingCenterReportScreenshotState) { return const WidgetCustomCircularProgressIndicator(); } else if (state is FailureReportScreenshotState) { - final errorMessage = state.errorMessage; + final errorMessage = state.errorMessage.convertErrorMessageToHumanMessage(); return WidgetError( title: 'oops'.tr(), - message: errorMessage, + message: errorMessage.hideResponseCode(), onTryAgain: doLoadData, ); } else if (state is SuccessLoadReportScreenshotState) { @@ -314,12 +317,12 @@ class _ReportScreenshotPageState extends State { if (state is LoadingCenterMemberState) { return const WidgetCustomCircularProgressIndicator(); } else if (state is FailureMemberState) { - final errorMessage = state.errorMessage; + final errorMessage = state.errorMessage.convertErrorMessageToHumanMessage(); return Padding( padding: EdgeInsets.symmetric(horizontal: helper.getDefaultPaddingLayout), child: WidgetError( title: 'oops'.tr(), - message: errorMessage, + message: errorMessage.hideResponseCode(), onTryAgain: prepareData, ), ); @@ -530,7 +533,10 @@ class _ReportScreenshotPageState extends State { String? thumbnail; final listFiles = element.files ?? []; if (listFiles.isNotEmpty) { - thumbnail = listFiles.first.url; + thumbnail = listFiles.first.urlBlur ?? ''; + if (thumbnail.isEmpty) { + thumbnail = listFiles.first.url; + } } const heightImage = 92.0; @@ -606,8 +612,14 @@ class _ReportScreenshotPageState extends State { context.pushNamed( PhotoViewPage.routeName, extra: { - PhotoViewPage.parameterListPhotos: - listFiles.where((element) => element.url != null).map((e) => e.url!).toList(), + PhotoViewPage.parameterListPhotos: listFiles + .where((element) { + return element.url != null; + }) + .map((e) => e) + .toList(), + PhotoViewPage.parameterIsShowIconDownload: + userId == element.userId?.toString() || userRole == UserRole.superAdmin, }, ); }, @@ -716,9 +728,13 @@ class _ReportScreenshotPageState extends State { Widget buildWidgetActivity(int activity, int durationInSeconds) { final duration = helper.convertSecondToHms(durationInSeconds); + final durationHour = duration.hour; final durationMinute = duration.minute; final durationSecond = duration.second; final listStrDuration = []; + if (durationHour > 0) { + listStrDuration.add('alias_hour_n'.tr(args: [durationHour.toString()])); + } if (durationMinute > 0) { listStrDuration.add('alias_minute_n'.tr(args: [durationMinute.toString()])); } @@ -797,46 +813,93 @@ class _ReportScreenshotPageState extends State { } final trackId = element.id; + final note = element.note ?? ''; return Align( alignment: Alignment.center, child: Padding( padding: EdgeInsets.only(top: heightImage), child: Row( mainAxisSize: MainAxisSize.max, - mainAxisAlignment: MainAxisAlignment.end, + mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ - Material( - borderRadius: BorderRadius.circular(999), - child: InkWell( - borderRadius: BorderRadius.circular(999), - onTap: () { - if (trackId == null) { - widgetHelper.showSnackBar( - context, - 'track_id_invalid'.tr(), - ); - return; - } - - trackingBloc.add( - DeleteTrackUserTrackingEvent( - trackId: trackId, + note.isEmpty + ? Container() + : Tooltip( + message: note, + child: Padding( + padding: const EdgeInsets.all(8.0), + child: FaIcon( + FontAwesomeIcons.penToSquare, + color: Theme.of(context).colorScheme.primary, + size: 14, + ), ), - ); - }, - child: const Padding( - padding: EdgeInsets.all(8.0), - child: FaIcon( - FontAwesomeIcons.trashCan, - color: Colors.red, - size: 14, ), - ), + buildWidgetIconDelete( + const FaIcon( + FontAwesomeIcons.trashCan, + color: Colors.red, + size: 14, ), + onTap: () { + if (trackId == null) { + widgetHelper.showSnackBar( + context, + 'track_id_invalid'.tr(), + ); + return; + } + + widgetHelper.showDialogConfirmation( + context, + 'title_delete_track'.tr(), + 'content_delete_track'.tr(), + [ + TextButton( + onPressed: () => context.pop(false), + child: Text('cancel'.tr()), + ), + TextButton( + onPressed: () => context.pop(true), + style: TextButton.styleFrom( + foregroundColor: Colors.red, + ), + child: Text('delete'.tr()), + ), + ], + ).then((value) { + if (value != null && value) { + trackingBloc.add( + DeleteTrackUserTrackingEvent( + trackId: trackId, + ), + ); + } + }); + }, ), ], ), ), ); } + + Widget buildWidgetIconDelete( + Widget icon, { + Function()? onTap, + ValueChanged? onHover, + }) { + return Material( + borderRadius: BorderRadius.circular(999), + child: InkWell( + borderRadius: BorderRadius.circular(999), + onTap: onTap, + onHover: onHover, + child: Padding( + padding: const EdgeInsets.all(8.0), + child: icon, + ), + ), + ); + } } diff --git a/lib/feature/presentation/page/reset_password/reset_password_page.dart b/lib/feature/presentation/page/reset_password/reset_password_page.dart index 1a04511..f5d48d2 100644 --- a/lib/feature/presentation/page/reset_password/reset_password_page.dart +++ b/lib/feature/presentation/page/reset_password/reset_password_page.dart @@ -22,9 +22,9 @@ class ResetPasswordPage extends StatefulWidget { final String code; const ResetPasswordPage({ - Key? key, + super.key, required this.code, - }) : super(key: key); + }); @override State createState() => _ResetPasswordPageState(); diff --git a/lib/feature/presentation/page/reset_password_success/reset_password_success_page.dart b/lib/feature/presentation/page/reset_password_success/reset_password_success_page.dart index aadb3d5..01fff87 100644 --- a/lib/feature/presentation/page/reset_password_success/reset_password_success_page.dart +++ b/lib/feature/presentation/page/reset_password_success/reset_password_success_page.dart @@ -12,8 +12,8 @@ class ResetPasswordSuccessPage extends StatelessWidget { static const routeName = 'reset-password-success'; ResetPasswordSuccessPage({ - Key? key, - }) : super(key: key); + super.key, + }); final helper = sl(); diff --git a/lib/feature/presentation/page/setting/setting_page.dart b/lib/feature/presentation/page/setting/setting_page.dart index 232fcce..ba815f3 100644 --- a/lib/feature/presentation/page/setting/setting_page.dart +++ b/lib/feature/presentation/page/setting/setting_page.dart @@ -4,13 +4,20 @@ import 'package:dipantau_desktop_client/core/util/enum/global_variable.dart'; import 'package:dipantau_desktop_client/core/util/enum/user_role.dart'; import 'package:dipantau_desktop_client/core/util/helper.dart'; import 'package:dipantau_desktop_client/core/util/shared_preferences_manager.dart'; +import 'package:dipantau_desktop_client/core/util/string_extension.dart'; import 'package:dipantau_desktop_client/core/util/widget_helper.dart'; +import 'package:dipantau_desktop_client/feature/data/model/user_setting/user_setting_body.dart'; +import 'package:dipantau_desktop_client/feature/data/model/user_setting/user_setting_response.dart'; import 'package:dipantau_desktop_client/feature/presentation/bloc/appearance/appearance_bloc.dart'; +import 'package:dipantau_desktop_client/feature/presentation/bloc/setting/setting_bloc.dart'; import 'package:dipantau_desktop_client/feature/presentation/page/home/home_page.dart'; import 'package:dipantau_desktop_client/feature/presentation/page/member_setting/member_setting_page.dart'; import 'package:dipantau_desktop_client/feature/presentation/page/setting_discord/setting_discord_page.dart'; +import 'package:dipantau_desktop_client/feature/presentation/page/setting_member_blur_screenshot/setting_member_blur_screenshot_page.dart'; import 'package:dipantau_desktop_client/feature/presentation/page/setup_credential/setup_credential_page.dart'; import 'package:dipantau_desktop_client/feature/presentation/page/splash/splash_page.dart'; +import 'package:dipantau_desktop_client/feature/presentation/page/user_registration_setting/user_registration_setting_page.dart'; +import 'package:dipantau_desktop_client/feature/presentation/widget/widget_custom_circular_progress_indicator.dart'; import 'package:dipantau_desktop_client/feature/presentation/widget/widget_primary_button.dart'; import 'package:dipantau_desktop_client/feature/presentation/widget/widget_theme_container.dart'; import 'package:dipantau_desktop_client/injection_container.dart'; @@ -21,19 +28,21 @@ import 'package:flutter/scheduler.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:go_router/go_router.dart'; import 'package:launch_at_startup/launch_at_startup.dart'; +import 'package:tray_manager/tray_manager.dart'; import 'package:window_manager/window_manager.dart'; class SettingPage extends StatefulWidget { static const routePath = '/setting'; static const routeName = 'setting'; - const SettingPage({Key? key}) : super(key: key); + const SettingPage({super.key}); @override State createState() => _SettingPageState(); } class _SettingPageState extends State { + final settingBloc = sl(); final helper = sl(); final navigationRailDestinations = []; final sharedPreferencesManager = sl(); @@ -59,6 +68,7 @@ class _SettingPageState extends State { var isEnableReminderTrackFri = true; var isEnableReminderTrackSat = false; var isEnableReminderTrackSun = false; + UserSettingResponse? userSetting; @override void setState(VoidCallback fn) { @@ -76,6 +86,7 @@ class _SettingPageState extends State { final strUserRole = sharedPreferencesManager.getString(SharedPreferencesManager.keyUserRole) ?? ''; userRole = strUserRole.fromStringUserRole; prepareData(); + doLoadUserSetting(); WidgetsBinding.instance.addPostFrameCallback((_) { setupNavigationRailDestinations(); setState(() {}); @@ -85,59 +96,96 @@ class _SettingPageState extends State { @override Widget build(BuildContext context) { - return Scaffold( - body: navigationRailDestinations.isEmpty - ? Container() - : Row( - children: [ - Column( + return BlocProvider( + create: (context) => settingBloc, + child: BlocListener( + listener: (context, state) { + if (state is SuccessLoadUserSettingState) { + userSetting = state.response; + setState(() {}); + } else if (state is SuccessUpdateUserSettingState) { + final newUserSetting = UserSettingResponse( + id: userSetting!.id!, + isEnableBlurScreenshot: !(userSetting!.isEnableBlurScreenshot!), + userId: userSetting!.userId, + name: userSetting!.name, + isOverrideBlurScreenshot: userSetting?.isOverrideBlurScreenshot ?? false, + ); + userSetting = newUserSetting; + } else if (state is FailureSettingState) { + final errorMessage = state.errorMessage.convertErrorMessageToHumanMessage(); + if (errorMessage.contains('401')) { + widgetHelper.showDialog401(context); + return; + } + widgetHelper.showSnackBar(context, errorMessage.hideResponseCode()); + } else if (state is FailureSnackBarSettingState) { + final errorMessage = state.errorMessage.convertErrorMessageToHumanMessage(); + if (errorMessage.contains('401')) { + widgetHelper.showDialog401(context); + return; + } + widgetHelper.showSnackBar(context, errorMessage.hideResponseCode()); + } + }, + child: Scaffold( + body: navigationRailDestinations.isEmpty + ? Container() + : Row( children: [ - Expanded( - child: SizedBox( - width: 172, - child: NavigationRail( - destinations: navigationRailDestinations, - selectedIndex: selectedIndexNavigationRail, - onDestinationSelected: (newValue) { - setState(() => selectedIndexNavigationRail = newValue); - }, - extended: true, - ), - ), - ), - Padding( - padding: const EdgeInsets.only(bottom: 16.0), - child: TextButton( - onPressed: () => context.pop(), - child: Row( - children: [ - const Icon( - Icons.arrow_back_ios_new, - size: 14, + Column( + children: [ + Expanded( + child: SizedBox( + width: 172, + child: NavigationRail( + destinations: navigationRailDestinations, + selectedIndex: selectedIndexNavigationRail, + onDestinationSelected: (newValue) { + if (newValue == 0) { + doLoadUserSetting(); + } + setState(() => selectedIndexNavigationRail = newValue); + }, + extended: true, ), - const SizedBox(width: 4), - Text( - 'back_to_main_menu'.tr(), + ), + ), + Padding( + padding: const EdgeInsets.only(bottom: 16.0), + child: TextButton( + onPressed: () => context.pop(), + child: Row( + children: [ + const Icon( + Icons.arrow_back_ios_new, + size: 14, + ), + const SizedBox(width: 4), + Text( + 'back_to_main_menu'.tr(), + ), + ], ), - ], + ), ), + ], + ), + const VerticalDivider( + thickness: 1, + width: 1, + ), + Expanded( + flex: 3, + child: Padding( + padding: const EdgeInsets.all(8.0), + child: buildWidgetBody(), ), ), ], ), - const VerticalDivider( - thickness: 1, - width: 1, - ), - Expanded( - flex: 3, - child: Padding( - padding: const EdgeInsets.all(8.0), - child: buildWidgetBody(), - ), - ), - ], - ), + ), + ), ); } @@ -193,6 +241,8 @@ class _SettingPageState extends State { const SizedBox(height: 16), buildWidgetAlwaysOnTop(), const SizedBox(height: 16), + buildWidgetUserSetting(), + const SizedBox(height: 16), buildWidgetChooseAppearance(), const SizedBox(height: 16), buildWidgetCheckForUpdate(), @@ -681,6 +731,11 @@ class _SettingPageState extends State { buildWidgetTask(), const SizedBox(height: 16), buildWidgetDiscordChannelId(), + const SizedBox(height: 16), + buildWidgetMemberBlurScreenshot(), + // TODO: untuk sementara tutup dulu fitur ini karena belum selesai endpoint-nya + /*const SizedBox(height: 16), + buildWidgetUserRegistration(),*/ ], ); } @@ -724,54 +779,17 @@ class _SettingPageState extends State { } Widget buildWidgetSetHostName() { - return Row( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - 'hostname'.tr(), - style: Theme.of(context).textTheme.bodyLarge, - ), - Text( - hostname, - style: Theme.of(context).textTheme.bodyMedium?.copyWith( - color: Colors.grey, - ), - maxLines: 1, - overflow: TextOverflow.ellipsis, - ), - ], - ), - ), - const SizedBox(width: 16), - InkWell( - borderRadius: BorderRadius.circular(999), - onTap: () { - context.pushNamed( - SetupCredentialPage.routeName, - extra: { - SetupCredentialPage.parameterIsShowWarning: true, - }, - ); + return buildWidgetItemSettingArrow( + 'hostname'.tr(), + hostname, + onTap: () { + context.pushNamed( + SetupCredentialPage.routeName, + extra: { + SetupCredentialPage.parameterIsShowWarning: true, }, - child: Container( - padding: const EdgeInsets.symmetric( - horizontal: 16, - vertical: 8, - ), - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(999), - ), - child: const Icon( - Icons.keyboard_arrow_right, - color: Colors.grey, - ), - ), - ), - ], + ); + }, ); } @@ -952,6 +970,25 @@ class _SettingPageState extends State { } Future doLogout() async { + if (isGlobalTimerStart) { + showDialog( + context: context, + builder: (context) { + return AlertDialog( + title: Text('warning'.tr()), + content: Text('please_stop_the_timer_if_you_want_to_logout'.tr()), + actions: [ + TextButton( + onPressed: () => context.pop(), + child: Text('dismiss'.tr()), + ), + ], + ); + }, + ); + return; + } + final isLogout = await showDialog( context: context, builder: (context) { @@ -982,6 +1019,7 @@ class _SettingPageState extends State { if (isLogout != null && mounted) { await helper.setLogout(); if (mounted) { + trayManager.setTitle('--:--:--'); context.goNamed(SplashPage.routeName); } } @@ -1108,143 +1146,58 @@ class _SettingPageState extends State { } Widget buildWidgetMember() { - return Row( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - 'members'.tr(), - style: Theme.of(context).textTheme.bodyLarge, - ), - Text( - 'add_edit_or_remove_member'.tr(), - style: Theme.of(context).textTheme.bodyMedium?.copyWith( - color: Colors.grey, - ), - ), - ], - ), - ), - const SizedBox(width: 16), - InkWell( - borderRadius: BorderRadius.circular(999), - onTap: () { - context.pushNamed(MemberSettingPage.routeName); - }, - child: Container( - padding: const EdgeInsets.symmetric( - horizontal: 16, - vertical: 8, - ), - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(999), - ), - child: const Icon( - Icons.keyboard_arrow_right, - color: Colors.grey, - ), - ), - ), - ], + return buildWidgetItemSettingArrow( + 'members'.tr(), + 'add_edit_or_remove_member'.tr(), + onTap: () { + context.pushNamed(MemberSettingPage.routeName); + }, ); } Widget buildWidgetProject() { - return Row( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - 'projects'.tr(), - style: Theme.of(context).textTheme.bodyLarge, - ), - Text( - 'add_edit_or_remove_project'.tr(), - style: Theme.of(context).textTheme.bodyMedium?.copyWith( - color: Colors.grey, - ), - ), - ], - ), - ), - const SizedBox(width: 16), - InkWell( - borderRadius: BorderRadius.circular(999), - onTap: () { - // TODO: Arahkan ke halaman project_setting_page.dart - widgetHelper.showSnackBar(context, 'coming_soon'.tr()); - }, - child: Container( - padding: const EdgeInsets.symmetric( - horizontal: 16, - vertical: 8, - ), - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(999), - ), - child: const Icon( - Icons.keyboard_arrow_right, - color: Colors.grey, - ), - ), - ), - ], + return buildWidgetItemSettingArrow( + 'projects'.tr(), + 'add_edit_or_remove_project'.tr(), + onTap: () { + // TODO: Arahkan ke halaman project_setting_page.dart + widgetHelper.showSnackBar(context, 'coming_soon'.tr()); + }, ); } Widget buildWidgetTask() { - return Row( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - 'tasks_2'.tr(), - style: Theme.of(context).textTheme.bodyLarge, - ), - Text( - 'add_edit_or_remove_task'.tr(), - style: Theme.of(context).textTheme.bodyMedium?.copyWith( - color: Colors.grey, - ), - ), - ], - ), - ), - const SizedBox(width: 16), - InkWell( - borderRadius: BorderRadius.circular(999), - onTap: () { - // TODO: Arahkan ke halaman task_setting_page.dart - widgetHelper.showSnackBar(context, 'coming_soon'.tr()); - }, - child: Container( - padding: const EdgeInsets.symmetric( - horizontal: 16, - vertical: 8, - ), - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(999), - ), - child: const Icon( - Icons.keyboard_arrow_right, - color: Colors.grey, - ), - ), - ), - ], + return buildWidgetItemSettingArrow( + 'tasks_2'.tr(), + 'add_edit_or_remove_task'.tr(), + onTap: () { + // TODO: Arahkan ke halaman task_setting_page.dart + widgetHelper.showSnackBar(context, 'coming_soon'.tr()); + }, ); } Widget buildWidgetDiscordChannelId() { + return buildWidgetItemSettingArrow( + 'discord_channel_id'.tr(), + 'subtitle_discord_channel_id'.tr(), + onTap: () { + context.pushNamed(SettingDiscordPage.routeName); + }, + ); + } + + Widget buildWidgetMemberBlurScreenshot() { + return buildWidgetItemSettingArrow( + 'screenshot_blur'.tr(), + 'subtitle_screenshot_blur'.tr(), + onTap: () { + context.pushNamed(SettingMemberBlurScreenshotPage.routeName); + }, + ); + } + + Widget buildWidgetItemSettingArrow(String title, String subtitle, {Function()? onTap}) { return Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ @@ -1253,11 +1206,11 @@ class _SettingPageState extends State { crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( - 'discord_channel_id'.tr(), + title, style: Theme.of(context).textTheme.bodyLarge, ), Text( - 'subtitle_discord_channel_id'.tr(), + subtitle, style: Theme.of(context).textTheme.bodyMedium?.copyWith( color: Colors.grey, ), @@ -1268,9 +1221,7 @@ class _SettingPageState extends State { const SizedBox(width: 16), InkWell( borderRadius: BorderRadius.circular(999), - onTap: () { - context.pushNamed(SettingDiscordPage.routeName); - }, + onTap: onTap, child: Container( padding: const EdgeInsets.symmetric( horizontal: 16, @@ -1289,6 +1240,16 @@ class _SettingPageState extends State { ); } + Widget buildWidgetUserRegistration() { + return buildWidgetItemSettingArrow( + 'user_registration'.tr(), + 'subtitle_user_registration'.tr(), + onTap: () { + context.pushNamed(UserRegistrationSettingPage.routeName); + }, + ); + } + void updateReminderTrack() async { final isEnableReminderNotTrack = valueNotifierIsEnableReminderTrack.value; await sharedPreferencesManager.putBool(SharedPreferencesManager.keyIsEnableReminderTrack, isEnableReminderNotTrack); @@ -1366,4 +1327,97 @@ class _SettingPageState extends State { } countTimeReminderTrackInSeconds = 0; } + + Widget buildWidgetUserSetting() { + final isOverrideBlurScreenshot = userSetting?.isOverrideBlurScreenshot ?? false; + var description = 'description_screenshot_blur_user'.tr(); + if (isOverrideBlurScreenshot && (userRole == UserRole.admin || userRole == UserRole.employee)) { + description += ' ${'this_setting_is_override_by_super_admin'.tr()}'; + } + return Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'screenshot_blur'.tr(), + style: Theme.of(context).textTheme.bodyLarge, + ), + Text( + description, + style: Theme.of(context).textTheme.bodyMedium?.copyWith( + color: Colors.grey, + ), + ), + ], + ), + ), + const SizedBox(width: 16), + SizedBox( + width: 76, + child: Align( + alignment: Alignment.centerRight, + child: BlocBuilder( + builder: (context, state) { + if (state is LoadingCenterSettingState || state is LoadingButtonSettingState) { + return const SizedBox( + width: 32, + height: 32, + child: WidgetCustomCircularProgressIndicator(), + ); + } else if ((state is FailureSettingState || state is FailureSnackBarSettingState) && + userSetting == null) { + return TextButton( + onPressed: doLoadUserSetting, + child: Text('refresh'.tr()), + ); + } + + if (userSetting == null) { + return Container(); + } + + return Switch.adaptive( + value: userSetting?.isEnableBlurScreenshot ?? false, + activeColor: Theme.of(context).colorScheme.primary, + onChanged: isOverrideBlurScreenshot && (userRole == UserRole.admin || userRole == UserRole.employee) + ? null + : (value) { + final id = userSetting?.id; + final userId = userSetting?.userId; + if (id == null || userId == null) { + widgetHelper.showSnackBar(context, 'invalid_id_or_user_id'.tr()); + return; + } + + final body = UserSettingBody( + data: [ + ItemUserSettingBody( + id: userSetting!.id!, + isEnableBlurScreenshot: value, + userId: userSetting!.userId!, + ), + ], + isOverrideBlurScreenshot: null, + ); + settingBloc.add( + UpdateUserSettingEvent( + body: body, + ), + ); + }, + ); + }, + ), + ), + ), + ], + ); + } + + void doLoadUserSetting() { + settingBloc.add(LoadUserSettingEvent()); + } } diff --git a/lib/feature/presentation/page/setting_discord/setting_discord_page.dart b/lib/feature/presentation/page/setting_discord/setting_discord_page.dart index db34c27..06a7060 100644 --- a/lib/feature/presentation/page/setting_discord/setting_discord_page.dart +++ b/lib/feature/presentation/page/setting_discord/setting_discord_page.dart @@ -16,7 +16,7 @@ class SettingDiscordPage extends StatefulWidget { static const routePath = '/setting-discord'; static const routeName = 'setting-discord'; - const SettingDiscordPage({Key? key}) : super(key: key); + const SettingDiscordPage({super.key}); @override State createState() => _SettingDiscordPageState(); @@ -94,10 +94,10 @@ class _SettingDiscordPageState extends State { }, builder: (context, state) { if (state is FailureSettingState) { - final errorMessage = state.errorMessage; + final errorMessage = state.errorMessage.convertErrorMessageToHumanMessage(); return WidgetError( title: 'oops'.tr(), - message: errorMessage, + message: errorMessage.hideResponseCode(), onTryAgain: doLoadData, ); } else if (state is LoadingCenterSettingState) { @@ -178,6 +178,7 @@ class _SettingDiscordPageState extends State { UpdateKvSettingEvent( body: KvSettingBody( discordChannelId: discordChannelId, + signUpMethod: null, ), ), ); diff --git a/lib/feature/presentation/page/setting_member_blur_screenshot/setting_member_blur_screenshot_page.dart b/lib/feature/presentation/page/setting_member_blur_screenshot/setting_member_blur_screenshot_page.dart new file mode 100644 index 0000000..6db57be --- /dev/null +++ b/lib/feature/presentation/page/setting_member_blur_screenshot/setting_member_blur_screenshot_page.dart @@ -0,0 +1,321 @@ +import 'package:dipantau_desktop_client/core/util/helper.dart'; +import 'package:dipantau_desktop_client/core/util/string_extension.dart'; +import 'package:dipantau_desktop_client/core/util/widget_helper.dart'; +import 'package:dipantau_desktop_client/feature/data/model/user_setting/user_setting_body.dart'; +import 'package:dipantau_desktop_client/feature/data/model/user_setting/user_setting_response.dart'; +import 'package:dipantau_desktop_client/feature/presentation/bloc/setting/setting_bloc.dart'; +import 'package:dipantau_desktop_client/feature/presentation/widget/widget_custom_circular_progress_indicator.dart'; +import 'package:dipantau_desktop_client/feature/presentation/widget/widget_error.dart'; +import 'package:dipantau_desktop_client/feature/presentation/widget/widget_primary_button.dart'; +import 'package:dipantau_desktop_client/injection_container.dart'; +import 'package:easy_localization/easy_localization.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; + +class SettingMemberBlurScreenshotPage extends StatefulWidget { + static const routePath = '/member-blur-screenshot'; + static const routeName = 'member-blur-screenshot'; + + const SettingMemberBlurScreenshotPage({super.key}); + + @override + State createState() => _SettingMemberBlurScreenshotPageState(); +} + +class _SettingMemberBlurScreenshotPageState extends State { + final settingBloc = sl(); + final widgetHelper = WidgetHelper(); + final helper = sl(); + final listData = <_ItemSettingMember>[]; + + var isLoadingButton = false; + var isOverride = false; + + @override + void setState(VoidCallback fn) { + if (mounted) { + super.setState(fn); + } + } + + @override + void initState() { + doLoadData(); + super.initState(); + } + + void doLoadData() { + settingBloc.add(LoadAllUserSettingEvent()); + } + + @override + Widget build(BuildContext context) { + return IgnorePointer( + ignoring: isLoadingButton, + child: BlocProvider( + create: (context) => settingBloc, + child: BlocListener( + listener: (context, state) { + setState(() => isLoadingButton = state is LoadingButtonSettingState); + if (state is FailureSettingState) { + final errorMessage = state.errorMessage.convertErrorMessageToHumanMessage(); + if (errorMessage.contains('401')) { + widgetHelper.showDialog401(context); + return; + } + } else if (state is FailureSnackBarSettingState) { + final errorMessage = state.errorMessage.convertErrorMessageToHumanMessage(); + if (errorMessage.contains('401')) { + widgetHelper.showDialog401(context); + return; + } + widgetHelper.showSnackBar(context, errorMessage.hideResponseCode()); + } else if (state is SuccessLoadAllUserSettingState) { + isOverride = state.response.isOverrideBlurScreenshot ?? false; + listData.clear(); + for (final element in state.response.data ?? []) { + final id = element.id ?? -1; + final userId = element.userId ?? -1; + final name = element.name ?? ''; + if (id == -1 || userId == -1 || name.isEmpty) { + continue; + } + listData.add( + _ItemSettingMember( + id: id, + userId: userId, + name: name, + isEnableBlurScreenshot: element.isEnableBlurScreenshot ?? false, + ), + ); + } + } else if (state is SuccessUpdateUserSettingState) { + widgetHelper.showSnackBar(context, 'user_setting_updated_successfully'.tr()); + } + }, + child: Scaffold( + appBar: AppBar( + title: Text( + 'screenshot_blur'.tr(), + ), + centerTitle: false, + ), + body: Padding( + padding: EdgeInsets.symmetric(horizontal: helper.getDefaultPaddingLayout), + child: BlocBuilder( + builder: (context, state) { + if (state is LoadingCenterSettingState) { + return const WidgetCustomCircularProgressIndicator(); + } else if (state is FailureSettingState) { + final errorMessage = state.errorMessage.convertErrorMessageToHumanMessage(); + return WidgetError( + title: 'oops'.tr(), + message: errorMessage.hideResponseCode(), + onTryAgain: doLoadData, + ); + } + if (listData.isEmpty) { + return WidgetError( + title: 'info'.tr(), + message: 'no_data_to_display'.tr(), + ); + } + + return Column( + children: [ + SizedBox(height: helper.getDefaultPaddingLayoutTop), + buildWidgetOverrideSetting(), + isOverride + ? Expanded( + child: Column( + children: [ + const SizedBox(height: 16), + const Divider(), + const SizedBox(height: 8), + buildWidgetActionAll(), + const SizedBox(height: 4), + Expanded( + child: buildWidgetListData(), + ), + ], + ), + ) + : Expanded( + child: Container(), + ), + Padding( + padding: EdgeInsets.only( + top: 16, + bottom: helper.getDefaultPaddingLayoutBottom, + ), + child: SizedBox( + width: double.infinity, + child: WidgetPrimaryButton( + onPressed: submit, + isLoading: isLoadingButton, + child: Text( + 'save'.tr(), + ), + ), + ), + ), + ], + ); + }, + ), + ), + ), + ), + ), + ); + } + + Widget buildWidgetActionAll() { + return Row( + children: [ + Text( + 'member_n'.plural(listData.length), + style: Theme.of(context).textTheme.bodyMedium?.copyWith( + color: Colors.grey, + ), + ), + Expanded( + child: Container(), + ), + TextButton( + onPressed: () { + for (final itemData in listData) { + itemData.isEnableBlurScreenshot = false; + } + setState(() {}); + }, + child: Text('disable_all'.tr()), + ), + Container( + width: 1, + height: 24, + color: Colors.grey, + margin: const EdgeInsets.symmetric(horizontal: 8), + ), + TextButton( + onPressed: () { + for (final itemData in listData) { + itemData.isEnableBlurScreenshot = true; + } + setState(() {}); + }, + child: Text('enable_all'.tr()), + ), + ], + ); + } + + void submit() { + UserSettingBody body; + if (isOverride) { + body = UserSettingBody( + data: listData + .map( + (e) => ItemUserSettingBody( + id: e.id, + isEnableBlurScreenshot: e.isEnableBlurScreenshot, + userId: e.userId, + ), + ) + .toList(), + isOverrideBlurScreenshot: isOverride, + ); + } else { + body = UserSettingBody( + data: [], + isOverrideBlurScreenshot: false, + ); + } + settingBloc.add( + UpdateUserSettingEvent( + body: body, + ), + ); + } + + Widget buildWidgetListData() { + return ListView.separated( + padding: EdgeInsets.only( + top: helper.getDefaultPaddingLayoutTop, + ), + itemBuilder: (context, index) { + final element = listData[index]; + final isEnableBlurScreenshot = element.isEnableBlurScreenshot; + return Row( + children: [ + Expanded( + child: Text(element.name), + ), + Switch.adaptive( + value: isEnableBlurScreenshot, + activeColor: Theme.of(context).colorScheme.primary, + onChanged: (newValue) { + listData[index].isEnableBlurScreenshot = newValue; + setState(() {}); + }, + ), + ], + ); + }, + separatorBuilder: (context, index) => const Divider(), + itemCount: listData.length, + ); + } + + Widget buildWidgetOverrideSetting() { + return Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'override'.tr(), + style: Theme.of(context).textTheme.bodyLarge, + ), + Text( + 'description_override_member_blur_screenshot'.tr(), + style: Theme.of(context).textTheme.bodyMedium?.copyWith( + color: Colors.grey, + ), + ), + ], + ), + ), + const SizedBox(width: 16), + Switch.adaptive( + value: isOverride, + activeColor: Theme.of(context).colorScheme.primary, + onChanged: (value) { + setState(() => isOverride = value); + }, + ), + ], + ); + } +} + +class _ItemSettingMember { + final int id; + final int userId; + final String name; + bool isEnableBlurScreenshot; + + _ItemSettingMember({ + required this.id, + required this.userId, + required this.name, + required this.isEnableBlurScreenshot, + }); + + @override + String toString() { + return '_ItemSettingMember{id: $id, userId: $userId, name: $name, isEnableBlurScreenshot: $isEnableBlurScreenshot}'; + } +} diff --git a/lib/feature/presentation/page/setup_credential/setup_credential_page.dart b/lib/feature/presentation/page/setup_credential/setup_credential_page.dart index 110904b..4a7ec98 100644 --- a/lib/feature/presentation/page/setup_credential/setup_credential_page.dart +++ b/lib/feature/presentation/page/setup_credential/setup_credential_page.dart @@ -2,10 +2,14 @@ import 'package:dipantau_desktop_client/core/util/enum/global_variable.dart'; import 'package:dipantau_desktop_client/core/util/helper.dart'; import 'package:dipantau_desktop_client/core/util/shared_preferences_manager.dart'; import 'package:dipantau_desktop_client/core/util/widget_helper.dart'; +import 'package:dipantau_desktop_client/feature/presentation/bloc/setup_credential/setup_credential_bloc.dart'; +import 'package:dipantau_desktop_client/feature/presentation/widget/widget_loading_center_full_screen.dart'; import 'package:dipantau_desktop_client/feature/presentation/widget/widget_primary_button.dart'; import 'package:dipantau_desktop_client/injection_container.dart' as di; +import 'package:dipantau_desktop_client/injection_container.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:go_router/go_router.dart'; class SetupCredentialPage extends StatefulWidget { @@ -18,10 +22,10 @@ class SetupCredentialPage extends StatefulWidget { final bool isShowWarning; const SetupCredentialPage({ - Key? key, + super.key, this.isFromSplashScreen = false, this.isShowWarning = false, - }) : super(key: key); + }); @override State createState() => _SetupCredentialPageState(); @@ -32,6 +36,7 @@ class _SetupCredentialPageState extends State { final controllerHostname = TextEditingController(); final widgetHelper = WidgetHelper(); final formState = GlobalKey(); + final setupCredentialBloc = sl(); var isLogin = false; var defaultDomainApi = ''; @@ -52,37 +57,81 @@ class _SetupCredentialPageState extends State { appBar: AppBar( automaticallyImplyLeading: widget.isFromSplashScreen ? false : true, ), - body: Padding( - padding: EdgeInsets.only( - left: helper.getDefaultPaddingLayout, - top: helper.getDefaultPaddingLayoutTop, - right: helper.getDefaultPaddingLayout, - bottom: helper.getDefaultPaddingLayout, + body: BlocProvider( + create: (context) => setupCredentialBloc, + child: BlocListener( + listener: (context, state) { + if (state is FailureSetupCredentialState) { + final errorMessage = 'invalid_hostname'.tr(); + widgetHelper.showDialogMessage( + context, + 'info'.tr(), + errorMessage, + ); + } else if (state is SuccessPingSetupCredentialState) { + final hostname = state.baseUrl; + sharedPreferencesManager.putString(SharedPreferencesManager.keyDomainApi, hostname).then((value) { + helper.setDomainApiToFlavor(hostname); + di.init(); + if (mounted) { + context.go('/'); + } + }); + } + }, + child: Stack( + children: [ + buildWidgetBody(), + buildWidgetLoadingOverlay(), + ], + ), ), - child: SizedBox( - width: double.infinity, - child: Form( - key: formState, - autovalidateMode: AutovalidateMode.onUserInteraction, - child: Column( - mainAxisSize: MainAxisSize.max, - mainAxisAlignment: MainAxisAlignment.center, - children: [ - buildWidgetTitle(), - const SizedBox(height: 8), - Text( - 'subtitle_set_hostname'.tr(), - style: Theme.of(context).textTheme.bodyMedium?.copyWith( - color: Colors.grey, - ), - textAlign: TextAlign.center, - ), - const SizedBox(height: 24), - buildWidgetTextFieldHostname(), - const SizedBox(height: 24), - buildWidgetButtonSave(), - ], - ), + ), + ); + } + + Widget buildWidgetLoadingOverlay() { + return BlocBuilder( + builder: (context, state) { + if (state is LoadingSetupCredentialState) { + return const WidgetLoadingCenterFullScreen(); + } + return Container(); + }, + ); + } + + Widget buildWidgetBody() { + return Padding( + padding: EdgeInsets.only( + left: helper.getDefaultPaddingLayout, + top: helper.getDefaultPaddingLayoutTop, + right: helper.getDefaultPaddingLayout, + bottom: helper.getDefaultPaddingLayout, + ), + child: SizedBox( + width: double.infinity, + child: Form( + key: formState, + autovalidateMode: AutovalidateMode.onUserInteraction, + child: Column( + mainAxisSize: MainAxisSize.max, + mainAxisAlignment: MainAxisAlignment.center, + children: [ + buildWidgetTitle(), + const SizedBox(height: 8), + Text( + 'subtitle_set_hostname'.tr(), + style: Theme.of(context).textTheme.bodyMedium?.copyWith( + color: Colors.grey, + ), + textAlign: TextAlign.center, + ), + const SizedBox(height: 24), + buildWidgetTextFieldHostname(), + const SizedBox(height: 24), + buildWidgetButtonSave(), + ], ), ), ), @@ -148,13 +197,12 @@ class _SetupCredentialPageState extends State { ) as bool?; } if (isContinue != null && isContinue) { - final hostname = controllerHostname.text.trim(); - await sharedPreferencesManager.putString(SharedPreferencesManager.keyDomainApi, hostname); - helper.setDomainApiToFlavor(hostname); - di.init(); - if (mounted) { - context.go('/'); - } + final hostname = helper.removeTrailingSlash(controllerHostname.text.trim()).trim(); + setupCredentialBloc.add( + PingSetupCredentialEvent( + baseUrl: hostname, + ), + ); } } } diff --git a/lib/feature/presentation/page/splash/splash_page.dart b/lib/feature/presentation/page/splash/splash_page.dart index 40519f0..f097eb7 100644 --- a/lib/feature/presentation/page/splash/splash_page.dart +++ b/lib/feature/presentation/page/splash/splash_page.dart @@ -13,7 +13,7 @@ class SplashPage extends StatefulWidget { static const routePath = '/splash'; static const routeName = 'splash'; - const SplashPage({Key? key}) : super(key: key); + const SplashPage({super.key}); @override State createState() => _SplashPageState(); diff --git a/lib/feature/presentation/page/sync/sync_page.dart b/lib/feature/presentation/page/sync/sync_page.dart index 02b57e9..7654729 100644 --- a/lib/feature/presentation/page/sync/sync_page.dart +++ b/lib/feature/presentation/page/sync/sync_page.dart @@ -7,6 +7,7 @@ import 'package:dipantau_desktop_client/core/util/shared_preferences_manager.dar import 'package:dipantau_desktop_client/core/util/string_extension.dart'; import 'package:dipantau_desktop_client/core/util/widget_helper.dart'; import 'package:dipantau_desktop_client/feature/data/model/create_track/bulk_create_track_data_body.dart'; +import 'package:dipantau_desktop_client/feature/data/model/track_user/track_user_response.dart'; import 'package:dipantau_desktop_client/feature/database/entity/track/track.dart'; import 'package:dipantau_desktop_client/feature/presentation/bloc/sync_manual/sync_manual_bloc.dart'; import 'package:dipantau_desktop_client/feature/presentation/page/photo_view/photo_view_page.dart'; @@ -25,7 +26,7 @@ class SyncPage extends StatefulWidget { static const routePath = '/sync'; static const routeName = 'sync'; - const SyncPage({Key? key}) : super(key: key); + const SyncPage({super.key}); @override State createState() => _SyncPageState(); @@ -333,7 +334,16 @@ class _SyncPageState extends State { context.pushNamed( PhotoViewPage.routeName, extra: { - PhotoViewPage.parameterListPhotos: listScreenshots.map((e) => e.path).toList(), + PhotoViewPage.parameterListPhotos: listScreenshots.map((e) { + return ItemFileTrackUserResponse( + id: null, + url: e.path, + sizeInByte: 0, + urlBlur: null, + sizeBlurInByte: 0, + ); + }).toList(), + PhotoViewPage.parameterIsShowIconDownload: true, }, ); }, @@ -442,9 +452,13 @@ class _SyncPageState extends State { Widget buildWidgetActivity(int activity, int durationInSeconds) { final duration = helper.convertSecondToHms(durationInSeconds); + final durationHour = duration.hour; final durationMinute = duration.minute; final durationSecond = duration.second; final listStrDuration = []; + if (durationHour > 0) { + listStrDuration.add('alias_hour_n'.tr(args: [durationHour.toString()])); + } if (durationMinute > 0) { listStrDuration.add('alias_minute_n'.tr(args: [durationMinute.toString()])); } diff --git a/lib/feature/presentation/page/user_registration_setting/user_registration_setting_page.dart b/lib/feature/presentation/page/user_registration_setting/user_registration_setting_page.dart new file mode 100644 index 0000000..4fcb0cf --- /dev/null +++ b/lib/feature/presentation/page/user_registration_setting/user_registration_setting_page.dart @@ -0,0 +1,214 @@ +import 'package:dipantau_desktop_client/core/util/enum/sign_up_method.dart'; +import 'package:dipantau_desktop_client/core/util/helper.dart'; +import 'package:dipantau_desktop_client/core/util/string_extension.dart'; +import 'package:dipantau_desktop_client/core/util/widget_helper.dart'; +import 'package:dipantau_desktop_client/feature/data/model/kv_setting/kv_setting_body.dart'; +import 'package:dipantau_desktop_client/feature/presentation/bloc/setting/setting_bloc.dart'; +import 'package:dipantau_desktop_client/feature/presentation/widget/widget_custom_circular_progress_indicator.dart'; +import 'package:dipantau_desktop_client/feature/presentation/widget/widget_error.dart'; +import 'package:dipantau_desktop_client/feature/presentation/widget/widget_primary_button.dart'; +import 'package:dipantau_desktop_client/injection_container.dart'; +import 'package:easy_localization/easy_localization.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:go_router/go_router.dart'; + +class UserRegistrationSettingPage extends StatefulWidget { + static const routePath = '/user-registration-setting'; + static const routeName = 'user-registration-setting'; + + const UserRegistrationSettingPage({super.key}); + + @override + State createState() => _UserRegistrationSettingPageState(); +} + +class _UserRegistrationSettingPageState extends State { + final settingBloc = sl(); + final helper = sl(); + final widgetHelper = WidgetHelper(); + + var isLoadingButton = false; + var isPreparingSuccess = false; + SignUpMethod? signUpMethod; + + @override + void setState(VoidCallback fn) { + if (mounted) { + super.setState(fn); + } + } + + @override + void initState() { + doLoadData(); + super.initState(); + } + + void doLoadData() { + settingBloc.add(LoadKvSettingEvent()); + } + + @override + Widget build(BuildContext context) { + return IgnorePointer( + ignoring: isLoadingButton, + child: BlocProvider( + create: (context) => settingBloc, + child: BlocListener( + listener: (context, state) { + setState(() { + isLoadingButton = state is LoadingButtonSettingState; + }); + if (state is FailureSettingState) { + final errorMessage = state.errorMessage.convertErrorMessageToHumanMessage(); + if (errorMessage.contains('401')) { + widgetHelper.showDialog401(context); + return; + } + } else if (state is SuccessLoadKvSettingState) { + isPreparingSuccess = true; + final strSignUpMethod = state.response?.signUpMethod ?? ''; + signUpMethod = SignUpMethodExtension.parseString(strSignUpMethod); + signUpMethod ??= SignUpMethod.auto; + } else if (state is FailureSnackBarSettingState) { + final errorMessage = state.errorMessage.convertErrorMessageToHumanMessage(); + widgetHelper.showSnackBar(context, errorMessage.hideResponseCode()); + } else if (state is SuccessUpdateKvSettingState) { + widgetHelper.showSnackBar( + context, + 'user_registration_workflow_successfully_updated'.tr(), + ); + context.pop(); + } + }, + child: Scaffold( + appBar: AppBar( + title: Text( + 'user_registration_workflow'.tr(), + ), + centerTitle: false, + ), + body: Padding( + padding: EdgeInsets.all(helper.getDefaultPaddingLayout), + child: BlocBuilder( + buildWhen: (previousState, currentState) { + return currentState is LoadingCenterSettingState || + currentState is FailureSettingState || + currentState is SuccessLoadKvSettingState; + }, + builder: (context, state) { + if (state is FailureSettingState) { + final errorMessage = state.errorMessage.convertErrorMessageToHumanMessage(); + return WidgetError( + title: 'oops'.tr(), + message: errorMessage.hideResponseCode(), + onTryAgain: doLoadData, + ); + } else if (state is LoadingCenterSettingState) { + return const WidgetCustomCircularProgressIndicator(); + } + return !isPreparingSuccess ? Container() : buildWidgetForm(); + }, + ), + ), + ), + ), + ), + ); + } + + Widget buildWidgetForm() { + return Column( + children: [ + ListTile( + leading: Radio( + value: SignUpMethod.auto, + groupValue: signUpMethod, + onChanged: (value) { + setState(() { + signUpMethod = value; + }); + }, + ), + title: Text('auto_approval'.tr()), + titleAlignment: ListTileTitleAlignment.top, + subtitle: Text( + 'description_auto_approval'.tr(), + style: const TextStyle( + color: Colors.grey, + ), + ), + onTap: () { + setState(() { + signUpMethod = SignUpMethod.auto; + }); + }, + ), + ListTile( + leading: Radio( + value: SignUpMethod.manual, + groupValue: signUpMethod, + onChanged: (value) { + setState(() { + signUpMethod = value; + }); + }, + ), + title: Text('manual_approval'.tr()), + titleAlignment: ListTileTitleAlignment.top, + subtitle: Text( + 'description_manual_approval'.tr(), + style: const TextStyle( + color: Colors.grey, + ), + ), + onTap: () { + setState(() { + signUpMethod = SignUpMethod.manual; + }); + }, + ), + Expanded( + child: Container(), + ), + buildWidgetButtonSave(), + ], + ); + } + + Widget buildWidgetButtonSave() { + return SizedBox( + width: double.infinity, + child: WidgetPrimaryButton( + onPressed: saveData, + isLoading: isLoadingButton, + child: Text( + 'save'.tr(), + ), + ), + ); + } + + void saveData() { + String strSignUpMethod; + if (signUpMethod != null) { + strSignUpMethod = signUpMethod!.toValue(); + } else { + widgetHelper.showSnackBar( + context, + 'please_choose_user_registration_workflow'.tr(), + ); + return; + } + + settingBloc.add( + UpdateKvSettingEvent( + body: KvSettingBody( + discordChannelId: null, + signUpMethod: strSignUpMethod, + ), + ), + ); + } +} diff --git a/lib/feature/presentation/page/verify_forgot_password/verify_forgot_password_page.dart b/lib/feature/presentation/page/verify_forgot_password/verify_forgot_password_page.dart index efc62cc..4d27951 100644 --- a/lib/feature/presentation/page/verify_forgot_password/verify_forgot_password_page.dart +++ b/lib/feature/presentation/page/verify_forgot_password/verify_forgot_password_page.dart @@ -20,9 +20,9 @@ class VerifyForgotPasswordPage extends StatefulWidget { final String email; const VerifyForgotPasswordPage({ - Key? key, + super.key, required this.email, - }) : super(key: key); + }); @override State createState() => _VerifyForgotPasswordPageState(); diff --git a/lib/feature/presentation/widget/widget_choose_project.dart b/lib/feature/presentation/widget/widget_choose_project.dart index 6d67dc6..fb34d83 100644 --- a/lib/feature/presentation/widget/widget_choose_project.dart +++ b/lib/feature/presentation/widget/widget_choose_project.dart @@ -1,4 +1,5 @@ import 'package:dipantau_desktop_client/core/util/helper.dart'; +import 'package:dipantau_desktop_client/core/util/string_extension.dart'; import 'package:dipantau_desktop_client/feature/data/model/project/project_response.dart'; import 'package:dipantau_desktop_client/feature/presentation/bloc/project/project_bloc.dart'; import 'package:dipantau_desktop_client/feature/presentation/widget/widget_custom_circular_progress_indicator.dart'; @@ -13,8 +14,8 @@ class WidgetChooseProject extends StatefulWidget { const WidgetChooseProject({ required this.defaultSelectedProjectId, - Key? key, - }) : super(key: key); + super.key, + }); @override State createState() => _WidgetChooseProjectState(); @@ -63,12 +64,12 @@ class _WidgetChooseProjectState extends State { if (state is LoadingProjectState) { return const WidgetCustomCircularProgressIndicator(); } else if (state is FailureProjectState) { - final errorMessage = state.errorMessage; + final errorMessage = state.errorMessage.convertErrorMessageToHumanMessage(); return Padding( padding: EdgeInsets.symmetric(horizontal: helper.getDefaultPaddingLayout), child: WidgetError( title: 'info'.tr(), - message: errorMessage, + message: errorMessage.hideResponseCode(), ), ); } else if (state is SuccessLoadDataProjectState) { diff --git a/lib/feature/presentation/widget/widget_custom_circular_progress_indicator.dart b/lib/feature/presentation/widget/widget_custom_circular_progress_indicator.dart index 94dde8e..9391872 100644 --- a/lib/feature/presentation/widget/widget_custom_circular_progress_indicator.dart +++ b/lib/feature/presentation/widget/widget_custom_circular_progress_indicator.dart @@ -4,9 +4,9 @@ class WidgetCustomCircularProgressIndicator extends StatelessWidget { final Color? color; const WidgetCustomCircularProgressIndicator({ - Key? key, + super.key, this.color, - }) : super(key: key); + }); @override Widget build(BuildContext context) { diff --git a/lib/feature/presentation/widget/widget_error.dart b/lib/feature/presentation/widget/widget_error.dart index 219ab01..194db11 100644 --- a/lib/feature/presentation/widget/widget_error.dart +++ b/lib/feature/presentation/widget/widget_error.dart @@ -9,11 +9,11 @@ class WidgetError extends StatelessWidget { final Function()? onTryAgain; const WidgetError({ - Key? key, + super.key, required this.title, required this.message, this.onTryAgain, - }) : super(key: key); + }); @override Widget build(BuildContext context) { diff --git a/lib/feature/presentation/widget/widget_icon_circle.dart b/lib/feature/presentation/widget/widget_icon_circle.dart index f11585c..eee7421 100644 --- a/lib/feature/presentation/widget/widget_icon_circle.dart +++ b/lib/feature/presentation/widget/widget_icon_circle.dart @@ -9,8 +9,8 @@ class WidgetIconCircle extends StatelessWidget { required this.iconData, this.size = 32.0, this.padding = 16.0, - Key? key, - }) : super(key: key); + super.key, + }); @override Widget build(BuildContext context) { diff --git a/lib/feature/presentation/widget/widget_loading_center_full_screen.dart b/lib/feature/presentation/widget/widget_loading_center_full_screen.dart new file mode 100644 index 0000000..54c68fb --- /dev/null +++ b/lib/feature/presentation/widget/widget_loading_center_full_screen.dart @@ -0,0 +1,16 @@ +import 'package:dipantau_desktop_client/feature/presentation/widget/widget_custom_circular_progress_indicator.dart'; +import 'package:flutter/material.dart'; + +class WidgetLoadingCenterFullScreen extends StatelessWidget { + const WidgetLoadingCenterFullScreen({super.key}); + + @override + Widget build(BuildContext context) { + return Container( + width: double.infinity, + height: double.infinity, + color: Colors.black.withOpacity(.5), + child: const WidgetCustomCircularProgressIndicator(), + ); + } +} diff --git a/lib/feature/presentation/widget/widget_primary_button.dart b/lib/feature/presentation/widget/widget_primary_button.dart index fe920d9..aeffeb5 100644 --- a/lib/feature/presentation/widget/widget_primary_button.dart +++ b/lib/feature/presentation/widget/widget_primary_button.dart @@ -8,12 +8,12 @@ class WidgetPrimaryButton extends StatelessWidget { final ButtonStyle? buttonStyle; const WidgetPrimaryButton({ - Key? key, + super.key, required this.onPressed, required this.child, this.isLoading, this.buttonStyle, - }) : super(key: key); + }); @override Widget build(BuildContext context) { diff --git a/lib/feature/presentation/widget/widget_theme_container.dart b/lib/feature/presentation/widget/widget_theme_container.dart index cca0062..81e58c4 100644 --- a/lib/feature/presentation/widget/widget_theme_container.dart +++ b/lib/feature/presentation/widget/widget_theme_container.dart @@ -8,12 +8,12 @@ class WidgetThemeContainer extends StatefulWidget { final Color? borderColor; const WidgetThemeContainer({ - Key? key, + super.key, required this.mode, required this.width, required this.height, this.borderColor, - }) : super(key: key); + }); @override State createState() => _WidgetThemeContainerState(); diff --git a/lib/injection_container.dart b/lib/injection_container.dart index 8bf9748..a72e1a5 100644 --- a/lib/injection_container.dart +++ b/lib/injection_container.dart @@ -7,50 +7,61 @@ import 'package:dipantau_desktop_client/core/util/notification_helper.dart'; import 'package:dipantau_desktop_client/core/util/shared_preferences_manager.dart'; import 'package:dipantau_desktop_client/core/util/widget_helper.dart'; import 'package:dipantau_desktop_client/feature/data/datasource/auth/auth_remote_data_source.dart'; +import 'package:dipantau_desktop_client/feature/data/datasource/general/general_remote_data_source.dart'; import 'package:dipantau_desktop_client/feature/data/datasource/project/project_remote_data_source.dart'; import 'package:dipantau_desktop_client/feature/data/datasource/setting/setting_remote_data_source.dart'; import 'package:dipantau_desktop_client/feature/data/datasource/track/track_remote_data_source.dart'; import 'package:dipantau_desktop_client/feature/data/datasource/user/user_remote_data_source.dart'; import 'package:dipantau_desktop_client/feature/data/repository/auth/auth_repository_impl.dart'; +import 'package:dipantau_desktop_client/feature/data/repository/general/general_repository_impl.dart'; import 'package:dipantau_desktop_client/feature/data/repository/project/project_repository_impl.dart'; import 'package:dipantau_desktop_client/feature/data/repository/setting/setting_repository_impl.dart'; import 'package:dipantau_desktop_client/feature/data/repository/track/track_repository_impl.dart'; import 'package:dipantau_desktop_client/feature/data/repository/user/user_repository_impl.dart'; import 'package:dipantau_desktop_client/feature/database/app_database.dart'; import 'package:dipantau_desktop_client/feature/domain/repository/auth/auth_repository.dart'; +import 'package:dipantau_desktop_client/feature/domain/repository/general/general_repository.dart'; import 'package:dipantau_desktop_client/feature/domain/repository/project/project_repository.dart'; import 'package:dipantau_desktop_client/feature/domain/repository/setting/setting_repository.dart'; import 'package:dipantau_desktop_client/feature/domain/repository/track/track_repository.dart'; import 'package:dipantau_desktop_client/feature/domain/repository/user/user_repository.dart'; import 'package:dipantau_desktop_client/feature/domain/usecase/bulk_create_track_data/bulk_create_track_data.dart'; import 'package:dipantau_desktop_client/feature/domain/usecase/bulk_create_track_image/bulk_create_track_image.dart'; +import 'package:dipantau_desktop_client/feature/domain/usecase/create_manual_track/create_manual_track.dart'; import 'package:dipantau_desktop_client/feature/domain/usecase/create_track/create_track.dart'; import 'package:dipantau_desktop_client/feature/domain/usecase/delete_track_user/delete_track_user.dart'; import 'package:dipantau_desktop_client/feature/domain/usecase/forgot_password/forgot_password.dart'; import 'package:dipantau_desktop_client/feature/domain/usecase/get_all_member/get_all_member.dart'; +import 'package:dipantau_desktop_client/feature/domain/usecase/get_all_user_setting/get_all_user_setting.dart'; import 'package:dipantau_desktop_client/feature/domain/usecase/get_kv_setting/get_kv_setting.dart'; import 'package:dipantau_desktop_client/feature/domain/usecase/get_profile/get_profile.dart'; import 'package:dipantau_desktop_client/feature/domain/usecase/get_project/get_project.dart'; +import 'package:dipantau_desktop_client/feature/domain/usecase/get_project_task_by_user_id/get_project_task_by_user_id.dart'; import 'package:dipantau_desktop_client/feature/domain/usecase/get_track_user/get_track_user.dart'; import 'package:dipantau_desktop_client/feature/domain/usecase/get_track_user_lite/get_track_user_lite.dart'; +import 'package:dipantau_desktop_client/feature/domain/usecase/get_user_setting/get_user_setting.dart'; import 'package:dipantau_desktop_client/feature/domain/usecase/login/login.dart'; +import 'package:dipantau_desktop_client/feature/domain/usecase/ping/ping.dart'; import 'package:dipantau_desktop_client/feature/domain/usecase/refresh_token/refresh_token.dart'; import 'package:dipantau_desktop_client/feature/domain/usecase/reset_password/reset_password.dart'; import 'package:dipantau_desktop_client/feature/domain/usecase/send_app_version/send_app_version.dart'; import 'package:dipantau_desktop_client/feature/domain/usecase/set_kv_setting/set_kv_setting.dart'; import 'package:dipantau_desktop_client/feature/domain/usecase/sign_up/sign_up.dart'; import 'package:dipantau_desktop_client/feature/domain/usecase/update_user/update_user.dart'; +import 'package:dipantau_desktop_client/feature/domain/usecase/update_user_setting/update_user_setting.dart'; import 'package:dipantau_desktop_client/feature/domain/usecase/verify_forgot_password/verify_forgot_password.dart'; import 'package:dipantau_desktop_client/feature/presentation/bloc/appearance/appearance_bloc.dart'; import 'package:dipantau_desktop_client/feature/presentation/bloc/cron_tracking/cron_tracking_bloc.dart'; import 'package:dipantau_desktop_client/feature/presentation/bloc/forgot_password/forgot_password_bloc.dart'; import 'package:dipantau_desktop_client/feature/presentation/bloc/home/home_bloc.dart'; import 'package:dipantau_desktop_client/feature/presentation/bloc/login/login_bloc.dart'; +import 'package:dipantau_desktop_client/feature/presentation/bloc/manual_tracking/manual_tracking_bloc.dart'; import 'package:dipantau_desktop_client/feature/presentation/bloc/member/member_bloc.dart'; import 'package:dipantau_desktop_client/feature/presentation/bloc/project/project_bloc.dart'; import 'package:dipantau_desktop_client/feature/presentation/bloc/report_screenshot/report_screenshot_bloc.dart'; import 'package:dipantau_desktop_client/feature/presentation/bloc/reset_password/reset_password_bloc.dart'; import 'package:dipantau_desktop_client/feature/presentation/bloc/setting/setting_bloc.dart'; +import 'package:dipantau_desktop_client/feature/presentation/bloc/setup_credential/setup_credential_bloc.dart'; import 'package:dipantau_desktop_client/feature/presentation/bloc/sign_up/sign_up_bloc.dart'; import 'package:dipantau_desktop_client/feature/presentation/bloc/sync_manual/sync_manual_bloc.dart'; import 'package:dipantau_desktop_client/feature/presentation/bloc/tracking/tracking_bloc.dart'; @@ -118,6 +129,9 @@ void init() { helper: sl(), getKvSetting: sl(), setKvSetting: sl(), + getUserSetting: sl(), + getAllUserSetting: sl(), + updateUserSetting: sl(), ), ); sl.registerFactory( @@ -153,6 +167,19 @@ void init() { resetPassword: sl(), ), ); + sl.registerFactory( + () => ManualTrackingBloc( + helper: sl(), + createManualTrack: sl(), + getProjectTaskByUserId: sl(), + ), + ); + sl.registerFactory( + () => SetupCredentialBloc( + helper: sl(), + ping: sl(), + ), + ); // use case sl.registerLazySingleton(() => GetProject(repository: sl())); @@ -174,6 +201,12 @@ void init() { sl.registerLazySingleton(() => ForgotPassword(repository: sl())); sl.registerLazySingleton(() => VerifyForgotPassword(repository: sl())); sl.registerLazySingleton(() => ResetPassword(repository: sl())); + sl.registerLazySingleton(() => CreateManualTrack(repository: sl())); + sl.registerLazySingleton(() => GetProjectTaskByUserId(repository: sl())); + sl.registerLazySingleton(() => GetAllUserSetting(repository: sl())); + sl.registerLazySingleton(() => GetUserSetting(repository: sl())); + sl.registerLazySingleton(() => UpdateUserSetting(repository: sl())); + sl.registerLazySingleton(() => Ping(repository: sl())); // repository sl.registerLazySingleton( @@ -206,6 +239,12 @@ void init() { networkInfo: sl(), ), ); + sl.registerLazySingleton( + () => GeneralRepositoryImpl( + remoteDataSource: sl(), + networkInfo: sl(), + ), + ); // data source sl.registerLazySingleton( @@ -233,6 +272,11 @@ void init() { dio: sl(instanceName: dioRefreshToken), ), ); + sl.registerLazySingleton( + () => GeneralRemoteDataSourceImpl( + dio: sl(instanceName: dioLogging), + ), + ); // core sl.registerLazySingleton(() => NetworkInfoImpl(sl())); diff --git a/lib/main.dart b/lib/main.dart index b05d484..0d31dbe 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -6,6 +6,7 @@ import 'package:dipantau_desktop_client/config/flavor_config.dart'; import 'package:dipantau_desktop_client/core/util/enum/appearance_mode.dart'; import 'package:dipantau_desktop_client/core/util/enum/global_variable.dart'; import 'package:dipantau_desktop_client/core/util/shared_preferences_manager.dart'; +import 'package:dipantau_desktop_client/feature/data/model/track_user/track_user_response.dart'; import 'package:dipantau_desktop_client/feature/data/model/user_profile/user_profile_response.dart'; import 'package:dipantau_desktop_client/feature/presentation/bloc/appearance/appearance_bloc.dart'; import 'package:dipantau_desktop_client/feature/presentation/page/add_member/add_edit_member_page.dart'; @@ -14,6 +15,7 @@ import 'package:dipantau_desktop_client/feature/presentation/page/error/error_pa import 'package:dipantau_desktop_client/feature/presentation/page/forgot_password/forgot_password_page.dart'; import 'package:dipantau_desktop_client/feature/presentation/page/home/home_page.dart'; import 'package:dipantau_desktop_client/feature/presentation/page/login/login_page.dart'; +import 'package:dipantau_desktop_client/feature/presentation/page/manual_tracking/manual_tracking_page.dart'; import 'package:dipantau_desktop_client/feature/presentation/page/member_setting/member_setting_page.dart'; import 'package:dipantau_desktop_client/feature/presentation/page/photo_view/photo_view_page.dart'; import 'package:dipantau_desktop_client/feature/presentation/page/register/register_page.dart'; @@ -23,9 +25,11 @@ import 'package:dipantau_desktop_client/feature/presentation/page/reset_password import 'package:dipantau_desktop_client/feature/presentation/page/reset_password_success/reset_password_success_page.dart'; import 'package:dipantau_desktop_client/feature/presentation/page/setting/setting_page.dart'; import 'package:dipantau_desktop_client/feature/presentation/page/setting_discord/setting_discord_page.dart'; +import 'package:dipantau_desktop_client/feature/presentation/page/setting_member_blur_screenshot/setting_member_blur_screenshot_page.dart'; import 'package:dipantau_desktop_client/feature/presentation/page/setup_credential/setup_credential_page.dart'; import 'package:dipantau_desktop_client/feature/presentation/page/splash/splash_page.dart'; import 'package:dipantau_desktop_client/feature/presentation/page/sync/sync_page.dart'; +import 'package:dipantau_desktop_client/feature/presentation/page/user_registration_setting/user_registration_setting_page.dart'; import 'package:dipantau_desktop_client/feature/presentation/page/verify_forgot_password/verify_forgot_password_page.dart'; import 'package:dipantau_desktop_client/feature/presentation/widget/widget_custom_circular_progress_indicator.dart'; import 'package:dipantau_desktop_client/injection_container.dart' as di; @@ -164,14 +168,14 @@ class _MyAppState extends State { path: RegisterSuccessPage.routePath, name: RegisterSuccessPage.routeName, builder: (context, state) => RegisterSuccessPage( - email: state.queryParams['email'] as String, + email: state.uri.queryParameters['email'] as String, ), ), GoRoute( path: ForgotPasswordPage.routePath, name: ForgotPasswordPage.routeName, builder: (context, state) => ForgotPasswordPage( - email: state.queryParams['email'], + email: state.uri.queryParameters['email'], ), ), GoRoute( @@ -213,9 +217,16 @@ class _MyAppState extends State { builder: (context, state) { final arguments = state.extra as Map?; final listPhotos = arguments != null && arguments.containsKey(PhotoViewPage.parameterListPhotos) - ? arguments[PhotoViewPage.parameterListPhotos] as List? + ? arguments[PhotoViewPage.parameterListPhotos] as List? : null; - return PhotoViewPage(listPhotos: listPhotos); + final isShowIconDownload = + arguments != null && arguments.containsKey(PhotoViewPage.parameterIsShowIconDownload) + ? arguments[PhotoViewPage.parameterIsShowIconDownload] as bool? + : null; + return PhotoViewPage( + listPhotos: listPhotos, + isShowIconDownload: isShowIconDownload, + ); }, ), GoRoute( @@ -271,6 +282,27 @@ class _MyAppState extends State { return ResetPasswordPage(code: code); }, ), + GoRoute( + path: ManualTrackingPage.routePath, + name: ManualTrackingPage.routeName, + builder: (context, state) { + return const ManualTrackingPage(); + }, + ), + GoRoute( + path: SettingMemberBlurScreenshotPage.routePath, + name: SettingMemberBlurScreenshotPage.routeName, + builder: (context, state) { + return const SettingMemberBlurScreenshotPage(); + }, + ), + GoRoute( + path: UserRegistrationSettingPage.routePath, + name: UserRegistrationSettingPage.routeName, + builder: (context, state) { + return const UserRegistrationSettingPage(); + }, + ), ], errorBuilder: (context, state) => const ErrorPage(), ); @@ -379,8 +411,14 @@ class _MyAppState extends State { isDarkMode = state.isDarkMode; } - return WillPopScope( - onWillPop: () async => false, + return PopScope( + canPop: false, + onPopInvoked: (didPop) { + if (didPop) { + return; + } + return; + }, child: MaterialApp.router( title: 'Dipantau', themeMode: isDarkMode ? ThemeMode.dark : ThemeMode.light, diff --git a/macos/Flutter/GeneratedPluginRegistrant.swift b/macos/Flutter/GeneratedPluginRegistrant.swift index d0bc274..44a932a 100644 --- a/macos/Flutter/GeneratedPluginRegistrant.swift +++ b/macos/Flutter/GeneratedPluginRegistrant.swift @@ -5,7 +5,7 @@ import FlutterMacOS import Foundation -import auto_updater +import auto_updater_macos import connectivity_plus import flutter_local_notifications import package_info_plus @@ -17,10 +17,10 @@ import tray_manager import window_manager func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { - AutoUpdaterPlugin.register(with: registry.registrar(forPlugin: "AutoUpdaterPlugin")) - ConnectivityPlugin.register(with: registry.registrar(forPlugin: "ConnectivityPlugin")) + AutoUpdaterMacosPlugin.register(with: registry.registrar(forPlugin: "AutoUpdaterMacosPlugin")) + ConnectivityPlusPlugin.register(with: registry.registrar(forPlugin: "ConnectivityPlusPlugin")) FlutterLocalNotificationsPlugin.register(with: registry.registrar(forPlugin: "FlutterLocalNotificationsPlugin")) - FLTPackageInfoPlusPlugin.register(with: registry.registrar(forPlugin: "FLTPackageInfoPlusPlugin")) + FPPPackageInfoPlusPlugin.register(with: registry.registrar(forPlugin: "FPPPackageInfoPlusPlugin")) PathProviderPlugin.register(with: registry.registrar(forPlugin: "PathProviderPlugin")) ScreenRetrieverPlugin.register(with: registry.registrar(forPlugin: "ScreenRetrieverPlugin")) SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin")) diff --git a/macos/Podfile.lock b/macos/Podfile.lock index c53fcd4..2aca182 100644 --- a/macos/Podfile.lock +++ b/macos/Podfile.lock @@ -1,60 +1,54 @@ PODS: - - auto_updater (0.0.1): + - auto_updater_macos (0.0.1): - FlutterMacOS - Sparkle - connectivity_plus (0.0.1): + - Flutter - FlutterMacOS - - ReachabilitySwift - flutter_local_notifications (0.0.1): - FlutterMacOS - FlutterMacOS (1.0.0) - - FMDB (2.7.5): - - FMDB/standard (= 2.7.5) - - FMDB/standard (2.7.5) - package_info_plus (0.0.1): - FlutterMacOS - path_provider_foundation (0.0.1): - Flutter - FlutterMacOS - - ReachabilitySwift (5.0.0) - screen_retriever (0.0.1): - FlutterMacOS - shared_preferences_foundation (0.0.1): - Flutter - FlutterMacOS - - Sparkle (2.4.2) - - sqflite (0.0.2): + - Sparkle (2.6.2) + - sqflite (0.0.3): + - Flutter - FlutterMacOS - - FMDB (>= 2.7.5) - tray_manager (0.0.1): - FlutterMacOS - window_manager (0.2.0): - FlutterMacOS DEPENDENCIES: - - auto_updater (from `Flutter/ephemeral/.symlinks/plugins/auto_updater/macos`) - - connectivity_plus (from `Flutter/ephemeral/.symlinks/plugins/connectivity_plus/macos`) + - auto_updater_macos (from `Flutter/ephemeral/.symlinks/plugins/auto_updater_macos/macos`) + - connectivity_plus (from `Flutter/ephemeral/.symlinks/plugins/connectivity_plus/darwin`) - flutter_local_notifications (from `Flutter/ephemeral/.symlinks/plugins/flutter_local_notifications/macos`) - FlutterMacOS (from `Flutter/ephemeral`) - package_info_plus (from `Flutter/ephemeral/.symlinks/plugins/package_info_plus/macos`) - path_provider_foundation (from `Flutter/ephemeral/.symlinks/plugins/path_provider_foundation/darwin`) - screen_retriever (from `Flutter/ephemeral/.symlinks/plugins/screen_retriever/macos`) - shared_preferences_foundation (from `Flutter/ephemeral/.symlinks/plugins/shared_preferences_foundation/darwin`) - - sqflite (from `Flutter/ephemeral/.symlinks/plugins/sqflite/macos`) + - sqflite (from `Flutter/ephemeral/.symlinks/plugins/sqflite/darwin`) - tray_manager (from `Flutter/ephemeral/.symlinks/plugins/tray_manager/macos`) - window_manager (from `Flutter/ephemeral/.symlinks/plugins/window_manager/macos`) SPEC REPOS: trunk: - - FMDB - - ReachabilitySwift - Sparkle EXTERNAL SOURCES: - auto_updater: - :path: Flutter/ephemeral/.symlinks/plugins/auto_updater/macos + auto_updater_macos: + :path: Flutter/ephemeral/.symlinks/plugins/auto_updater_macos/macos connectivity_plus: - :path: Flutter/ephemeral/.symlinks/plugins/connectivity_plus/macos + :path: Flutter/ephemeral/.symlinks/plugins/connectivity_plus/darwin flutter_local_notifications: :path: Flutter/ephemeral/.symlinks/plugins/flutter_local_notifications/macos FlutterMacOS: @@ -68,28 +62,26 @@ EXTERNAL SOURCES: shared_preferences_foundation: :path: Flutter/ephemeral/.symlinks/plugins/shared_preferences_foundation/darwin sqflite: - :path: Flutter/ephemeral/.symlinks/plugins/sqflite/macos + :path: Flutter/ephemeral/.symlinks/plugins/sqflite/darwin tray_manager: :path: Flutter/ephemeral/.symlinks/plugins/tray_manager/macos window_manager: :path: Flutter/ephemeral/.symlinks/plugins/window_manager/macos SPEC CHECKSUMS: - auto_updater: d3c03e9e5f2a00ec78572d9f7473cb8c9a6c0273 - connectivity_plus: 18d3c32514c886e046de60e9c13895109866c747 + auto_updater_macos: 3e3462c418fe4e731917eacd8d28eef7af84086d + connectivity_plus: ddd7f30999e1faaef5967c23d5b6d503d10434db flutter_local_notifications: 3805ca215b2fb7f397d78b66db91f6a747af52e4 FlutterMacOS: 8f6f14fa908a6fb3fba0cd85dbd81ec4b251fb24 - FMDB: 2ce00b547f966261cd18927a3ddb07cb6f3db82a - package_info_plus: 02d7a575e80f194102bef286361c6c326e4c29ce - path_provider_foundation: eaf5b3e458fc0e5fbb9940fb09980e853fe058b8 - ReachabilitySwift: 985039c6f7b23a1da463388634119492ff86c825 + package_info_plus: fa739dd842b393193c5ca93c26798dff6e3d0e0c + path_provider_foundation: 2b6b4c569c0fb62ec74538f866245ac84301af46 screen_retriever: 59634572a57080243dd1bf715e55b6c54f241a38 - shared_preferences_foundation: e2dae3258e06f44cc55f49d42024fd8dd03c590c - Sparkle: 5ef7097e655c60f4aeb23fd1658fc3e8dd50f4ec - sqflite: a5789cceda41d54d23f31d6de539d65bb14100ea + shared_preferences_foundation: fcdcbc04712aee1108ac7fda236f363274528f78 + Sparkle: a62c7dc4f410ced73beb2169cf1d3cc3f028a295 + sqflite: 673a0e54cc04b7d6dba8d24fb8095b31c3a99eec tray_manager: 9064e219c56d75c476e46b9a21182087930baf90 window_manager: 3a1844359a6295ab1e47659b1a777e36773cd6e8 PODFILE CHECKSUM: 8d40c19d3cbdb380d870685c3a564c989f1efa52 -COCOAPODS: 1.12.1 +COCOAPODS: 1.13.0 diff --git a/macos/Runner.xcodeproj/project.pbxproj b/macos/Runner.xcodeproj/project.pbxproj index 364c46d..7d17195 100644 --- a/macos/Runner.xcodeproj/project.pbxproj +++ b/macos/Runner.xcodeproj/project.pbxproj @@ -208,7 +208,7 @@ isa = PBXProject; attributes = { LastSwiftUpdateCheck = 0920; - LastUpgradeCheck = 1300; + LastUpgradeCheck = 1510; ORGANIZATIONNAME = ""; TargetAttributes = { 33CC10EC2044A3C60003C045 = { diff --git a/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme index 49ffbfe..47348a2 100644 --- a/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme +++ b/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme @@ -1,6 +1,6 @@ =3.0.3 <4.0.0" - flutter: ">=3.10.0" diff --git a/pubspec.yaml b/pubspec.yaml index 26fa558..95e0e96 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -17,7 +17,7 @@ publish_to: 'none' # Remove this line if you wish to publish to pub.dev # https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html # In Windows, build-name is used as the major, minor, and patch parts # of the product and file versions while build-number is used as the build suffix. -version: 1.3.0+8 +version: 1.6.0+11 environment: sdk: '>=3.0.3 <4.0.0' @@ -37,7 +37,7 @@ dependencies: cupertino_icons: ^1.0.2 # This plugin allows Flutter desktop apps to resizing and repositioning the window. - window_manager: ^0.2.9 + window_manager: ^0.3.9 # A powerful HTTP client for Dart, which supports interceptors, form-data, request cancellation # file downloading, timeout, etc. @@ -75,7 +75,7 @@ dependencies: floor: ^1.4.2 # Flutter plugin for discovering the state of the network (WiFi & mobile/cellular) - connectivity_plus: ^3.0.2 + connectivity_plus: ^6.0.3 # A predictable state management library that helps implement the # BLoC design pattern. @@ -85,10 +85,10 @@ dependencies: json_annotation: ^4.8.1 # A declarative router for Flutter based on Navigation 2 supporting deep linking, data-driven routes and more. - go_router: ^6.0.1 + go_router: ^14.1.4 # A Flutter packages to use fonts from fonts.google.com - google_fonts: ^5.0.0 + google_fonts: ^6.2.1 # This plugin allows Flutter desktop apps to defines system tray. tray_manager: ^0.2.0 @@ -99,7 +99,7 @@ dependencies: # A cross platform plugin for displaying and scheduling local notifications for Flutter applications # with the ability to customize for each platform. - flutter_local_notifications: ^13.0.0 + flutter_local_notifications: 13.0.0 # The Font Awesome Icon pack available as Flutter Icons. Provides 1600 additional icons to use # in your apps. @@ -112,25 +112,28 @@ dependencies: sqflite: ^2.2.8+4 # Provides a collection of Flutter grids layouts (staggered, masonry, quilted, woven, etc.). - flutter_staggered_grid_view: ^0.6.2 + flutter_staggered_grid_view: ^0.7.0 # Photo View provides a gesture sensitive zoomable widget. Photo View is largely used to show # interactive images and other stuff such as SVG. - photo_view: ^0.14.0 + photo_view: ^0.15.0 # Render After Effects animations natively on Flutter. This package is a pure Dart implementation # of a Lottie player. - lottie: ^2.4.0 + lottie: ^3.1.2 # This plugin allows Flutter desktop apps to automatically update themselves (based on sparkle and winsparkle). - auto_updater: ^0.1.7 + auto_updater: ^0.2.1 # Flutter plugin for querying information about the application package, such as CFBundleVersion # on iOS or versionCode on Android. - package_info_plus: ^4.0.2 + package_info_plus: ^8.0.0 # This plugin allow Flutter desktop apps to Auto launch on startup / login. - launch_at_startup: ^0.2.2 + launch_at_startup: ^0.3.0 + + # A lightweight library for parsing, traversing, querying, transforming and building XML documents. + xml: ^6.3.0 dev_dependencies: flutter_test: @@ -141,7 +144,7 @@ dev_dependencies: # activated in the `analysis_options.yaml` file located at the root of your # package. See that file for information about deactivating specific lint # rules and activating additional ones. - flutter_lints: ^2.0.1 + flutter_lints: ^4.0.0 # Tools to write binaries that run builders. build_runner: ^2.4.5 @@ -163,7 +166,10 @@ dev_dependencies: bloc_test: ^9.1.0 # This package provides a library that performs static analysis of Dart code. - analyzer: ^5.13.0 + analyzer: ^6.4.1 + + # DevTools extension for Flutter: Manage SharedPreferences efficiently. Edit, search, and view keys. + shared_preferences_tools: 1.0.3 # For information on the generic Dart part of this file, see the # following page: https://dart.dev/tools/pub/pubspec diff --git a/test/core/network/network_info_test.dart b/test/core/network/network_info_test.dart index e73d52c..7919e61 100644 --- a/test/core/network/network_info_test.dart +++ b/test/core/network/network_info_test.dart @@ -19,8 +19,8 @@ void main() { 'menggunakan mobile data', () async { // arrange - final tHasConnection = Future.value(ConnectivityResult.mobile); - when(mockConnectivity.checkConnectivity()).thenAnswer((_) => tHasConnection); + final tHasConnection = [ConnectivityResult.mobile]; + when(mockConnectivity.checkConnectivity()).thenAnswer((_) async => tHasConnection); // act final result = await networkInfoImpl.isConnected; @@ -36,8 +36,8 @@ void main() { 'menggunakan wifi', () async { // arrange - final tHasConnection = Future.value(ConnectivityResult.wifi); - when(mockConnectivity.checkConnectivity()).thenAnswer((_) => tHasConnection); + final tHasConnection = [ConnectivityResult.wifi]; + when(mockConnectivity.checkConnectivity()).thenAnswer((_) async => tHasConnection); // act final result = await networkInfoImpl.isConnected; @@ -53,8 +53,8 @@ void main() { 'tidak terhubung sama sekali', () async { // arrange - final tHasConnection = Future.value(ConnectivityResult.none); - when(mockConnectivity.checkConnectivity()).thenAnswer((_) => tHasConnection); + final tHasConnection = [ConnectivityResult.none]; + when(mockConnectivity.checkConnectivity()).thenAnswer((_) async => tHasConnection); // act final result = await networkInfoImpl.isConnected; diff --git a/test/feature/data/datasource/general/general_remote_data_source_test.dart b/test/feature/data/datasource/general/general_remote_data_source_test.dart new file mode 100644 index 0000000..ad0373b --- /dev/null +++ b/test/feature/data/datasource/general/general_remote_data_source_test.dart @@ -0,0 +1,109 @@ +import 'dart:convert'; + +import 'package:dio/dio.dart'; +import 'package:dipantau_desktop_client/config/flavor_config.dart'; +import 'package:dipantau_desktop_client/feature/data/datasource/general/general_remote_data_source.dart'; +import 'package:dipantau_desktop_client/feature/data/model/general/general_response.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:mockito/mockito.dart'; + +import '../../../../fixture/fixture_reader.dart'; +import '../../../../helper/mock_helper.mocks.dart'; + +void main() { + late GeneralRemoteDataSource remoteDataSource; + late MockDio mockDio; + late MockHttpClientAdapter mockDioAdapter; + + const baseUrl = 'https://example.com'; + + setUp(() { + FlavorConfig( + values: FlavorValues( + baseUrl: baseUrl, + baseUrlAuth: '', + baseUrlUser: '', + baseUrlTrack: '', + baseUrlProject: '', + baseUrlSetting: '', + ), + ); + mockDio = MockDio(); + mockDioAdapter = MockHttpClientAdapter(); + mockDio.httpClientAdapter = mockDioAdapter; + remoteDataSource = GeneralRemoteDataSourceImpl(dio: mockDio); + }); + + final tRequestOptions = RequestOptions(path: ''); + + group('ping', () { + const tPathResponse = 'general_response.json'; + const hostname = baseUrl; + final tResponse = GeneralResponse.fromJson( + json.decode( + fixture(tPathResponse), + ), + ); + + void setUpMockDioSuccess() { + final responsePayload = json.decode(fixture(tPathResponse)); + final response = Response( + requestOptions: tRequestOptions, + data: responsePayload, + statusCode: 200, + headers: Headers.fromMap({ + Headers.contentTypeHeader: [Headers.jsonContentType], + }), + ); + when(mockDio.get(any)).thenAnswer((_) async => response); + } + + test( + 'pastikan endpoint ping benar-benar terpanggil dengan method GET', + () async { + // arrange + setUpMockDioSuccess(); + + // act + await remoteDataSource.ping(hostname); + + // assert + verify(mockDio.get('$hostname/api/ping')); + }, + ); + + test( + 'pastikan mengembalikan objek class model GeneralResponse ketika menerima respon sukses ' + 'dari endpoint', + () async { + // arrange + setUpMockDioSuccess(); + + // act + final result = await remoteDataSource.ping(hostname); + + // assert + expect(result, tResponse); + }, + ); + + test( + 'pastikan akan menerima exception DioError ketika menerima respon kegagalan dari endpoint', + () async { + // arrange + final response = Response( + requestOptions: tRequestOptions, + data: 'Bad Request', + statusCode: 400, + ); + when(mockDio.get(any)).thenAnswer((_) async => response); + + // act + final call = remoteDataSource.ping(hostname); + + // assert + expect(() => call, throwsA(const TypeMatcher())); + }, + ); + }); +} diff --git a/test/feature/data/datasource/project/project_remote_data_source_test.dart b/test/feature/data/datasource/project/project_remote_data_source_test.dart index dc55eec..f36d81a 100644 --- a/test/feature/data/datasource/project/project_remote_data_source_test.dart +++ b/test/feature/data/datasource/project/project_remote_data_source_test.dart @@ -4,6 +4,7 @@ import 'package:dio/dio.dart'; import 'package:dipantau_desktop_client/config/flavor_config.dart'; import 'package:dipantau_desktop_client/feature/data/datasource/project/project_remote_data_source.dart'; import 'package:dipantau_desktop_client/feature/data/model/project/project_response.dart'; +import 'package:dipantau_desktop_client/feature/data/model/project_task/project_task_response.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:mockito/mockito.dart'; @@ -106,4 +107,75 @@ void main() { }, ); }); + + group('getProjectTaskByUserId', () { + const tUserId = 'testUserId'; + const tPathResponse = 'project_task_response.json'; + final tResponse = ProjectTaskResponse.fromJson( + json.decode( + fixture(tPathResponse), + ), + ); + + void setUpMockDioSuccess() { + final responsePayload = json.decode(fixture(tPathResponse)); + final response = Response( + requestOptions: tRequestOptions, + data: responsePayload, + statusCode: 200, + headers: Headers.fromMap({ + Headers.contentTypeHeader: [Headers.jsonContentType], + }), + ); + when(mockDio.get(any, options: anyNamed('options'))).thenAnswer((_) async => response); + } + + test( + 'pastikan endpoint getProjectTaskByUserId benar-benar terpanggil dengan method GET', + () async { + // arrange + setUpMockDioSuccess(); + + // act + await remoteDataSource.getProjectTaskByUserId(tUserId); + + // assert + verify(mockDio.get('$baseUrl/user/$tUserId/detail', options: anyNamed('options'))); + }, + ); + + test( + 'pastikan mengembalikan objek class model ProjectTaskResponse ketika menerima respon sukses ' + 'dari endpoint', + () async { + // arrange + setUpMockDioSuccess(); + + // act + final result = await remoteDataSource.getProjectTaskByUserId(tUserId); + + // assert + expect(result, tResponse); + }, + ); + + test( + 'pastikan akan menerima exception DioException ketika menerima respon kegagalan dari endpoint', + () async { + // arrange + final response = Response( + requestOptions: tRequestOptions, + data: 'Bad Request', + statusCode: 400, + ); + when(mockDio.get(any, options: anyNamed('options'))).thenAnswer((_) async => response); + + // act + final call = remoteDataSource.getProjectTaskByUserId(tUserId); + + // assert + expect(() => call, throwsA(const TypeMatcher())); + }, + ); + }); } diff --git a/test/feature/data/datasource/setting/setting_remote_data_source_test.dart b/test/feature/data/datasource/setting/setting_remote_data_source_test.dart index cc78920..b267d34 100644 --- a/test/feature/data/datasource/setting/setting_remote_data_source_test.dart +++ b/test/feature/data/datasource/setting/setting_remote_data_source_test.dart @@ -3,8 +3,11 @@ import 'dart:convert'; import 'package:dio/dio.dart'; import 'package:dipantau_desktop_client/config/flavor_config.dart'; import 'package:dipantau_desktop_client/feature/data/datasource/setting/setting_remote_data_source.dart'; +import 'package:dipantau_desktop_client/feature/data/model/all_user_setting/all_user_setting_response.dart'; import 'package:dipantau_desktop_client/feature/data/model/kv_setting/kv_setting_body.dart'; import 'package:dipantau_desktop_client/feature/data/model/kv_setting/kv_setting_response.dart'; +import 'package:dipantau_desktop_client/feature/data/model/user_setting/user_setting_body.dart'; +import 'package:dipantau_desktop_client/feature/data/model/user_setting/user_setting_response.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:mockito/mockito.dart'; @@ -177,4 +180,220 @@ void main() { }, ); }); + + group('getAllUserSetting', () { + const tPathResponse = 'all_user_setting_response.json'; + final tResponse = AllUserSettingResponse.fromJson( + json.decode( + fixture(tPathResponse), + ), + ); + + void setUpMockDioSuccess() { + final responsePayload = json.decode(fixture(tPathResponse)); + final response = Response( + requestOptions: tRequestOptions, + data: responsePayload, + statusCode: 200, + headers: Headers.fromMap({ + Headers.contentTypeHeader: [Headers.jsonContentType], + }), + ); + when(mockDio.get(any, options: anyNamed('options'))).thenAnswer((_) async => response); + } + + test( + 'pastikan endpoint getAllUserSetting benar-benar terpanggil dengan method GET', + () async { + // arrange + setUpMockDioSuccess(); + + // act + await remoteDataSource.getAllUserSetting(); + + // assert + verify(mockDio.get('$baseUrl/user/all', options: anyNamed('options'))); + }, + ); + + test( + 'pastikan mengembalikan objek class model AllUserSettingResponse ketika menerima respon sukses ' + 'dari endpoint', + () async { + // arrange + setUpMockDioSuccess(); + + // act + final result = await remoteDataSource.getAllUserSetting(); + + // assert + expect(result, tResponse); + }, + ); + + test( + 'pastikan akan menerima exception DioException ketika menerima respon kegagalan dari endpoint', + () async { + // arrange + final response = Response( + requestOptions: tRequestOptions, + data: 'Bad Request', + statusCode: 400, + ); + when(mockDio.get(any, options: anyNamed('options'))).thenAnswer((_) async => response); + + // act + final call = remoteDataSource.getAllUserSetting(); + + // assert + expect(() => call, throwsA(const TypeMatcher())); + }, + ); + }); + + group('getUserSetting', () { + const tPathResponse = 'user_setting_response.json'; + final tResponse = UserSettingResponse.fromJson( + json.decode( + fixture(tPathResponse), + ), + ); + + void setUpMockDioSuccess() { + final responsePayload = json.decode(fixture(tPathResponse)); + final response = Response( + requestOptions: tRequestOptions, + data: responsePayload, + statusCode: 200, + headers: Headers.fromMap({ + Headers.contentTypeHeader: [Headers.jsonContentType], + }), + ); + when(mockDio.get(any, options: anyNamed('options'))).thenAnswer((_) async => response); + } + + test( + 'pastikan endpoint getUserSetting benar-benar terpanggil dengan method GET', + () async { + // arrange + setUpMockDioSuccess(); + + // act + await remoteDataSource.getUserSetting(); + + // assert + verify(mockDio.get('$baseUrl/user', options: anyNamed('options'))); + }, + ); + + test( + 'pastikan mengembalikan objek class model UserSettingResponse ketika menerima respon sukses ' + 'dari endpoint', + () async { + // arrange + setUpMockDioSuccess(); + + // act + final result = await remoteDataSource.getUserSetting(); + + // assert + expect(result, tResponse); + }, + ); + + test( + 'pastikan akan menerima exception DioException ketika menerima respon kegagalan dari endpoint', + () async { + // arrange + final response = Response( + requestOptions: tRequestOptions, + data: 'Bad Request', + statusCode: 400, + ); + when(mockDio.get(any, options: anyNamed('options'))).thenAnswer((_) async => response); + + // act + final call = remoteDataSource.getUserSetting(); + + // assert + expect(() => call, throwsA(const TypeMatcher())); + }, + ); + }); + + group('updateUserSetting', () { + final bodyOverride = UserSettingBody.fromJson( + json.decode( + fixture('user_setting_body.json'), + ), + ); + final bodyOverrideNull = UserSettingBody( + data: [], + isOverrideBlurScreenshot: null, + ); + const tPathResponse = 'general_response.json'; + const tResponse = true; + + void setUpMockDioSuccess() { + final responsePayload = json.decode(fixture(tPathResponse)); + final response = Response( + requestOptions: tRequestOptions, + data: responsePayload, + statusCode: 200, + headers: Headers.fromMap({ + Headers.contentTypeHeader: [Headers.jsonContentType], + }), + ); + when(mockDio.post(any, data: anyNamed('data'), options: anyNamed('options'))).thenAnswer((_) async => response); + } + + test( + 'pastikan endpoint updateUserSetting benar-benar terpanggil dengan method POST', + () async { + // arrange + setUpMockDioSuccess(); + + // act + await remoteDataSource.updateUserSetting(bodyOverride); + await remoteDataSource.updateUserSetting(bodyOverrideNull); + + // assert + verify(mockDio.post('$baseUrl/user', data: anyNamed('data'), options: anyNamed('options'))); + }, + ); + + test( + 'pastikan mengembalikan objek class model GeneralResponse ketika menerima respon sukses ' + 'dari endpoint', + () async { + // arrange + setUpMockDioSuccess(); + + // act + final result = await remoteDataSource.updateUserSetting(bodyOverride); + + // assert + expect(result, tResponse); + }, + ); + + test( + 'pastikan akan menerima exception DioException ketika menerima respon kegagalan dari endpoint', + () async { + // arrange + final response = Response( + requestOptions: tRequestOptions, + data: 'Bad Request', + statusCode: 400, + ); + when(mockDio.post(any, data: anyNamed('data'), options: anyNamed('options'))).thenAnswer((_) async => response); + + // act + final call = remoteDataSource.updateUserSetting(bodyOverride); + + // assert + expect(() => call, throwsA(const TypeMatcher())); + }, + ); + }); } diff --git a/test/feature/data/datasource/track/track_remote_data_source_test.dart b/test/feature/data/datasource/track/track_remote_data_source_test.dart index fcfc7d4..fee95b8 100644 --- a/test/feature/data/datasource/track/track_remote_data_source_test.dart +++ b/test/feature/data/datasource/track/track_remote_data_source_test.dart @@ -7,6 +7,7 @@ import 'package:dipantau_desktop_client/feature/data/model/create_track/bulk_cre import 'package:dipantau_desktop_client/feature/data/model/create_track/bulk_create_track_image_body.dart'; import 'package:dipantau_desktop_client/feature/data/model/create_track/create_track_body.dart'; import 'package:dipantau_desktop_client/feature/data/model/general/general_response.dart'; +import 'package:dipantau_desktop_client/feature/data/model/manual_create_track/manual_create_track_body.dart'; import 'package:dipantau_desktop_client/feature/data/model/track_user/track_user_response.dart'; import 'package:dipantau_desktop_client/feature/data/model/track_user_lite/track_user_lite_response.dart'; import 'package:flutter_test/flutter_test.dart'; @@ -499,4 +500,79 @@ void main() { }, ); }); + + group('createManualTrack', () { + final tBody = ManualCreateTrackBody.fromJson( + json.decode( + fixture('manual_create_track_body.json'), + ), + ); + const tPathResponse = 'general_response.json'; + final tResponse = GeneralResponse.fromJson( + json.decode( + fixture(tPathResponse), + ), + ); + + void setUpMockDioSuccess() { + final responsePayload = json.decode(fixture(tPathResponse)); + final response = Response( + requestOptions: tRequestOptions, + data: responsePayload, + statusCode: 200, + headers: Headers.fromMap({ + Headers.contentTypeHeader: [Headers.jsonContentType], + }), + ); + when(mockDio.post(any, data: anyNamed('data'), options: anyNamed('options'))).thenAnswer((_) async => response); + } + + test( + 'pastikan endpoint createManualTrack benar-benar terpanggil dengan method POST', + () async { + // arrange + setUpMockDioSuccess(); + + // act + await remoteDataSource.createManualTrack(tBody); + + // assert + verify(mockDio.post('$baseUrl/manual', data: anyNamed('data'), options: anyNamed('options'))); + }, + ); + + test( + 'pastikan mengembalikan objek class model GeneralResponse ketika menerima respon sukses ' + 'dari endpoint', + () async { + // arrange + setUpMockDioSuccess(); + + // act + final result = await remoteDataSource.createManualTrack(tBody); + + // assert + expect(result, tResponse); + }, + ); + + test( + 'pastikan akan menerima exception DioException ketika menerima respon kegagalan dari endpoint', + () async { + // arrange + final response = Response( + requestOptions: tRequestOptions, + data: 'Bad Request', + statusCode: 400, + ); + when(mockDio.post(any, data: anyNamed('data'), options: anyNamed('options'))).thenAnswer((_) async => response); + + // act + final call = remoteDataSource.createManualTrack(tBody); + + // assert + expect(() => call, throwsA(const TypeMatcher())); + }, + ); + }); } diff --git a/test/feature/data/model/all_user_setting/all_user_setting_response_test.dart b/test/feature/data/model/all_user_setting/all_user_setting_response_test.dart new file mode 100644 index 0000000..6b8f52b --- /dev/null +++ b/test/feature/data/model/all_user_setting/all_user_setting_response_test.dart @@ -0,0 +1,72 @@ +import 'dart:convert'; + +import 'package:dipantau_desktop_client/feature/data/model/all_user_setting/all_user_setting_response.dart'; +import 'package:flutter_test/flutter_test.dart'; + +import '../../../../fixture/fixture_reader.dart'; + +void main() { + const tPathJson = 'all_user_setting_response.json'; + final tModel = AllUserSettingResponse.fromJson( + json.decode( + fixture(tPathJson), + ), + ); + + test( + 'pastikan output dari nilai props', + () async { + // assert + expect( + tModel.props, + [ + tModel.data, + tModel.isOverrideBlurScreenshot, + ], + ); + }, + ); + + test( + 'pastikan output dari fungsi toString', + () async { + // assert + expect( + tModel.toString(), + 'AllUserSettingResponse{data: ${tModel.data}, isOverrideBlurScreenshot: ${tModel.isOverrideBlurScreenshot}}', + ); + }, + ); + + test( + 'pastikan fungsi fromJson bisa mengembalikan objek class model', + () async { + // arrange + final jsonData = json.decode(fixture(tPathJson)); + + // act + final actualModel = AllUserSettingResponse.fromJson(jsonData); + + // assert + expect(actualModel, tModel); + }, + ); + + test( + 'pastikan fungsi toJson bisa mengembalikan objek map', + () async { + // arrange + final model = AllUserSettingResponse.fromJson( + json.decode( + fixture(tPathJson), + ), + ); + + // act + final actualMap = json.encode(model.toJson()); + + // assert + expect(actualMap, json.encode(tModel.toJson())); + }, + ); +} diff --git a/test/feature/data/model/kv_setting/kv_setting_body_test.dart b/test/feature/data/model/kv_setting/kv_setting_body_test.dart index c363abd..ec8e21f 100644 --- a/test/feature/data/model/kv_setting/kv_setting_body_test.dart +++ b/test/feature/data/model/kv_setting/kv_setting_body_test.dart @@ -21,6 +21,7 @@ void main() { tModel.props, [ tModel.discordChannelId, + tModel.signUpMethod, ], ); }, @@ -32,7 +33,7 @@ void main() { // assert expect( tModel.toString(), - 'KvSettingBody{discordChannelId: ${tModel.discordChannelId}}', + 'KvSettingBody{discordChannelId: ${tModel.discordChannelId}, signUpMethod: ${tModel.signUpMethod}}', ); }, ); diff --git a/test/feature/data/model/kv_setting/kv_setting_response_test.dart b/test/feature/data/model/kv_setting/kv_setting_response_test.dart index cba0f79..a8a97bd 100644 --- a/test/feature/data/model/kv_setting/kv_setting_response_test.dart +++ b/test/feature/data/model/kv_setting/kv_setting_response_test.dart @@ -21,6 +21,7 @@ void main() { tModel.props, [ tModel.discordChannelId, + tModel.signUpMethod, ], ); }, @@ -32,7 +33,7 @@ void main() { // assert expect( tModel.toString(), - 'KvSettingResponse{discordChannelId: ${tModel.discordChannelId}}', + 'KvSettingResponse{discordChannelId: ${tModel.discordChannelId}, signUpMethod: ${tModel.signUpMethod}}', ); }, ); diff --git a/test/feature/data/model/manual_create_track/manual_create_track_body_test.dart b/test/feature/data/model/manual_create_track/manual_create_track_body_test.dart new file mode 100644 index 0000000..45f6313 --- /dev/null +++ b/test/feature/data/model/manual_create_track/manual_create_track_body_test.dart @@ -0,0 +1,76 @@ +import 'dart:convert'; + +import 'package:dipantau_desktop_client/feature/data/model/manual_create_track/manual_create_track_body.dart'; +import 'package:flutter_test/flutter_test.dart'; + +import '../../../../fixture/fixture_reader.dart'; + +void main() { + const tPathJson = 'manual_create_track_body.json'; + final tModel = ManualCreateTrackBody.fromJson( + json.decode( + fixture(tPathJson), + ), + ); + + test( + 'pastikan output dari nilai props', + () async { + // assert + expect( + tModel.props, + [ + tModel.taskId, + tModel.startDate, + tModel.finishDate, + tModel.duration, + tModel.note, + ], + ); + }, + ); + + test( + 'pastikan output dari fungsi toString', + () async { + // assert + expect( + tModel.toString(), + 'ManualCreateTrackBody{taskId: ${tModel.taskId}, startDate: ${tModel.startDate}, ' + 'finishDate: ${tModel.finishDate}, duration: ${tModel.duration}, note: ${tModel.note}}', + ); + }, + ); + + test( + 'pastikan fungsi fromJson bisa mengembalikan objek class model', + () async { + // arrange + final jsonData = json.decode(fixture(tPathJson)); + + // act + final actualModel = ManualCreateTrackBody.fromJson(jsonData); + + // assert + expect(actualModel, tModel); + }, + ); + + test( + 'pastikan fungsi toJson bisa mengembalikan objek map', + () async { + // arrange + final model = ManualCreateTrackBody.fromJson( + json.decode( + fixture(tPathJson), + ), + ); + + // act + final actualMap = json.encode(model.toJson()); + + // assert + expect(actualMap, json.encode(tModel.toJson())); + }, + ); +} diff --git a/test/feature/data/model/project_task/project_task_response_test.dart b/test/feature/data/model/project_task/project_task_response_test.dart new file mode 100644 index 0000000..c79f7fb --- /dev/null +++ b/test/feature/data/model/project_task/project_task_response_test.dart @@ -0,0 +1,71 @@ +import 'dart:convert'; + +import 'package:dipantau_desktop_client/feature/data/model/project_task/project_task_response.dart'; +import 'package:flutter_test/flutter_test.dart'; + +import '../../../../fixture/fixture_reader.dart'; + +void main() { + const tPathJson = 'project_task_response.json'; + final tModel = ProjectTaskResponse.fromJson( + json.decode( + fixture(tPathJson), + ), + ); + + test( + 'pastikan output dari nilai props', + () async { + // assert + expect( + tModel.props, + [ + tModel.data, + ], + ); + }, + ); + + test( + 'pastikan output dari fungsi toString', + () async { + // assert + expect( + tModel.toString(), + 'ProjectTaskResponse{data: ${tModel.data}}', + ); + }, + ); + + test( + 'pastikan fungsi fromJson bisa mengembalikan objek class model', + () async { + // arrange + final jsonData = json.decode(fixture(tPathJson)); + + // act + final actualModel = ProjectTaskResponse.fromJson(jsonData); + + // assert + expect(actualModel, tModel); + }, + ); + + test( + 'pastikan fungsi toJson bisa mengembalikan objek map', + () async { + // arrange + final model = ProjectTaskResponse.fromJson( + json.decode( + fixture(tPathJson), + ), + ); + + // act + final actualMap = json.encode(model.toJson()); + + // assert + expect(actualMap, json.encode(tModel.toJson())); + }, + ); +} diff --git a/test/feature/data/model/user_setting/user_setting_body_test.dart b/test/feature/data/model/user_setting/user_setting_body_test.dart new file mode 100644 index 0000000..e0f1c66 --- /dev/null +++ b/test/feature/data/model/user_setting/user_setting_body_test.dart @@ -0,0 +1,72 @@ +import 'dart:convert'; + +import 'package:dipantau_desktop_client/feature/data/model/user_setting/user_setting_body.dart'; +import 'package:flutter_test/flutter_test.dart'; + +import '../../../../fixture/fixture_reader.dart'; + +void main() { + const tPathJson = 'user_setting_body.json'; + final tModel = UserSettingBody.fromJson( + json.decode( + fixture(tPathJson), + ), + ); + + test( + 'pastikan output dari nilai props', + () async { + // assert + expect( + tModel.props, + [ + tModel.data, + tModel.isOverrideBlurScreenshot, + ], + ); + }, + ); + + test( + 'pastikan output dari fungsi toString', + () async { + // assert + expect( + tModel.toString(), + 'UserSettingBody{data: ${tModel.data}, isOverrideBlurScreenshot: ${tModel.isOverrideBlurScreenshot}}', + ); + }, + ); + + test( + 'pastikan fungsi fromJson bisa mengembalikan objek class model', + () async { + // arrange + final jsonData = json.decode(fixture(tPathJson)); + + // act + final actualModel = UserSettingBody.fromJson(jsonData); + + // assert + expect(actualModel, tModel); + }, + ); + + test( + 'pastikan fungsi toJson bisa mengembalikan objek map', + () async { + // arrange + final model = UserSettingBody.fromJson( + json.decode( + fixture(tPathJson), + ), + ); + + // act + final actualMap = json.encode(model.toJson()); + + // assert + expect(actualMap, json.encode(tModel.toJson())); + }, + ); +} diff --git a/test/feature/data/model/user_setting/user_setting_response_test.dart b/test/feature/data/model/user_setting/user_setting_response_test.dart new file mode 100644 index 0000000..a21f3cb --- /dev/null +++ b/test/feature/data/model/user_setting/user_setting_response_test.dart @@ -0,0 +1,76 @@ +import 'dart:convert'; + +import 'package:dipantau_desktop_client/feature/data/model/user_setting/user_setting_response.dart'; +import 'package:flutter_test/flutter_test.dart'; + +import '../../../../fixture/fixture_reader.dart'; + +void main() { + const tPathJson = 'user_setting_response.json'; + final tModel = UserSettingResponse.fromJson( + json.decode( + fixture(tPathJson), + ), + ); + + test( + 'pastikan output dari nilai props', + () async { + // assert + expect( + tModel.props, + [ + tModel.id, + tModel.isEnableBlurScreenshot, + tModel.userId, + tModel.name, + tModel.isOverrideBlurScreenshot, + ], + ); + }, + ); + + test( + 'pastikan output dari fungsi toString', + () async { + // assert + expect( + tModel.toString(), + 'UserSettingResponse{id: ${tModel.id}, isEnableBlurScreenshot: ${tModel.isEnableBlurScreenshot}, ' + 'userId: ${tModel.userId}, name: ${tModel.name}, isOverrideBlurScreenshot: ${tModel.isOverrideBlurScreenshot}}', + ); + }, + ); + + test( + 'pastikan fungsi fromJson bisa mengembalikan objek class model', + () async { + // arrange + final jsonData = json.decode(fixture(tPathJson)); + + // act + final actualModel = UserSettingResponse.fromJson(jsonData); + + // assert + expect(actualModel, tModel); + }, + ); + + test( + 'pastikan fungsi toJson bisa mengembalikan objek map', + () async { + // arrange + final model = UserSettingResponse.fromJson( + json.decode( + fixture(tPathJson), + ), + ); + + // act + final actualMap = json.encode(model.toJson()); + + // assert + expect(actualMap, json.encode(tModel.toJson())); + }, + ); +} diff --git a/test/feature/data/repository/general/general_repository_impl_test.dart b/test/feature/data/repository/general/general_repository_impl_test.dart new file mode 100644 index 0000000..82c1d33 --- /dev/null +++ b/test/feature/data/repository/general/general_repository_impl_test.dart @@ -0,0 +1,189 @@ +import 'dart:convert'; + +import 'package:dio/dio.dart'; +import 'package:dipantau_desktop_client/core/error/failure.dart'; +import 'package:dipantau_desktop_client/feature/data/model/general/general_response.dart'; +import 'package:dipantau_desktop_client/feature/data/repository/general/general_repository_impl.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:mockito/mockito.dart'; + +import '../../../../fixture/fixture_reader.dart'; +import '../../../../helper/mock_helper.mocks.dart'; + +void main() { + late GeneralRepositoryImpl repository; + late MockGeneralRemoteDataSource mockRemoteDataSource; + late MockNetworkInfo mockNetworkInfo; + + setUp(() { + mockRemoteDataSource = MockGeneralRemoteDataSource(); + mockNetworkInfo = MockNetworkInfo(); + repository = GeneralRepositoryImpl( + remoteDataSource: mockRemoteDataSource, + networkInfo: mockNetworkInfo, + ); + }); + + final tRequestOptions = RequestOptions(path: ''); + + void setUpMockNetworkConnected() { + when(mockNetworkInfo.isConnected).thenAnswer((_) async => true); + } + + void setUpMockNetworkDisconnected() { + when(mockNetworkInfo.isConnected).thenAnswer((_) async => false); + } + + void testDisconnected(Function endpointInvoke) { + test( + 'pastikan mengembalikan objek ConnectionFailure ketika device tidak terhubung ke internet', + () async { + // arrange + setUpMockNetworkDisconnected(); + + // act + final result = await endpointInvoke.call(); + + // assert + verify(mockNetworkInfo.isConnected); + expect(result.failure, ConnectionFailure()); + }, + ); + } + + void testServerFailureString(Function whenInvoke, Function actInvoke, Function verifyInvoke) { + test( + 'pastikan mengembalikan objek ServerFailure ketika repository menerima respon kegagalan ' + 'dari endpoint dengan respon data html atau string', + () async { + // arrange + setUpMockNetworkConnected(); + when(whenInvoke.call()).thenThrow( + DioException( + requestOptions: tRequestOptions, + message: 'testError', + response: Response( + requestOptions: tRequestOptions, + data: 'testDataError', + statusCode: 400, + ), + ), + ); + + // act + final result = await actInvoke.call(); + + // assert + verify(verifyInvoke.call()); + expect(result.failure, ServerFailure('testError')); + }, + ); + } + + void testParsingFailure(Function whenInvoke, Function actInvoke, Function verifyInvoke) { + test( + 'pastikan mengembalikan objek ParsingFailure ketika RemoteDataSource menerima respon kegagalan ' + 'dari endpoint', + () async { + // arrange + setUpMockNetworkConnected(); + when(whenInvoke.call()).thenThrow(TypeError()); + + // act + final result = await actInvoke.call(); + + // assert + verify(verifyInvoke.call()); + expect(result.failure, ParsingFailure(TypeError().toString())); + }, + ); + } + + group('ping', () { + final tResponse = GeneralResponse.fromJson( + json.decode( + fixture('general_response.json'), + ), + ); + const hostname = 'https://example.com'; + + test( + 'pastikan mengembalikan objek model GeneralResponse ketika RemoteDataSource berhasil menerima ' + 'respon sukses dari endpoint', + () async { + // arrange + setUpMockNetworkConnected(); + when(mockRemoteDataSource.ping(any)).thenAnswer((_) async => tResponse); + + // act + final result = await repository.ping(hostname); + + // assert + verify(mockRemoteDataSource.ping(hostname)); + expect(result.response, tResponse); + }, + ); + + test( + 'pastikan mengembalikan objek ServerFailure ketika RemoteDataSource berhasil menerima ' + 'respon timeout dari endpoint', + () async { + // arrange + setUpMockNetworkConnected(); + when(mockRemoteDataSource.ping(any)) + .thenThrow(DioException(requestOptions: tRequestOptions, message: 'testError')); + + // act + final result = await repository.ping(hostname); + + // assert + verify(mockRemoteDataSource.ping(hostname)); + expect(result.failure, ServerFailure('testError')); + }, + ); + + test( + 'pastikan mengembalikan objek ServerFailure ketika RemoteDataSource menerima respon kegagaln ' + 'dari endpoint', + () async { + // arrange + setUpMockNetworkConnected(); + when(mockRemoteDataSource.ping(any)).thenThrow( + DioException( + requestOptions: tRequestOptions, + message: 'testError', + response: Response( + requestOptions: tRequestOptions, + data: { + 'title': 'testTitleError', + 'message': 'testMessageError', + }, + statusCode: 400, + ), + ), + ); + + // act + final result = await repository.ping(hostname); + + // assert + verify(mockRemoteDataSource.ping(hostname)); + expect(result.failure, ServerFailure('400 testMessageError')); + }, + ); + + testServerFailureString( + () => mockRemoteDataSource.ping(any), + () => repository.ping(hostname), + () => mockRemoteDataSource.ping(hostname), + ); + + testParsingFailure( + () => mockRemoteDataSource.ping(any), + () => repository.ping(hostname), + () => mockRemoteDataSource.ping(hostname), + ); + + testDisconnected(() => repository.ping(hostname)); + }); +} diff --git a/test/feature/data/repository/project/project_repository_impl_test.dart b/test/feature/data/repository/project/project_repository_impl_test.dart index f5e9702..940dde2 100644 --- a/test/feature/data/repository/project/project_repository_impl_test.dart +++ b/test/feature/data/repository/project/project_repository_impl_test.dart @@ -4,6 +4,7 @@ import 'package:dartz/dartz.dart'; import 'package:dio/dio.dart'; import 'package:dipantau_desktop_client/core/error/failure.dart'; import 'package:dipantau_desktop_client/feature/data/model/project/project_response.dart'; +import 'package:dipantau_desktop_client/feature/data/model/project_task/project_task_response.dart'; import 'package:dipantau_desktop_client/feature/data/repository/project/project_repository_impl.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:mockito/mockito.dart'; @@ -52,6 +53,23 @@ void main() { ); } + void testDisconnected2(Function endpointInvoke) { + test( + 'pastikan mengembalikan objek ConnectionFailure ketika device tidak terhubung ke internet', + () async { + // arrange + setUpMockNetworkDisconnected(); + + // act + final result = await endpointInvoke.call(); + + // assert + verify(mockNetworkInfo.isConnected); + expect(result.failure, ConnectionFailure()); + }, + ); + } + void testServerFailureString(Function whenInvoke, Function actInvoke, Function verifyInvoke) { test( 'pastikan mengembalikan objek ServerFailure ketika EmployeeRepository menerima respon kegagalan ' @@ -81,6 +99,35 @@ void main() { ); } + void testServerFailureString2(Function whenInvoke, Function actInvoke, Function verifyInvoke) { + test( + 'pastikan mengembalikan objek ServerFailure ketika repository menerima respon kegagalan ' + 'dari endpoint dengan respon data html atau string', + () async { + // arrange + setUpMockNetworkConnected(); + when(whenInvoke.call()).thenThrow( + DioException( + requestOptions: tRequestOptions, + message: 'testError', + response: Response( + requestOptions: tRequestOptions, + data: 'testDataError', + statusCode: 400, + ), + ), + ); + + // act + final result = await actInvoke.call(); + + // assert + verify(verifyInvoke.call()); + expect(result.failure, ServerFailure('testError')); + }, + ); + } + void testParsingFailure(Function whenInvoke, Function actInvoke, Function verifyInvoke) { test( 'pastikan mengembalikan objek ParsingFailure ketika RemoteDataSource menerima respon kegagalan ' @@ -100,6 +147,25 @@ void main() { ); } + void testParsingFailure2(Function whenInvoke, Function actInvoke, Function verifyInvoke) { + test( + 'pastikan mengembalikan objek ParsingFailure ketika RemoteDataSource menerima respon kegagalan ' + 'dari endpoint', + () async { + // arrange + setUpMockNetworkConnected(); + when(whenInvoke.call()).thenThrow(TypeError()); + + // act + final result = await actInvoke.call(); + + // assert + verify(verifyInvoke.call()); + expect(result.failure, ParsingFailure(TypeError().toString())); + }, + ); + } + group('getProject', () { const tUserId = 'testUserId'; final tResponse = ProjectResponse.fromJson( @@ -187,4 +253,92 @@ void main() { testDisconnected(() => repository.getProject(tUserId)); }); + + group('getProjectTaskByUserId', () { + const userId = 'userId'; + final tResponse = ProjectTaskResponse.fromJson( + json.decode( + fixture('project_task_response.json'), + ), + ); + + test( + 'pastikan mengembalikan objek model ProjectTaskResponse ketika RemoteDataSource berhasil menerima ' + 'respon sukses dari endpoint', + () async { + // arrange + setUpMockNetworkConnected(); + when(mockRemoteDataSource.getProjectTaskByUserId(any)).thenAnswer((_) async => tResponse); + + // act + final result = await repository.getProjectTaskByUserId(userId); + + // assert + verify(mockRemoteDataSource.getProjectTaskByUserId(userId)); + expect(result.response, tResponse); + }, + ); + + test( + 'pastikan mengembalikan objek ServerFailure ketika RemoteDataSource berhasil menerima ' + 'respon timeout dari endpoint', + () async { + // arrange + setUpMockNetworkConnected(); + when(mockRemoteDataSource.getProjectTaskByUserId(any)) + .thenThrow(DioException(requestOptions: tRequestOptions, message: 'testError')); + + // act + final result = await repository.getProjectTaskByUserId(userId); + + // assert + verify(mockRemoteDataSource.getProjectTaskByUserId(userId)); + expect(result.failure, ServerFailure('testError')); + }, + ); + + test( + 'pastikan mengembalikan objek ServerFailure ketika RemoteDataSource menerima respon kegagalan ' + 'dari endpoint', + () async { + // arrange + setUpMockNetworkConnected(); + when(mockRemoteDataSource.getProjectTaskByUserId(any)).thenThrow( + DioException( + requestOptions: tRequestOptions, + message: 'testError', + response: Response( + requestOptions: tRequestOptions, + data: { + 'title': 'testTitleError', + 'message': 'testMessageError', + }, + statusCode: 400, + ), + ), + ); + + // act + final result = await repository.getProjectTaskByUserId(userId); + + // assert + verify(mockRemoteDataSource.getProjectTaskByUserId(userId)); + expect(result.failure, ServerFailure('400 testMessageError')); + }, + ); + + testServerFailureString2( + () => mockRemoteDataSource.getProjectTaskByUserId(any), + () => repository.getProjectTaskByUserId(userId), + () => mockRemoteDataSource.getProjectTaskByUserId(userId), + ); + + testParsingFailure2( + () => mockRemoteDataSource.getProjectTaskByUserId(any), + () => repository.getProjectTaskByUserId(userId), + () => mockRemoteDataSource.getProjectTaskByUserId(userId), + ); + + testDisconnected2(() => repository.getProjectTaskByUserId(userId)); + }); } diff --git a/test/feature/data/repository/setting/setting_repository_impl_test.dart b/test/feature/data/repository/setting/setting_repository_impl_test.dart index 22fd3bb..bf74b8f 100644 --- a/test/feature/data/repository/setting/setting_repository_impl_test.dart +++ b/test/feature/data/repository/setting/setting_repository_impl_test.dart @@ -2,8 +2,11 @@ import 'dart:convert'; import 'package:dio/dio.dart'; import 'package:dipantau_desktop_client/core/error/failure.dart'; +import 'package:dipantau_desktop_client/feature/data/model/all_user_setting/all_user_setting_response.dart'; import 'package:dipantau_desktop_client/feature/data/model/kv_setting/kv_setting_body.dart'; import 'package:dipantau_desktop_client/feature/data/model/kv_setting/kv_setting_response.dart'; +import 'package:dipantau_desktop_client/feature/data/model/user_setting/user_setting_body.dart'; +import 'package:dipantau_desktop_client/feature/data/model/user_setting/user_setting_response.dart'; import 'package:dipantau_desktop_client/feature/data/repository/setting/setting_repository_impl.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:mockito/mockito.dart'; @@ -272,4 +275,263 @@ void main() { testDisconnected(() => repository.setKvSetting(tBody)); }); + + group('getAllUserSetting', () { + final tResponse = AllUserSettingResponse.fromJson( + json.decode( + fixture('all_user_setting_response.json'), + ), + ); + + test( + 'pastikan mengembalikan objek model AllUserSettingResponse ketika remote data source berhasil menerima ' + 'respon sukses dari endpoint', + () async { + // arrange + setUpMockNetworkConnected(); + when(mockRemoteDataSource.getAllUserSetting()).thenAnswer((_) async => tResponse); + + // act + final result = await repository.getAllUserSetting(); + + // assert + verify(mockRemoteDataSource.getAllUserSetting()); + expect(result.response, tResponse); + }, + ); + + test( + 'pastikan mengembalikan objek ServerFailure ketika remote data source berhasil menerima respon timeout dari endpoint', + () async { + // arrange + setUpMockNetworkConnected(); + when(mockRemoteDataSource.getAllUserSetting()) + .thenThrow(DioException(requestOptions: tRequestOptions, message: 'testError')); + + // act + final result = await repository.getAllUserSetting(); + + // assert + verify(mockRemoteDataSource.getAllUserSetting()); + expect(result.failure, ServerFailure('testError')); + }, + ); + + test( + 'pastikan mengembalikan objek ServerFailure ketika remote data source menerima respon kegagalan ' + 'dari endpoint', + () async { + // arrange + setUpMockNetworkConnected(); + when(mockRemoteDataSource.getAllUserSetting()).thenThrow( + DioException( + requestOptions: tRequestOptions, + message: 'testError', + response: Response( + requestOptions: tRequestOptions, + data: { + 'title': 'testTitleError', + 'message': 'testMessageError', + }, + statusCode: 400, + ), + ), + ); + + // act + final result = await repository.getAllUserSetting(); + + // assert + verify(mockRemoteDataSource.getAllUserSetting()); + expect(result.failure, ServerFailure('400 testMessageError')); + }, + ); + + testServerFailureString( + () => mockRemoteDataSource.getAllUserSetting(), + () => repository.getAllUserSetting(), + () => mockRemoteDataSource.getAllUserSetting(), + ); + + testParsingFailure( + () => mockRemoteDataSource.getAllUserSetting(), + () => repository.getAllUserSetting(), + () => mockRemoteDataSource.getAllUserSetting(), + ); + + testDisconnected(() => repository.getAllUserSetting()); + }); + + group('getUserSetting', () { + final tResponse = UserSettingResponse.fromJson( + json.decode( + fixture('user_setting_response.json'), + ), + ); + + test( + 'pastikan mengembalikan objek model UserSettingResponse ketika remote data source berhasil menerima ' + 'respon sukses dari endpoint', + () async { + // arrange + setUpMockNetworkConnected(); + when(mockRemoteDataSource.getUserSetting()).thenAnswer((_) async => tResponse); + + // act + final result = await repository.getUserSetting(); + + // assert + verify(mockRemoteDataSource.getUserSetting()); + expect(result.response, tResponse); + }, + ); + + test( + 'pastikan mengembalikan objek ServerFailure ketika remote data source berhasil menerima respon timeout dari endpoint', + () async { + // arrange + setUpMockNetworkConnected(); + when(mockRemoteDataSource.getUserSetting()) + .thenThrow(DioException(requestOptions: tRequestOptions, message: 'testError')); + + // act + final result = await repository.getUserSetting(); + + // assert + verify(mockRemoteDataSource.getUserSetting()); + expect(result.failure, ServerFailure('testError')); + }, + ); + + test( + 'pastikan mengembalikan objek ServerFailure ketika remote data source menerima respon kegagalan ' + 'dari endpoint', + () async { + // arrange + setUpMockNetworkConnected(); + when(mockRemoteDataSource.getUserSetting()).thenThrow( + DioException( + requestOptions: tRequestOptions, + message: 'testError', + response: Response( + requestOptions: tRequestOptions, + data: { + 'title': 'testTitleError', + 'message': 'testMessageError', + }, + statusCode: 400, + ), + ), + ); + + // act + final result = await repository.getUserSetting(); + + // assert + verify(mockRemoteDataSource.getUserSetting()); + expect(result.failure, ServerFailure('400 testMessageError')); + }, + ); + + testServerFailureString( + () => mockRemoteDataSource.getUserSetting(), + () => repository.getUserSetting(), + () => mockRemoteDataSource.getUserSetting(), + ); + + testParsingFailure( + () => mockRemoteDataSource.getUserSetting(), + () => repository.getUserSetting(), + () => mockRemoteDataSource.getUserSetting(), + ); + + testDisconnected(() => repository.getUserSetting()); + }); + + group('updateUserSetting', () { + final body = UserSettingBody.fromJson( + json.decode( + fixture('user_setting_body.json'), + ), + ); + const tResponse = true; + + test( + 'pastikan mengembalikan nilai boolean true ketika remote data source berhasil menerima ' + 'respon sukses dari endpoint', + () async { + // arrange + setUpMockNetworkConnected(); + when(mockRemoteDataSource.updateUserSetting(any)).thenAnswer((_) async => tResponse); + + // act + final result = await repository.updateUserSetting(body); + + // assert + verify(mockRemoteDataSource.updateUserSetting(body)); + expect(result.response, tResponse); + }, + ); + + test( + 'pastikan mengembalikan objek ServerFailure ketika remote data source berhasil menerima respon timeout dari endpoint', + () async { + // arrange + setUpMockNetworkConnected(); + when(mockRemoteDataSource.updateUserSetting(any)) + .thenThrow(DioException(requestOptions: tRequestOptions, message: 'testError')); + + // act + final result = await repository.updateUserSetting(body); + + // assert + verify(mockRemoteDataSource.updateUserSetting(body)); + expect(result.failure, ServerFailure('testError')); + }, + ); + + test( + 'pastikan mengembalikan objek ServerFailure ketika remote data source menerima respon kegagalan ' + 'dari endpoint', + () async { + // arrange + setUpMockNetworkConnected(); + when(mockRemoteDataSource.updateUserSetting(any)).thenThrow( + DioException( + requestOptions: tRequestOptions, + message: 'testError', + response: Response( + requestOptions: tRequestOptions, + data: { + 'title': 'testTitleError', + 'message': 'testMessageError', + }, + statusCode: 400, + ), + ), + ); + + // act + final result = await repository.updateUserSetting(body); + + // assert + verify(mockRemoteDataSource.updateUserSetting(body)); + expect(result.failure, ServerFailure('400 testMessageError')); + }, + ); + + testServerFailureString( + () => mockRemoteDataSource.updateUserSetting(any), + () => repository.updateUserSetting(body), + () => mockRemoteDataSource.updateUserSetting(body), + ); + + testParsingFailure( + () => mockRemoteDataSource.updateUserSetting(any), + () => repository.updateUserSetting(body), + () => mockRemoteDataSource.updateUserSetting(body), + ); + + testDisconnected(() => repository.updateUserSetting(body)); + }); } diff --git a/test/feature/data/repository/track/track_repository_impl_test.dart b/test/feature/data/repository/track/track_repository_impl_test.dart index 3988e6c..8421e29 100644 --- a/test/feature/data/repository/track/track_repository_impl_test.dart +++ b/test/feature/data/repository/track/track_repository_impl_test.dart @@ -7,6 +7,7 @@ import 'package:dipantau_desktop_client/feature/data/model/create_track/bulk_cre import 'package:dipantau_desktop_client/feature/data/model/create_track/bulk_create_track_image_body.dart'; import 'package:dipantau_desktop_client/feature/data/model/create_track/create_track_body.dart'; import 'package:dipantau_desktop_client/feature/data/model/general/general_response.dart'; +import 'package:dipantau_desktop_client/feature/data/model/manual_create_track/manual_create_track_body.dart'; import 'package:dipantau_desktop_client/feature/data/model/track_user/track_user_response.dart'; import 'package:dipantau_desktop_client/feature/data/model/track_user_lite/track_user_lite_response.dart'; import 'package:dipantau_desktop_client/feature/data/repository/track/track_repository_impl.dart'; @@ -711,4 +712,96 @@ void main() { testDisconnected2(() => repository.deleteTrackUser(trackId)); }); + + group('createManualTrack', () { + final tBody = ManualCreateTrackBody.fromJson( + json.decode( + fixture('manual_create_track_body.json'), + ), + ); + final tResponse = GeneralResponse.fromJson( + json.decode( + fixture('general_response.json'), + ), + ); + + test( + 'pastikan mengembalikan objek model GeneralResponse ketika RemoteDataSource berhasil menerima ' + 'respon sukses dari endpoint', + () async { + // arrange + setUpMockNetworkConnected(); + when(mockRemoteDataSource.createManualTrack(any)).thenAnswer((_) async => tResponse); + + // act + final result = await repository.createManualTrack(tBody); + + // assert + verify(mockRemoteDataSource.createManualTrack(tBody)); + expect(result.response, tResponse); + }, + ); + + test( + 'pastikan mengembalikan objek ServerFailure ketika RemoteDataSource berhasil menerima ' + 'respon timeout dari endpoint', + () async { + // arrange + setUpMockNetworkConnected(); + when(mockRemoteDataSource.createManualTrack(any)) + .thenThrow(DioException(requestOptions: tRequestOptions, message: 'testError')); + + // act + final result = await repository.createManualTrack(tBody); + + // assert + verify(mockRemoteDataSource.createManualTrack(tBody)); + expect(result.failure, ServerFailure('testError')); + }, + ); + + test( + 'pastikan mengembalikan objek ServerFailure ketika RemoteDataSource menerima respon kegagalan ' + 'dari endpoint', + () async { + // arrange + setUpMockNetworkConnected(); + when(mockRemoteDataSource.createManualTrack(any)).thenThrow( + DioException( + requestOptions: tRequestOptions, + message: 'testError', + response: Response( + requestOptions: tRequestOptions, + data: { + 'title': 'testTitleError', + 'message': 'testMessageError', + }, + statusCode: 400, + ), + ), + ); + + // act + final result = await repository.createManualTrack(tBody); + + // assert + verify(mockRemoteDataSource.createManualTrack(tBody)); + expect(result.failure, ServerFailure('400 testMessageError')); + }, + ); + + testServerFailureString2( + () => mockRemoteDataSource.createManualTrack(any), + () => repository.createManualTrack(tBody), + () => mockRemoteDataSource.createManualTrack(tBody), + ); + + testParsingFailure2( + () => mockRemoteDataSource.createManualTrack(any), + () => repository.createManualTrack(tBody), + () => mockRemoteDataSource.createManualTrack(tBody), + ); + + testDisconnected2(() => repository.createManualTrack(tBody)); + }); } diff --git a/test/feature/domain/usecase/create_manual_track/create_manual_track_test.dart b/test/feature/domain/usecase/create_manual_track/create_manual_track_test.dart new file mode 100644 index 0000000..3a2c8ae --- /dev/null +++ b/test/feature/domain/usecase/create_manual_track/create_manual_track_test.dart @@ -0,0 +1,73 @@ +import 'dart:convert'; + +import 'package:dipantau_desktop_client/feature/data/model/general/general_response.dart'; +import 'package:dipantau_desktop_client/feature/data/model/manual_create_track/manual_create_track_body.dart'; +import 'package:dipantau_desktop_client/feature/domain/usecase/create_manual_track/create_manual_track.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:mockito/mockito.dart'; + +import '../../../../fixture/fixture_reader.dart'; +import '../../../../helper/mock_helper.mocks.dart'; + +void main() { + late CreateManualTrack useCase; + late MockTrackRepository mockRepository; + + setUp(() { + mockRepository = MockTrackRepository(); + useCase = CreateManualTrack(repository: mockRepository); + }); + + final tBody = ManualCreateTrackBody.fromJson( + json.decode( + fixture('manual_create_track_body.json'), + ), + ); + final tParams = ParamsCreateManualTrack(body: tBody); + + test( + 'pastikan objek repository berhasil menerima respon sukses atau gagal dari endpoint', + () async { + // arrange + final tResponse = GeneralResponse.fromJson( + json.decode( + fixture('general_response.json'), + ), + ); + final tResult = (failure: null, response: tResponse); + when(mockRepository.createManualTrack(any)).thenAnswer((_) async => tResult); + + // act + final result = await useCase(tParams); + + // assert + expect(result, tResult); + verify(mockRepository.createManualTrack(tBody)); + verifyNoMoreInteractions(mockRepository); + }, + ); + + test( + 'pastikan output dari nilai props', + () async { + // assert + expect( + tParams.props, + [ + tParams.body, + ], + ); + }, + ); + + test( + 'pastikan output dari fungsi toString', + () async { + // assert + expect( + tParams.toString(), + 'ParamsCreateManualTrack{body: ${tParams.body}}', + ); + }, + ); +} diff --git a/test/feature/domain/usecase/get_all_user_setting/get_all_user_setting_test.dart b/test/feature/domain/usecase/get_all_user_setting/get_all_user_setting_test.dart new file mode 100644 index 0000000..f79468f --- /dev/null +++ b/test/feature/domain/usecase/get_all_user_setting/get_all_user_setting_test.dart @@ -0,0 +1,42 @@ +import 'dart:convert'; + +import 'package:dipantau_desktop_client/core/usecase/usecase.dart'; +import 'package:dipantau_desktop_client/feature/data/model/all_user_setting/all_user_setting_response.dart'; +import 'package:dipantau_desktop_client/feature/domain/usecase/get_all_user_setting/get_all_user_setting.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:mockito/mockito.dart'; + +import '../../../../fixture/fixture_reader.dart'; +import '../../../../helper/mock_helper.mocks.dart'; + +void main() { + late GetAllUserSetting useCase; + late MockSettingRepository mockRepository; + + setUp(() { + mockRepository = MockSettingRepository(); + useCase = GetAllUserSetting(repository: mockRepository); + }); + + test( + 'pastikan objek repository berhasil menerima respon sukses atau gagal dari endpoint', + () async { + // arrange + final tResponse = AllUserSettingResponse.fromJson( + json.decode( + fixture('all_user_setting_response.json'), + ), + ); + final tResult = (failure: null, response: tResponse); + when(mockRepository.getAllUserSetting()).thenAnswer((_) async => tResult); + + // act + final result = await useCase(NoParams()); + + // assert + expect(result, tResult); + verify(mockRepository.getAllUserSetting()); + verifyNoMoreInteractions(mockRepository); + }, + ); +} diff --git a/test/feature/domain/usecase/get_project_task_by_user_id/get_project_task_by_user_id_test.dart b/test/feature/domain/usecase/get_project_task_by_user_id/get_project_task_by_user_id_test.dart new file mode 100644 index 0000000..00826ba --- /dev/null +++ b/test/feature/domain/usecase/get_project_task_by_user_id/get_project_task_by_user_id_test.dart @@ -0,0 +1,68 @@ +import 'dart:convert'; + +import 'package:dipantau_desktop_client/feature/data/model/project_task/project_task_response.dart'; +import 'package:dipantau_desktop_client/feature/domain/usecase/get_project_task_by_user_id/get_project_task_by_user_id.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:mockito/mockito.dart'; + +import '../../../../fixture/fixture_reader.dart'; +import '../../../../helper/mock_helper.mocks.dart'; + +void main() { + late GetProjectTaskByUserId useCase; + late MockProjectRepository mockRepository; + + setUp(() { + mockRepository = MockProjectRepository(); + useCase = GetProjectTaskByUserId(repository: mockRepository); + }); + + const userId = 'userId'; + final tParams = ParamsGetProjectTaskByUserId(userId: userId); + + test( + 'pastikan objek repository berhasil menerima respon sukses atau gagal dari endpoint', + () async { + // arrange + final tResponse = ProjectTaskResponse.fromJson( + json.decode( + fixture('project_task_response.json'), + ), + ); + final tResult = (failure: null, response: tResponse); + when(mockRepository.getProjectTaskByUserId(any)).thenAnswer((_) async => tResult); + + // act + final result = await useCase(tParams); + + // assert + expect(result, tResult); + verify(mockRepository.getProjectTaskByUserId(userId)); + verifyNoMoreInteractions(mockRepository); + }, + ); + + test( + 'pastikan output dari nilai props', + () async { + // assert + expect( + tParams.props, + [ + tParams.userId, + ], + ); + }, + ); + + test( + 'pastikan output dari fungsi toString', + () async { + // assert + expect( + tParams.toString(), + 'ParamsGetProjectTaskByUserId{userId: ${tParams.userId}}', + ); + }, + ); +} diff --git a/test/feature/domain/usecase/get_user_setting/get_user_setting_test.dart b/test/feature/domain/usecase/get_user_setting/get_user_setting_test.dart new file mode 100644 index 0000000..da68ea8 --- /dev/null +++ b/test/feature/domain/usecase/get_user_setting/get_user_setting_test.dart @@ -0,0 +1,42 @@ +import 'dart:convert'; + +import 'package:dipantau_desktop_client/core/usecase/usecase.dart'; +import 'package:dipantau_desktop_client/feature/data/model/user_setting/user_setting_response.dart'; +import 'package:dipantau_desktop_client/feature/domain/usecase/get_user_setting/get_user_setting.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:mockito/mockito.dart'; + +import '../../../../fixture/fixture_reader.dart'; +import '../../../../helper/mock_helper.mocks.dart'; + +void main() { + late GetUserSetting useCase; + late MockSettingRepository mockRepository; + + setUp(() { + mockRepository = MockSettingRepository(); + useCase = GetUserSetting(repository: mockRepository); + }); + + test( + 'pastikan objek repository berhasil menerima respon sukses atau gagal dari endpoint', + () async { + // arrange + final tResponse = UserSettingResponse.fromJson( + json.decode( + fixture('user_setting_response.json'), + ), + ); + final tResult = (failure: null, response: tResponse); + when(mockRepository.getUserSetting()).thenAnswer((_) async => tResult); + + // act + final result = await useCase(NoParams()); + + // assert + expect(result, tResult); + verify(mockRepository.getUserSetting()); + verifyNoMoreInteractions(mockRepository); + }, + ); +} diff --git a/test/feature/domain/usecase/ping/ping_test.dart b/test/feature/domain/usecase/ping/ping_test.dart new file mode 100644 index 0000000..609071e --- /dev/null +++ b/test/feature/domain/usecase/ping/ping_test.dart @@ -0,0 +1,68 @@ +import 'dart:convert'; + +import 'package:dipantau_desktop_client/feature/data/model/general/general_response.dart'; +import 'package:dipantau_desktop_client/feature/domain/usecase/ping/ping.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:mockito/mockito.dart'; + +import '../../../../fixture/fixture_reader.dart'; +import '../../../../helper/mock_helper.mocks.dart'; + +void main() { + late Ping useCase; + late MockGeneralRepository mockRepository; + + setUp(() { + mockRepository = MockGeneralRepository(); + useCase = Ping(repository: mockRepository); + }); + + const hostname = 'https://example.com'; + final tParams = ParamsPing(baseUrl: hostname); + + test( + 'pastikan objek repository berhasil menerima respon sukses atau gagal dari endpoint', + () async { + // arrange + final tResponse = GeneralResponse.fromJson( + json.decode( + fixture('general_response.json'), + ), + ); + final tResult = (failure: null, response: tResponse); + when(mockRepository.ping(any)).thenAnswer((_) async => tResult); + + // act + final result = await useCase(tParams); + + // assert + expect(result, tResult); + verify(mockRepository.ping(hostname)); + verifyNoMoreInteractions(mockRepository); + }, + ); + + test( + 'pastikan output dari nilai props', + () async { + // assert + expect( + tParams.props, + [ + tParams.baseUrl, + ], + ); + }, + ); + + test( + 'pastikan output dari fungsi toString', + () async { + // assert + expect( + tParams.toString(), + 'ParamsPing{baseUrl: ${tParams.baseUrl}}', + ); + }, + ); +} diff --git a/test/feature/domain/usecase/update_user_setting/update_user_setting_test.dart b/test/feature/domain/usecase/update_user_setting/update_user_setting_test.dart new file mode 100644 index 0000000..bda17f6 --- /dev/null +++ b/test/feature/domain/usecase/update_user_setting/update_user_setting_test.dart @@ -0,0 +1,68 @@ +import 'dart:convert'; + +import 'package:dipantau_desktop_client/feature/data/model/user_setting/user_setting_body.dart'; +import 'package:dipantau_desktop_client/feature/domain/usecase/update_user_setting/update_user_setting.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:mockito/mockito.dart'; + +import '../../../../fixture/fixture_reader.dart'; +import '../../../../helper/mock_helper.mocks.dart'; + +void main() { + late UpdateUserSetting useCase; + late MockSettingRepository mockRepository; + + setUp(() { + mockRepository = MockSettingRepository(); + useCase = UpdateUserSetting(repository: mockRepository); + }); + + final body = UserSettingBody.fromJson( + json.decode( + fixture('user_setting_body.json'), + ), + ); + final params = ParamsUpdateUserSetting(body: body); + + test( + 'pastikan objek repository berhasil menerima respon sukses atau gagal dari endpoint', + () async { + // arrange + const tResponse = true; + const tResult = (failure: null, response: tResponse); + when(mockRepository.updateUserSetting(any)).thenAnswer((_) async => tResult); + + // act + final result = await useCase(params); + + // assert + expect(result, tResult); + verify(mockRepository.updateUserSetting(body)); + verifyNoMoreInteractions(mockRepository); + }, + ); + + test( + 'pastikan output dari nilai props', + () async { + // assert + expect( + params.props, + [ + params.body, + ], + ); + }, + ); + + test( + 'pastikan output dari fungsi toString', + () async { + // assert + expect( + params.toString(), + 'ParamsUpdateUserSetting{body: ${params.body}}', + ); + }, + ); +} diff --git a/test/feature/presentation/bloc/manual_tracking/manual_tracking_bloc_test.dart b/test/feature/presentation/bloc/manual_tracking/manual_tracking_bloc_test.dart new file mode 100644 index 0000000..22dc996 --- /dev/null +++ b/test/feature/presentation/bloc/manual_tracking/manual_tracking_bloc_test.dart @@ -0,0 +1,235 @@ +import 'dart:convert'; + +import 'package:bloc_test/bloc_test.dart'; +import 'package:dipantau_desktop_client/core/error/failure.dart'; +import 'package:dipantau_desktop_client/feature/data/model/general/general_response.dart'; +import 'package:dipantau_desktop_client/feature/data/model/manual_create_track/manual_create_track_body.dart'; +import 'package:dipantau_desktop_client/feature/data/model/project_task/project_task_response.dart'; +import 'package:dipantau_desktop_client/feature/domain/usecase/create_manual_track/create_manual_track.dart'; +import 'package:dipantau_desktop_client/feature/domain/usecase/get_project_task_by_user_id/get_project_task_by_user_id.dart'; +import 'package:dipantau_desktop_client/feature/presentation/bloc/manual_tracking/manual_tracking_bloc.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:mockito/mockito.dart'; + +import '../../../../fixture/fixture_reader.dart'; +import '../../../../helper/mock_helper.mocks.dart'; + +void main() { + late ManualTrackingBloc bloc; + late MockHelper mockHelper; + late MockCreateManualTrack mockCreateManualTrack; + late MockGetProjectTaskByUserId mockGetProjectTaskByUserId; + + setUp(() { + mockHelper = MockHelper(); + mockCreateManualTrack = MockCreateManualTrack(); + mockGetProjectTaskByUserId = MockGetProjectTaskByUserId(); + bloc = ManualTrackingBloc( + helper: mockHelper, + createManualTrack: mockCreateManualTrack, + getProjectTaskByUserId: mockGetProjectTaskByUserId, + ); + }); + + const tErrorMessage = 'testErrorMessage'; + + test( + 'pastikan output dari initial state', + () async { + // assert + expect( + bloc.state, + isA(), + ); + }, + ); + + group('create manual tracking', () { + final body = ManualCreateTrackBody.fromJson( + json.decode( + fixture('manual_create_track_body.json'), + ), + ); + final params = ParamsCreateManualTrack(body: body); + final event = CreateManualTrackingEvent(body: body); + + blocTest( + 'pastikan emit [LoadingManualTrackingState, SuccessCreateManualTrackingState] ketika terima event ' + 'CreateManualTrackingEvent dengan proses berhasil', + build: () { + final response = GeneralResponse.fromJson( + json.decode( + fixture('general_response.json'), + ), + ); + final result = (failure: null, response: response); + when(mockCreateManualTrack(any)).thenAnswer((_) async => result); + return bloc; + }, + act: (ManualTrackingBloc bloc) { + return bloc.add(event); + }, + expect: () => [ + isA(), + isA(), + ], + verify: (_) { + verify(mockCreateManualTrack(params)); + }, + ); + + blocTest( + 'pastikan emit [LoadingManualTrackingState, FailureManualTrackingState] ketika terima event ' + 'CreateManualTrackingEvent dengan proses gagal dari endpoint', + build: () { + final result = (failure: ServerFailure(tErrorMessage), response: null); + when(mockCreateManualTrack(any)).thenAnswer((_) async => result); + return bloc; + }, + act: (ManualTrackingBloc bloc) { + return bloc.add(event); + }, + expect: () => [ + isA(), + isA(), + ], + verify: (_) { + verify(mockCreateManualTrack(params)); + }, + ); + + blocTest( + 'pastikan emit [LoadingManualTrackingState, FailureManualTrackingState] ketika terima event ' + 'CreateManualTrackingEvent dengan kondisi internet tidak terhubung ketika hit endpoint', + build: () { + final result = (failure: ConnectionFailure(), response: null); + when(mockCreateManualTrack(any)).thenAnswer((_) async => result); + return bloc; + }, + act: (ManualTrackingBloc bloc) { + return bloc.add(event); + }, + expect: () => [ + isA(), + isA(), + ], + verify: (_) { + verify(mockCreateManualTrack(params)); + }, + ); + + blocTest( + 'pastikan emit [LoadingManualTrackingState, FailureManualTrackingState] ketika terima event ' + 'CreateManualTrackingEvent dengan proses gagal parsing respon JSON dari endpoint', + build: () { + final result = (failure: ParsingFailure(tErrorMessage), response: null); + when(mockCreateManualTrack(any)).thenAnswer((_) async => result); + return bloc; + }, + act: (ManualTrackingBloc bloc) { + return bloc.add(event); + }, + expect: () => [ + isA(), + isA(), + ], + verify: (_) { + verify(mockCreateManualTrack(params)); + }, + ); + }); + + group('load data project task', () { + const userId = 'userId'; + final params = ParamsGetProjectTaskByUserId(userId: userId); + final event = LoadDataProjectTaskManualTrackingEvent(userId: userId); + + blocTest( + 'pastikan emit [LoadingManualTrackingState, SuccessLoadDataProjectTaskManualTrackingState] ketika terima event ' + 'LoadDataProjectTaskManualTrackingEvent dengan proses berhasil', + build: () { + final response = ProjectTaskResponse.fromJson( + json.decode( + fixture('project_task_response.json'), + ), + ); + final result = (failure: null, response: response); + when(mockGetProjectTaskByUserId(any)).thenAnswer((_) async => result); + return bloc; + }, + act: (ManualTrackingBloc bloc) { + return bloc.add(event); + }, + expect: () => [ + isA(), + isA(), + ], + verify: (_) async { + verify(mockGetProjectTaskByUserId(params)); + }, + ); + + blocTest( + 'pastikan emit [LoadingManualTrackingState, FailureCenterManualTrackingState] ketika terima event ' + 'LoadDataProjectTaskManualTrackingEvent dengan proses gagal dari endpoint', + build: () { + final failure = ServerFailure('errorMessage'); + final result = (failure: failure, response: null); + when(mockGetProjectTaskByUserId(any)).thenAnswer((_) async => result); + return bloc; + }, + act: (ManualTrackingBloc bloc) { + return bloc.add(event); + }, + expect: () => [ + isA(), + isA(), + ], + verify: (_) async { + verify(mockGetProjectTaskByUserId(params)); + }, + ); + + blocTest( + 'pastikan emit [LoadingManualTrackingState, FailureCenterManualTrackingState] ketika terima event ' + 'LoadDataProjectTaskManualTrackingEvent dengan kondisi internet tidak terhubung', + build: () { + final failure = ConnectionFailure(); + final result = (failure: failure, response: null); + when(mockGetProjectTaskByUserId(any)).thenAnswer((_) async => result); + return bloc; + }, + act: (ManualTrackingBloc bloc) { + return bloc.add(event); + }, + expect: () => [ + isA(), + isA(), + ], + verify: (_) async { + verify(mockGetProjectTaskByUserId(params)); + }, + ); + + blocTest( + 'pastikan emit [LoadingManualTrackingState, FailureCenterManualTrackingState] ketika terima event ' + 'LoadDataProjectTaskManualTrackingEvent dengan proses gagal parsing respon JSON dari endpoint', + build: () { + final failure = ParsingFailure('errorMessage'); + final result = (failure: failure, response: null); + when(mockGetProjectTaskByUserId(any)).thenAnswer((_) async => result); + return bloc; + }, + act: (ManualTrackingBloc bloc) { + return bloc.add(event); + }, + expect: () => [ + isA(), + isA(), + ], + verify: (_) async { + verify(mockGetProjectTaskByUserId(params)); + }, + ); + }); +} diff --git a/test/feature/presentation/bloc/manual_tracking/manual_tracking_event_test.dart b/test/feature/presentation/bloc/manual_tracking/manual_tracking_event_test.dart new file mode 100644 index 0000000..6a74395 --- /dev/null +++ b/test/feature/presentation/bloc/manual_tracking/manual_tracking_event_test.dart @@ -0,0 +1,44 @@ +import 'dart:convert'; + +import 'package:dipantau_desktop_client/feature/data/model/manual_create_track/manual_create_track_body.dart'; +import 'package:dipantau_desktop_client/feature/presentation/bloc/manual_tracking/manual_tracking_bloc.dart'; +import 'package:flutter_test/flutter_test.dart'; + +import '../../../../fixture/fixture_reader.dart'; + +void main() { + group('CreateManualTrackingEvent', () { + final body = ManualCreateTrackBody.fromJson( + json.decode( + fixture('manual_create_track_body.json'), + ), + ); + final event = CreateManualTrackingEvent(body: body); + + test( + 'pastikan output dari fungsi toString', + () async { + // assert + expect( + event.toString(), + 'CreateManualTrackingEvent{body: $body}', + ); + }, + ); + }); + + group('LoadDataProjectTaskManualTrackingEvent', () { + final event = LoadDataProjectTaskManualTrackingEvent(userId: 'userId'); + + test( + 'pastikan output dari fungsi toString', + () async { + // assert + expect( + event.toString(), + 'LoadDataProjectTaskManualTrackingEvent{userId: ${event.userId}}', + ); + }, + ); + }); +} diff --git a/test/feature/presentation/bloc/manual_tracking/manual_tracking_state_test.dart b/test/feature/presentation/bloc/manual_tracking/manual_tracking_state_test.dart new file mode 100644 index 0000000..804774f --- /dev/null +++ b/test/feature/presentation/bloc/manual_tracking/manual_tracking_state_test.dart @@ -0,0 +1,34 @@ +import 'package:dipantau_desktop_client/feature/presentation/bloc/manual_tracking/manual_tracking_bloc.dart'; +import 'package:flutter_test/flutter_test.dart'; + +void main() { + group('FailureManualTrackingState', () { + final state = FailureManualTrackingState(errorMessage: 'errorMessage'); + + test( + 'pastikan output dari fungsi toString', + () async { + // assert + expect( + state.toString(), + 'FailureManualTrackingState{errorMessage: ${state.errorMessage}}', + ); + }, + ); + }); + + group('FailureCenterManualTrackingState', () { + final state = FailureCenterManualTrackingState(errorMessage: 'errorMessage'); + + test( + 'pastikan output dari fungsi toString', + () async { + // assert + expect( + state.toString(), + 'FailureCenterManualTrackingState{errorMessage: ${state.errorMessage}}', + ); + }, + ); + }); +} diff --git a/test/feature/presentation/bloc/setting/setting_bloc_test.dart b/test/feature/presentation/bloc/setting/setting_bloc_test.dart index d21ae2e..d6193b8 100644 --- a/test/feature/presentation/bloc/setting/setting_bloc_test.dart +++ b/test/feature/presentation/bloc/setting/setting_bloc_test.dart @@ -3,9 +3,13 @@ import 'dart:convert'; import 'package:bloc_test/bloc_test.dart'; import 'package:dipantau_desktop_client/core/error/failure.dart'; import 'package:dipantau_desktop_client/core/usecase/usecase.dart'; +import 'package:dipantau_desktop_client/feature/data/model/all_user_setting/all_user_setting_response.dart'; import 'package:dipantau_desktop_client/feature/data/model/kv_setting/kv_setting_body.dart'; import 'package:dipantau_desktop_client/feature/data/model/kv_setting/kv_setting_response.dart'; +import 'package:dipantau_desktop_client/feature/data/model/user_setting/user_setting_body.dart'; +import 'package:dipantau_desktop_client/feature/data/model/user_setting/user_setting_response.dart'; import 'package:dipantau_desktop_client/feature/domain/usecase/set_kv_setting/set_kv_setting.dart'; +import 'package:dipantau_desktop_client/feature/domain/usecase/update_user_setting/update_user_setting.dart'; import 'package:dipantau_desktop_client/feature/presentation/bloc/setting/setting_bloc.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:mockito/mockito.dart'; @@ -18,15 +22,24 @@ void main() { late MockHelper mockHelper; late MockGetKvSetting mockGetKvSetting; late MockSetKvSetting mockSetKvSetting; + late MockGetUserSetting mockGetUserSetting; + late MockGetAllUserSetting mockGetAllUserSetting; + late MockUpdateUserSetting mockUpdateUserSetting; setUp(() { mockHelper = MockHelper(); mockGetKvSetting = MockGetKvSetting(); mockSetKvSetting = MockSetKvSetting(); + mockGetUserSetting = MockGetUserSetting(); + mockGetAllUserSetting = MockGetAllUserSetting(); + mockUpdateUserSetting = MockUpdateUserSetting(); bloc = SettingBloc( helper: mockHelper, getKvSetting: mockGetKvSetting, setKvSetting: mockSetKvSetting, + getUserSetting: mockGetUserSetting, + getAllUserSetting: mockGetAllUserSetting, + updateUserSetting: mockUpdateUserSetting, ); }); @@ -227,4 +240,287 @@ void main() { }, ); }); + + group('load user setting', () { + final tEvent = LoadUserSettingEvent(); + final tParams = NoParams(); + final tResponse = UserSettingResponse.fromJson( + json.decode( + fixture('user_setting_response.json'), + ), + ); + + blocTest( + 'pastikan emit [LoadingCenterSettingState, SuccessLoadUserSettingState] ketika terima event ' + 'LoadUserSettingEvent dengan proses berhasil', + build: () { + final result = (failure: null, response: tResponse); + when(mockGetUserSetting(any)).thenAnswer((_) async => result); + return bloc; + }, + act: (SettingBloc bloc) { + return bloc.add(tEvent); + }, + wait: const Duration(milliseconds: 500), + expect: () => [ + isA(), + isA(), + ], + verify: (_) { + verify(mockGetUserSetting(tParams)); + }, + ); + + blocTest( + 'pastikan emit [LoadingCenterSettingState, FailureSettingState] ketika terima event ' + 'LoadUserSettingEvent dengan proses gagal dari endpoint', + build: () { + final result = (failure: ServerFailure(tErrorMessage), response: null); + when(mockGetUserSetting(any)).thenAnswer((_) async => result); + return bloc; + }, + act: (SettingBloc bloc) { + return bloc.add(tEvent); + }, + wait: const Duration(milliseconds: 500), + expect: () => [ + isA(), + isA(), + ], + verify: (_) { + verify(mockGetUserSetting(tParams)); + }, + ); + + blocTest( + 'pastikan emit [LoadingCenterSettingState, FailureSettingState] ketika terima event ' + 'LoadUserSettingEvent dengan kondisi internet tidak terhubung', + build: () { + final result = (failure: ConnectionFailure(), response: null); + when(mockGetUserSetting(any)).thenAnswer((_) async => result); + return bloc; + }, + act: (SettingBloc bloc) { + return bloc.add(tEvent); + }, + wait: const Duration(milliseconds: 500), + expect: () => [ + isA(), + isA(), + ], + verify: (_) { + verify(mockGetUserSetting(tParams)); + }, + ); + + blocTest( + 'pastikan emit [LoadingCenterSettingState, FailureSettingState] ketika terima event ' + 'LoadUserSettingEvent dengan proses gagal parsing respon dari endpoint', + build: () { + final result = (failure: ParsingFailure(tErrorMessage), response: null); + when(mockGetUserSetting(any)).thenAnswer((_) async => result); + return bloc; + }, + act: (SettingBloc bloc) { + return bloc.add(tEvent); + }, + wait: const Duration(milliseconds: 500), + expect: () => [ + isA(), + isA(), + ], + verify: (_) { + verify(mockGetUserSetting(tParams)); + }, + ); + }); + + group('load all user setting', () { + final tEvent = LoadAllUserSettingEvent(); + final tParams = NoParams(); + final tResponse = AllUserSettingResponse.fromJson( + json.decode( + fixture('all_user_setting_response.json'), + ), + ); + + blocTest( + 'pastikan emit [LoadingCenterSettingState, SuccessLoadAllUserSettingState] ketika terima event ' + 'LoadAllUserSettingEvent dengan proses berhasil', + build: () { + final result = (failure: null, response: tResponse); + when(mockGetAllUserSetting(any)).thenAnswer((_) async => result); + return bloc; + }, + act: (SettingBloc bloc) { + return bloc.add(tEvent); + }, + expect: () => [ + isA(), + isA(), + ], + verify: (_) { + verify(mockGetAllUserSetting(tParams)); + }, + ); + + blocTest( + 'pastikan emit [LoadingCenterSettingState, FailureSettingState] ketika terima event ' + 'LoadAllUserSettingEvent dengan proses gagal dari endpoint', + build: () { + final result = (failure: ServerFailure(tErrorMessage), response: null); + when(mockGetAllUserSetting(any)).thenAnswer((_) async => result); + return bloc; + }, + act: (SettingBloc bloc) { + return bloc.add(tEvent); + }, + expect: () => [ + isA(), + isA(), + ], + verify: (_) { + verify(mockGetAllUserSetting(tParams)); + }, + ); + + blocTest( + 'pastikan emit [LoadingCenterSettingState, FailureSettingState] ketika terima event ' + 'LoadAllUserSettingEvent dengan kondisi internet tidak terhubung', + build: () { + final result = (failure: ConnectionFailure(), response: null); + when(mockGetAllUserSetting(any)).thenAnswer((_) async => result); + return bloc; + }, + act: (SettingBloc bloc) { + return bloc.add(tEvent); + }, + expect: () => [ + isA(), + isA(), + ], + verify: (_) { + verify(mockGetAllUserSetting(tParams)); + }, + ); + + blocTest( + 'pastikan emit [LoadingCenterSettingState, FailureSettingState] ketika terima event ' + 'LoadAllUserSettingEvent dengan proses gagal parsing respon dari endpoint', + build: () { + final result = (failure: ParsingFailure(tErrorMessage), response: null); + when(mockGetAllUserSetting(any)).thenAnswer((_) async => result); + return bloc; + }, + act: (SettingBloc bloc) { + return bloc.add(tEvent); + }, + expect: () => [ + isA(), + isA(), + ], + verify: (_) { + verify(mockGetAllUserSetting(tParams)); + }, + ); + }); + + group('update user setting', () { + final tBody = UserSettingBody.fromJson( + json.decode( + fixture('user_setting_body.json'), + ), + ); + final tEvent = UpdateUserSettingEvent( + body: tBody, + ); + final tParams = ParamsUpdateUserSetting( + body: tBody, + ); + const tResponse = true; + + blocTest( + 'pastikan emit [LoadingButtonSettingState, SuccessUpdateUserSettingState] ketika terima event ' + 'UpdateUserSettingEvent dengan proses berhasil', + build: () { + const result = (failure: null, response: tResponse); + when(mockUpdateUserSetting(any)).thenAnswer((_) async => result); + return bloc; + }, + act: (SettingBloc bloc) { + return bloc.add(tEvent); + }, + wait: const Duration(milliseconds: 500), + expect: () => [ + isA(), + isA(), + ], + verify: (_) { + verify(mockUpdateUserSetting(tParams)); + }, + ); + + blocTest( + 'pastikan emit [LoadingButtonSettingState, FailureSnackBarSettingState] ketika terima event ' + 'UpdateUserSettingEvent dengan proses gagal dari endpoint', + build: () { + final result = (failure: ServerFailure(tErrorMessage), response: null); + when(mockUpdateUserSetting(any)).thenAnswer((_) async => result); + return bloc; + }, + act: (SettingBloc bloc) { + return bloc.add(tEvent); + }, + wait: const Duration(milliseconds: 500), + expect: () => [ + isA(), + isA(), + ], + verify: (_) { + verify(mockUpdateUserSetting(tParams)); + }, + ); + + blocTest( + 'pastikan emit [LoadingButtonSettingState, FailureSnackBarSettingState] ketika terima event ' + 'UpdateUserSettingEvent dengan kondisi internet tidak terhubung', + build: () { + final result = (failure: ConnectionFailure(), response: null); + when(mockUpdateUserSetting(any)).thenAnswer((_) async => result); + return bloc; + }, + act: (SettingBloc bloc) { + return bloc.add(tEvent); + }, + wait: const Duration(milliseconds: 500), + expect: () => [ + isA(), + isA(), + ], + verify: (_) { + verify(mockUpdateUserSetting(tParams)); + }, + ); + + blocTest( + 'pastikan emit [LoadingButtonSettingState, FailureSnackBarSettingState] ketika terima event ' + 'UpdateUserSettingEvent dengan proses gagal parsing respon dari endpoint', + build: () { + final result = (failure: ParsingFailure(tErrorMessage), response: null); + when(mockUpdateUserSetting(any)).thenAnswer((_) async => result); + return bloc; + }, + act: (SettingBloc bloc) { + return bloc.add(tEvent); + }, + wait: const Duration(milliseconds: 500), + expect: () => [ + isA(), + isA(), + ], + verify: (_) { + verify(mockUpdateUserSetting(tParams)); + }, + ); + }); } diff --git a/test/feature/presentation/bloc/setting/setting_event_test.dart b/test/feature/presentation/bloc/setting/setting_event_test.dart index ab4312e..54e57af 100644 --- a/test/feature/presentation/bloc/setting/setting_event_test.dart +++ b/test/feature/presentation/bloc/setting/setting_event_test.dart @@ -1,6 +1,7 @@ import 'dart:convert'; import 'package:dipantau_desktop_client/feature/data/model/kv_setting/kv_setting_body.dart'; +import 'package:dipantau_desktop_client/feature/data/model/user_setting/user_setting_body.dart'; import 'package:dipantau_desktop_client/feature/presentation/bloc/setting/setting_bloc.dart'; import 'package:flutter_test/flutter_test.dart'; @@ -27,4 +28,25 @@ void main() { }, ); }); + + group('UpdateUserSettingEvent', () { + final tEvent = UpdateUserSettingEvent( + body: UserSettingBody.fromJson( + json.decode( + fixture('user_setting_body.json'), + ), + ), + ); + + test( + 'pastikan output dari fungsi toString', + () async { + // assert + expect( + tEvent.toString(), + 'UpdateUserSettingEvent{body: ${tEvent.body}}', + ); + }, + ); + }); } diff --git a/test/feature/presentation/bloc/setting/setting_state_test.dart b/test/feature/presentation/bloc/setting/setting_state_test.dart index 57e4c82..bcc85ba 100644 --- a/test/feature/presentation/bloc/setting/setting_state_test.dart +++ b/test/feature/presentation/bloc/setting/setting_state_test.dart @@ -1,6 +1,8 @@ import 'dart:convert'; +import 'package:dipantau_desktop_client/feature/data/model/all_user_setting/all_user_setting_response.dart'; import 'package:dipantau_desktop_client/feature/data/model/kv_setting/kv_setting_response.dart'; +import 'package:dipantau_desktop_client/feature/data/model/user_setting/user_setting_response.dart'; import 'package:dipantau_desktop_client/feature/presentation/bloc/setting/setting_bloc.dart'; import 'package:flutter_test/flutter_test.dart'; @@ -57,4 +59,44 @@ void main() { }, ); }); + + group('SuccessLoadUserSettingState', () { + final response = UserSettingResponse.fromJson( + json.decode( + fixture('user_setting_response.json'), + ), + ); + final state = SuccessLoadUserSettingState(response: response); + + test( + 'pastikan output dari fungsi toString', + () async { + // assert + expect( + state.toString(), + 'SuccessLoadUserSettingState{response: $response}', + ); + }, + ); + }); + + group('SuccessLoadAllUserSettingState', () { + final response = AllUserSettingResponse.fromJson( + json.decode( + fixture('all_user_setting_response.json'), + ), + ); + final state = SuccessLoadAllUserSettingState(response: response); + + test( + 'pastikan output dari fungsi toString', + () async { + // assert + expect( + state.toString(), + 'SuccessLoadAllUserSettingState{response: $response}', + ); + }, + ); + }); } diff --git a/test/feature/presentation/bloc/setup_credential/setup_credential_bloc_test.dart b/test/feature/presentation/bloc/setup_credential/setup_credential_bloc_test.dart new file mode 100644 index 0000000..7da245a --- /dev/null +++ b/test/feature/presentation/bloc/setup_credential/setup_credential_bloc_test.dart @@ -0,0 +1,131 @@ +import 'dart:convert'; + +import 'package:bloc_test/bloc_test.dart'; +import 'package:dipantau_desktop_client/core/error/failure.dart'; +import 'package:dipantau_desktop_client/feature/data/model/general/general_response.dart'; +import 'package:dipantau_desktop_client/feature/domain/usecase/ping/ping.dart'; +import 'package:dipantau_desktop_client/feature/presentation/bloc/setup_credential/setup_credential_bloc.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:mockito/mockito.dart'; + +import '../../../../fixture/fixture_reader.dart'; +import '../../../../helper/mock_helper.mocks.dart'; + +void main() { + late SetupCredentialBloc bloc; + late MockHelper mockHelper; + late MockPing mockPing; + + const errorMessage = 'testErrorMessage'; + + setUp(() { + mockHelper = MockHelper(); + mockPing = MockPing(); + bloc = SetupCredentialBloc( + helper: mockHelper, + ping: mockPing, + ); + }); + + test( + 'pastikan output dari initial state', + () async { + // assert + expect( + bloc.state, + isA(), + ); + }, + ); + + group('ping setup credential', () { + const baseUrl = 'https://example.com'; + final params = ParamsPing(baseUrl: baseUrl); + final event = PingSetupCredentialEvent(baseUrl: baseUrl); + + blocTest( + 'pastikan emit [LoadingSetupCredentialState, SuccessPingSetupCredentialState] ketika terima event ' + 'PingSetupCredentialEvent dengan proses berhasil', + build: () { + final response = GeneralResponse.fromJson( + json.decode( + fixture('general_response.json'), + ), + ); + final result = (failure: null, response: response); + when(mockPing(any)).thenAnswer((_) async => result); + return bloc; + }, + act: (SetupCredentialBloc bloc) { + return bloc.add(event); + }, + expect: () => [ + isA(), + isA(), + ], + verify: (_) { + verify(mockPing(params)); + }, + ); + + blocTest( + 'pastikan emit [LoadingSetupCredentialState, FailureSetupCredentialState] ketika terima event ' + 'PingSetupCredentialEvent dengan proses gagal dari endpoint', + build: () { + final result = (failure: ServerFailure(errorMessage), response: null); + when(mockPing(any)).thenAnswer((_) async => result); + return bloc; + }, + act: (SetupCredentialBloc bloc) { + return bloc.add(event); + }, + expect: () => [ + isA(), + isA(), + ], + verify: (_) { + verify(mockPing(params)); + }, + ); + + blocTest( + 'pastikan emit [LoadingSetupCredentialState, FailureSetupCredentialState] ketika terima event ' + 'PingSetupCredentialEvent dengan kondisi internet tidak terhubung ketika hit endpoint', + build: () { + final result = (failure: ConnectionFailure(), response: null); + when(mockPing(any)).thenAnswer((_) async => result); + return bloc; + }, + act: (SetupCredentialBloc bloc) { + return bloc.add(event); + }, + expect: () => [ + isA(), + isA(), + ], + verify: (_) { + verify(mockPing(params)); + }, + ); + + blocTest( + 'pastikan emit [LoadingSetupCredentialState, FailureSetupCredentialState] ketika terima event ' + 'PingSetupCredentialEvent dengan proses gagal parsing respon JSON endpoint', + build: () { + final result = (failure: ParsingFailure(errorMessage), response: null); + when(mockPing(any)).thenAnswer((_) async => result); + return bloc; + }, + act: (SetupCredentialBloc bloc) { + return bloc.add(event); + }, + expect: () => [ + isA(), + isA(), + ], + verify: (_) { + verify(mockPing(params)); + }, + ); + }); +} diff --git a/test/feature/presentation/bloc/setup_credential/setup_credential_event_test.dart b/test/feature/presentation/bloc/setup_credential/setup_credential_event_test.dart new file mode 100644 index 0000000..576022b --- /dev/null +++ b/test/feature/presentation/bloc/setup_credential/setup_credential_event_test.dart @@ -0,0 +1,19 @@ +import 'package:dipantau_desktop_client/feature/presentation/bloc/setup_credential/setup_credential_bloc.dart'; +import 'package:flutter_test/flutter_test.dart'; + +void main() { + group('PingSetupCredentialEvent', () { + final event = PingSetupCredentialEvent(baseUrl: 'https://example.com'); + + test( + 'pastikan output dari fungsi toString', + () async { + // assert + expect( + event.toString(), + 'PingSetupCredentialEvent{baseUrl: ${event.baseUrl}}', + ); + }, + ); + }); +} diff --git a/test/feature/presentation/bloc/setup_credential/setup_credential_state_test.dart b/test/feature/presentation/bloc/setup_credential/setup_credential_state_test.dart new file mode 100644 index 0000000..f20f2ef --- /dev/null +++ b/test/feature/presentation/bloc/setup_credential/setup_credential_state_test.dart @@ -0,0 +1,34 @@ +import 'package:dipantau_desktop_client/feature/presentation/bloc/setup_credential/setup_credential_bloc.dart'; +import 'package:flutter_test/flutter_test.dart'; + +void main() { + group('FailureSetupCredentialState', () { + final state = FailureSetupCredentialState(errorMessage: 'errorMessage'); + + test( + 'pastikan output dari fungsi toString', + () async { + // assert + expect( + state.toString(), + 'FailureSetupCredentialState{errorMessage: ${state.errorMessage}}', + ); + }, + ); + }); + + group('SuccessPingSetupCredentialState', () { + final state = SuccessPingSetupCredentialState(baseUrl: 'https://example.com'); + + test( + 'pastikan output dari fungsi toString', + () async { + // assert + expect( + state.toString(), + 'SuccessPingSetupCredentialState{baseUrl: ${state.baseUrl}}', + ); + }, + ); + }); +} diff --git a/test/fixture/all_user_setting_response.json b/test/fixture/all_user_setting_response.json new file mode 100644 index 0000000..171a64e --- /dev/null +++ b/test/fixture/all_user_setting_response.json @@ -0,0 +1,11 @@ +{ + "data": [ + { + "id": 1, + "is_enable_blur_screenshot": false, + "user_id": 1, + "name": "name" + } + ], + "is_override_blur_screenshot": false +} \ No newline at end of file diff --git a/test/fixture/kv_setting_body.json b/test/fixture/kv_setting_body.json index 9291b3d..7da35ac 100644 --- a/test/fixture/kv_setting_body.json +++ b/test/fixture/kv_setting_body.json @@ -1,3 +1,4 @@ { - "discord_channel_id": "testDiscordChannelId" + "discord_channel_id": "testDiscordChannelId", + "sign_up_method": "testSignUpMethod" } \ No newline at end of file diff --git a/test/fixture/kv_setting_response.json b/test/fixture/kv_setting_response.json index 9291b3d..7da35ac 100644 --- a/test/fixture/kv_setting_response.json +++ b/test/fixture/kv_setting_response.json @@ -1,3 +1,4 @@ { - "discord_channel_id": "testDiscordChannelId" + "discord_channel_id": "testDiscordChannelId", + "sign_up_method": "testSignUpMethod" } \ No newline at end of file diff --git a/test/fixture/manual_create_track_body.json b/test/fixture/manual_create_track_body.json new file mode 100644 index 0000000..b94143b --- /dev/null +++ b/test/fixture/manual_create_track_body.json @@ -0,0 +1,7 @@ +{ + "task_id": 1, + "start_date": "testStartDate", + "finish_date": "testFinishDate", + "duration": 0, + "note": "testNote" +} \ No newline at end of file diff --git a/test/fixture/project_task_response.json b/test/fixture/project_task_response.json new file mode 100644 index 0000000..72885f1 --- /dev/null +++ b/test/fixture/project_task_response.json @@ -0,0 +1,14 @@ +{ + "data": [ + { + "project_id": 1, + "project_name": "projectName", + "tasks": [ + { + "id": 1, + "name": "name" + } + ] + } + ] +} \ No newline at end of file diff --git a/test/fixture/track_user_response.json b/test/fixture/track_user_response.json index ca03abb..e09fb65 100644 --- a/test/fixture/track_user_response.json +++ b/test/fixture/track_user_response.json @@ -16,9 +16,12 @@ { "id": 1, "url": "testUrl", - "size": 1 + "size": 1, + "url_blur": "testUrlBlur", + "size_blur": 1 } - ] + ], + "note": "testNote" } ] } \ No newline at end of file diff --git a/test/fixture/user_setting_body.json b/test/fixture/user_setting_body.json new file mode 100644 index 0000000..7354556 --- /dev/null +++ b/test/fixture/user_setting_body.json @@ -0,0 +1,10 @@ +{ + "data": [ + { + "id": 1, + "is_enable_blur_screenshot": false, + "user_id": 1 + } + ], + "is_override_blur_screenshot": false +} \ No newline at end of file diff --git a/test/fixture/user_setting_response.json b/test/fixture/user_setting_response.json new file mode 100644 index 0000000..0da44df --- /dev/null +++ b/test/fixture/user_setting_response.json @@ -0,0 +1,7 @@ +{ + "id": 1, + "is_enable_blur_screenshot": false, + "user_id": 1, + "name": "name", + "is_override_blur_screenshot": false +} \ No newline at end of file diff --git a/test/helper/mock_helper.dart b/test/helper/mock_helper.dart index 2c3fd55..1913f43 100644 --- a/test/helper/mock_helper.dart +++ b/test/helper/mock_helper.dart @@ -4,33 +4,41 @@ import 'package:dipantau_desktop_client/core/network/network_info.dart'; import 'package:dipantau_desktop_client/core/util/helper.dart'; import 'package:dipantau_desktop_client/core/util/shared_preferences_manager.dart'; import 'package:dipantau_desktop_client/feature/data/datasource/auth/auth_remote_data_source.dart'; +import 'package:dipantau_desktop_client/feature/data/datasource/general/general_remote_data_source.dart'; import 'package:dipantau_desktop_client/feature/data/datasource/project/project_remote_data_source.dart'; import 'package:dipantau_desktop_client/feature/data/datasource/setting/setting_remote_data_source.dart'; import 'package:dipantau_desktop_client/feature/data/datasource/track/track_remote_data_source.dart'; import 'package:dipantau_desktop_client/feature/data/datasource/user/user_remote_data_source.dart'; import 'package:dipantau_desktop_client/feature/domain/repository/auth/auth_repository.dart'; +import 'package:dipantau_desktop_client/feature/domain/repository/general/general_repository.dart'; import 'package:dipantau_desktop_client/feature/domain/repository/project/project_repository.dart'; import 'package:dipantau_desktop_client/feature/domain/repository/setting/setting_repository.dart'; import 'package:dipantau_desktop_client/feature/domain/repository/track/track_repository.dart'; import 'package:dipantau_desktop_client/feature/domain/repository/user/user_repository.dart'; import 'package:dipantau_desktop_client/feature/domain/usecase/bulk_create_track_data/bulk_create_track_data.dart'; import 'package:dipantau_desktop_client/feature/domain/usecase/bulk_create_track_image/bulk_create_track_image.dart'; +import 'package:dipantau_desktop_client/feature/domain/usecase/create_manual_track/create_manual_track.dart'; import 'package:dipantau_desktop_client/feature/domain/usecase/create_track/create_track.dart'; import 'package:dipantau_desktop_client/feature/domain/usecase/delete_track_user/delete_track_user.dart'; import 'package:dipantau_desktop_client/feature/domain/usecase/forgot_password/forgot_password.dart'; import 'package:dipantau_desktop_client/feature/domain/usecase/get_all_member/get_all_member.dart'; +import 'package:dipantau_desktop_client/feature/domain/usecase/get_all_user_setting/get_all_user_setting.dart'; import 'package:dipantau_desktop_client/feature/domain/usecase/get_kv_setting/get_kv_setting.dart'; import 'package:dipantau_desktop_client/feature/domain/usecase/get_profile/get_profile.dart'; import 'package:dipantau_desktop_client/feature/domain/usecase/get_project/get_project.dart'; +import 'package:dipantau_desktop_client/feature/domain/usecase/get_project_task_by_user_id/get_project_task_by_user_id.dart'; import 'package:dipantau_desktop_client/feature/domain/usecase/get_track_user/get_track_user.dart'; import 'package:dipantau_desktop_client/feature/domain/usecase/get_track_user_lite/get_track_user_lite.dart'; +import 'package:dipantau_desktop_client/feature/domain/usecase/get_user_setting/get_user_setting.dart'; import 'package:dipantau_desktop_client/feature/domain/usecase/login/login.dart'; +import 'package:dipantau_desktop_client/feature/domain/usecase/ping/ping.dart'; import 'package:dipantau_desktop_client/feature/domain/usecase/refresh_token/refresh_token.dart'; import 'package:dipantau_desktop_client/feature/domain/usecase/reset_password/reset_password.dart'; import 'package:dipantau_desktop_client/feature/domain/usecase/send_app_version/send_app_version.dart'; import 'package:dipantau_desktop_client/feature/domain/usecase/set_kv_setting/set_kv_setting.dart'; import 'package:dipantau_desktop_client/feature/domain/usecase/sign_up/sign_up.dart'; import 'package:dipantau_desktop_client/feature/domain/usecase/update_user/update_user.dart'; +import 'package:dipantau_desktop_client/feature/domain/usecase/update_user_setting/update_user_setting.dart'; import 'package:dipantau_desktop_client/feature/domain/usecase/verify_forgot_password/verify_forgot_password.dart'; import 'package:mockito/annotations.dart'; import 'package:shared_preferences/shared_preferences.dart'; @@ -48,11 +56,13 @@ import 'package:shared_preferences/shared_preferences.dart'; MockSpec(), MockSpec(), MockSpec(), + MockSpec(), MockSpec(), MockSpec(), MockSpec(), MockSpec(), MockSpec(), + MockSpec(), MockSpec(), MockSpec(), MockSpec(), @@ -72,5 +82,11 @@ import 'package:shared_preferences/shared_preferences.dart'; MockSpec(), MockSpec(), MockSpec(), + MockSpec(), + MockSpec(), + MockSpec(), + MockSpec(), + MockSpec(), + MockSpec(), ]) void main() {} diff --git a/test/util/helper_test.dart b/test/util/helper_test.dart index ec5779d..06f184c 100644 --- a/test/util/helper_test.dart +++ b/test/util/helper_test.dart @@ -170,4 +170,25 @@ void main() { expect(unknownFailure, constantErrorMessage.failureUnknown); }, ); + + test( + 'pastikan function removeTrailingSlash bisa menghapus karakter "/" diakhir dari sebuah string.', + () async { + // arrange + const input = 'https://example.com/'; + const input2 = 'https://example.com'; + const input3 = 'https://example.com////'; + const output = 'https://example.com'; + + // act + final actual1 = helper.removeTrailingSlash(input); + final actual2 = helper.removeTrailingSlash(input2); + final actual3 = helper.removeTrailingSlash(input3); + + // assert + expect(actual1, output); + expect(actual2, output); + expect(actual3, output); + }, + ); } diff --git a/windows/flutter/generated_plugin_registrant.cc b/windows/flutter/generated_plugin_registrant.cc index 6201f9f..70160cb 100644 --- a/windows/flutter/generated_plugin_registrant.cc +++ b/windows/flutter/generated_plugin_registrant.cc @@ -6,15 +6,15 @@ #include "generated_plugin_registrant.h" -#include +#include #include #include #include #include void RegisterPlugins(flutter::PluginRegistry* registry) { - AutoUpdaterPluginRegisterWithRegistrar( - registry->GetRegistrarForPlugin("AutoUpdaterPlugin")); + AutoUpdaterWindowsPluginCApiRegisterWithRegistrar( + registry->GetRegistrarForPlugin("AutoUpdaterWindowsPluginCApi")); ConnectivityPlusWindowsPluginRegisterWithRegistrar( registry->GetRegistrarForPlugin("ConnectivityPlusWindowsPlugin")); ScreenRetrieverPluginRegisterWithRegistrar( diff --git a/windows/flutter/generated_plugins.cmake b/windows/flutter/generated_plugins.cmake index eb2f99c..2d258cd 100644 --- a/windows/flutter/generated_plugins.cmake +++ b/windows/flutter/generated_plugins.cmake @@ -3,7 +3,7 @@ # list(APPEND FLUTTER_PLUGIN_LIST - auto_updater + auto_updater_windows connectivity_plus screen_retriever tray_manager