diff --git a/lib/features/base/widget/border_button_field.dart b/lib/features/base/widget/border_button_field.dart index d350df417..1485265e1 100644 --- a/lib/features/base/widget/border_button_field.dart +++ b/lib/features/base/widget/border_button_field.dart @@ -17,6 +17,7 @@ class BorderButtonField extends StatelessWidget { final TextStyle? textStyle; final MouseCursor? mouseCursor; final String? hintText; + final bool isEmpty; const BorderButtonField({ super.key, @@ -26,6 +27,7 @@ class BorderButtonField extends StatelessWidget { this.textStyle, this.mouseCursor, this.hintText, + this.isEmpty = false, }); @override @@ -40,7 +42,7 @@ class BorderButtonField extends StatelessWidget { decoration: BoxDecoration( borderRadius: BorderRadius.circular(10), border: Border.all( - color: AppColor.colorInputBorderCreateMailbox, + color: _getBorderColor(), width: 1), color: Colors.white), padding: const EdgeInsets.only(left: 12, right: 10), @@ -80,4 +82,11 @@ class BorderButtonField extends StatelessWidget { } return hintText ?? ''; } + + Color _getBorderColor() { + if (!isEmpty) { + return AppColor.colorInputBorderCreateMailbox; + } + return AppColor.colorInputBorderErrorVerifyName; + } } \ No newline at end of file diff --git a/lib/features/mailbox_creator/presentation/extensions/validator_failure_extension.dart b/lib/features/mailbox_creator/presentation/extensions/validator_failure_extension.dart index efdd69196..ace26fe65 100644 --- a/lib/features/mailbox_creator/presentation/extensions/validator_failure_extension.dart +++ b/lib/features/mailbox_creator/presentation/extensions/validator_failure_extension.dart @@ -42,4 +42,12 @@ extension ValicatorFailureExtension on VerifyNameFailure { return ''; } } + + String getMessageVacation(BuildContext context) { + if (exception is EmptyNameException) { + return AppLocalizations.of(context).this_field_cannot_be_blank; + } else { + return ''; + } + } } \ No newline at end of file diff --git a/lib/features/manage_account/presentation/configuration/vacation/vacation_bindings.dart b/lib/features/manage_account/presentation/configuration/vacation/vacation_bindings.dart index 8f3adbfeb..205a61aca 100644 --- a/lib/features/manage_account/presentation/configuration/vacation/vacation_bindings.dart +++ b/lib/features/manage_account/presentation/configuration/vacation/vacation_bindings.dart @@ -1,14 +1,19 @@ import 'package:get/get.dart'; import 'package:tmail_ui_user/features/base/base_bindings.dart'; +import 'package:tmail_ui_user/features/mailbox_creator/domain/usecases/verify_name_interactor.dart'; import 'package:tmail_ui_user/features/manage_account/domain/repository/manage_account_repository.dart'; import 'package:tmail_ui_user/features/manage_account/domain/usecases/get_all_vacation_interactor.dart'; +import 'package:tmail_ui_user/features/manage_account/domain/usecases/update_vacation_interactor.dart'; import 'package:tmail_ui_user/features/manage_account/presentation/configuration/vacation/vacation_controller.dart'; class VacationBindings extends BaseBindings { @override void bindingsController() { - Get.lazyPut(() => VacationController(Get.find())); + Get.lazyPut(() => VacationController( + Get.find(), + Get.find(), + Get.find())); } @override @@ -22,6 +27,8 @@ class VacationBindings extends BaseBindings { @override void bindingsInteractor() { Get.lazyPut(() => GetAllVacationInteractor(Get.find())); + Get.lazyPut(() => UpdateVacationInteractor(Get.find())); + Get.lazyPut(() => VerifyNameInteractor()); } @override diff --git a/lib/features/manage_account/presentation/configuration/vacation/vacation_controller.dart b/lib/features/manage_account/presentation/configuration/vacation/vacation_controller.dart index 20a693075..14c6e2343 100644 --- a/lib/features/manage_account/presentation/configuration/vacation/vacation_controller.dart +++ b/lib/features/manage_account/presentation/configuration/vacation/vacation_controller.dart @@ -1,28 +1,44 @@ -import 'package:core/utils/app_logger.dart'; +import 'package:core/core.dart'; import 'package:flutter/material.dart'; import 'package:get/get.dart'; import 'package:jmap_dart_client/jmap/mail/vacation/vacation_response.dart'; import 'package:tmail_ui_user/features/base/base_controller.dart'; +import 'package:tmail_ui_user/features/mailbox_creator/domain/model/verification/empty_name_validator.dart'; +import 'package:tmail_ui_user/features/mailbox_creator/domain/state/verify_name_view_state.dart'; +import 'package:tmail_ui_user/features/mailbox_creator/domain/usecases/verify_name_interactor.dart'; +import 'package:tmail_ui_user/features/mailbox_creator/presentation/extensions/validator_failure_extension.dart'; import 'package:tmail_ui_user/features/manage_account/domain/state/get_all_vacation_state.dart'; +import 'package:tmail_ui_user/features/manage_account/domain/state/update_vacation_state.dart'; import 'package:tmail_ui_user/features/manage_account/domain/usecases/get_all_vacation_interactor.dart'; +import 'package:tmail_ui_user/features/manage_account/domain/usecases/update_vacation_interactor.dart'; +import 'package:tmail_ui_user/features/manage_account/presentation/extensions/vacation_response_extension.dart'; import 'package:tmail_ui_user/features/manage_account/presentation/manage_account_dashboard_controller.dart'; import 'package:tmail_ui_user/features/manage_account/presentation/model/vacation/date_type.dart'; import 'package:tmail_ui_user/features/manage_account/presentation/model/vacation/vacation_presentation.dart'; import 'package:tmail_ui_user/features/manage_account/presentation/model/vacation/vacation_responder_status.dart'; +import 'package:tmail_ui_user/main/localizations/app_localizations.dart'; +import 'package:tmail_ui_user/main/routes/route_navigation.dart'; class VacationController extends BaseController { final _accountDashBoardController = Get.find(); + final _appToast = Get.find(); + final _imagePaths = Get.find(); final GetAllVacationInteractor _getAllVacationInteractor; + final UpdateVacationInteractor _updateVacationInteractor; + final VerifyNameInteractor _verifyNameInteractor; final vacationPresentation = VacationPresentation.initialize().obs; + final errorMessageBody = Rxn(); final TextEditingController messageBodyEditorController = TextEditingController(); - VacationResponse? currentVacation; - - VacationController(this._getAllVacationInteractor); + VacationController( + this._getAllVacationInteractor, + this._updateVacationInteractor, + this._verifyNameInteractor + ); @override void onReady() { @@ -37,6 +53,8 @@ class VacationController extends BaseController { (success) { if (success is GetAllVacationSuccess) { _handleGetAllVacationSuccess(success); + } else if (success is UpdateVacationSuccess) { + _handleUpdateVacationSuccess(success); } } ); @@ -53,41 +71,17 @@ class VacationController extends BaseController { } void _handleGetAllVacationSuccess(GetAllVacationSuccess success) { - if (success.listVacationResponse?.isNotEmpty == true) { - currentVacation = success.listVacationResponse!.first; + if (success.listVacationResponse.isNotEmpty) { + final currentVacation = success.listVacationResponse.first; log('VacationController::_handleGetAllVacationSuccess(): $currentVacation'); - final vacationStatus = currentVacation?.isEnabled; - final startDate = currentVacation?.fromDate?.value; - final endDate = currentVacation?.toDate?.value; - final messageBody = currentVacation?.htmlBody ?? currentVacation?.textBody; - final startTime = startDate != null - ? TimeOfDay.fromDateTime(startDate) - : null; - final endTime = endDate != null - ? TimeOfDay.fromDateTime(endDate) - : null; - final vacationStopEnabled = endDate != null; - - if (messageBody != null) { - messageBodyEditorController.text = messageBody; - } - - updateVacationPresentation( - newStatus: vacationStatus == true - ? VacationResponderStatus.activated - : VacationResponderStatus.deactivated, - startDate: startDate, - startTime: startTime, - endDate: endDate, - endTime: endTime, - vacationStopEnabled: vacationStopEnabled, - ); + final newVacationPresentation = currentVacation.toVacationPresentation(); + vacationPresentation.value = newVacationPresentation; + messageBodyEditorController.text = newVacationPresentation.messageBody ?? ''; } } - bool get isVacationDeactivated => - vacationPresentation.value.status == VacationResponderStatus.deactivated; + bool get isVacationDeactivated => !vacationPresentation.value.isEnabled; bool get isVacationStopEnabled => vacationPresentation.value.vacationStopEnabled; @@ -100,6 +94,7 @@ class VacationController extends BaseController { DateTime? endDate, TimeOfDay? endTime, bool? vacationStopEnabled, + String? messageBody }) { final currentVacation = vacationPresentation.value; final newVacation = currentVacation.copyWidth( @@ -108,7 +103,9 @@ class VacationController extends BaseController { startTime: startTime, endDate: endDate, endTime: endTime, - vacationStopEnabled: vacationStopEnabled); + vacationStopEnabled: vacationStopEnabled, + messageBody: messageBody); + log('VacationController::updateVacationPresentation():newVacation: $newVacation'); vacationPresentation.value = newVacation; } @@ -137,6 +134,11 @@ class VacationController extends BaseController { final timePicked = await showTimePicker( context: context, initialTime: currentTime ?? TimeOfDay.now(), + builder: (context, child) { + return MediaQuery( + data: const MediaQueryData(alwaysUse24HourFormat: true), + child: child!); + } ); if (timePicked == null) { @@ -150,8 +152,88 @@ class VacationController extends BaseController { } } - void saveVacation(BuildContext context) { + String? _getErrorStringByInputValue(BuildContext context, String? inputValue) { + return _verifyNameInteractor.execute(inputValue, [EmptyNameValidator()]).fold( + (failure) { + if (failure is VerifyNameFailure) { + return failure.getMessageVacation(context); + } else { + return null; + } + }, + (success) => null + ); + } + void updateMessageBody(BuildContext context, String? value) { + errorMessageBody.value = _getErrorStringByInputValue(context, value); + } + + void saveVacation(BuildContext context) { + FocusScope.of(context).unfocus(); + + if (vacationPresentation.value.isEnabled) { + final fromDate = vacationPresentation.value.fromDate; + if (fromDate == null) { + _appToast.showToastWithIcon( + context, + bgColor: AppColor.toastErrorBackgroundColor, + textColor: Colors.white, + message: AppLocalizations.of(context).errorMessageWhenStartDateVacationIsEmpty); + return; + } + + final vacationStopEnabled = vacationPresentation.value.vacationStopEnabled; + final toDate = vacationPresentation.value.toDate; + if (vacationStopEnabled && toDate != null && toDate.isBefore(fromDate)) { + _appToast.showToastWithIcon( + context, + bgColor: AppColor.toastErrorBackgroundColor, + textColor: Colors.white, + message: AppLocalizations.of(context).errorMessageWhenEndDateVacationIsInValid); + return; + } + + final messageBody = messageBodyEditorController.text; + if (messageBody.isEmpty) { + _appToast.showToastWithIcon( + context, + bgColor: AppColor.toastErrorBackgroundColor, + textColor: Colors.white, + message: AppLocalizations.of(context).errorMessageWhenMessageVacationIsEmpty); + return; + } + + final newVacationPresentation = vacationPresentation.value.copyWidth(messageBody: messageBody); + log('VacationController::saveVacation(): newVacationPresentation: $newVacationPresentation'); + final newVacationResponse = newVacationPresentation.toVacationResponse(); + log('VacationController::saveVacation(): newVacationResponse: $newVacationResponse'); + _updateVacationAction(newVacationResponse); + } + } + + void _updateVacationAction(VacationResponse vacationResponse) { + final accountId = _accountDashBoardController.accountId.value; + if (accountId != null) { + consumeState(_updateVacationInteractor.execute(accountId, vacationResponse)); + } + } + + void _handleUpdateVacationSuccess(UpdateVacationSuccess success) { + if (success.listVacationResponse.isNotEmpty) { + if (currentContext != null && currentOverlayContext != null) { + _appToast.showToastWithIcon( + currentOverlayContext!, + message: AppLocalizations.of(currentContext!).vacationSettingSaved, + icon: _imagePaths.icChecked); + } + final currentVacation = success.listVacationResponse.first; + log('VacationController::_handleUpdateVacationSuccess(): $currentVacation'); + + final newVacationPresentation = currentVacation.toVacationPresentation(); + vacationPresentation.value = newVacationPresentation; + messageBodyEditorController.text = newVacationPresentation.messageBody ?? ''; + } } @override diff --git a/lib/features/manage_account/presentation/configuration/vacation/vacation_view.dart b/lib/features/manage_account/presentation/configuration/vacation/vacation_view.dart index 5af8199b6..17faefe4c 100644 --- a/lib/features/manage_account/presentation/configuration/vacation/vacation_view.dart +++ b/lib/features/manage_account/presentation/configuration/vacation/vacation_view.dart @@ -20,349 +20,144 @@ class VacationView extends GetWidget { Widget build(BuildContext context) { return Scaffold( backgroundColor: Colors.white, - body: Container( - width: double.infinity, - color: Colors.white, - padding: const EdgeInsets.symmetric(vertical: 16, horizontal: 16), - child: ResponsiveWidget( - responsiveUtils: _responsiveUtils, - desktop: SingleChildScrollView( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Row(children: [ - Container( - width: 200, - color: Colors.white, - child: Text( - AppLocalizations.of(context).vacationResponder, - style: const TextStyle( - fontSize: 15, - fontWeight: FontWeight.normal, - color: Colors.black))), - const SizedBox(width: 16), - ToggleSwitch( - minWidth: 150.0, - cornerRadius: 20.0, - activeBgColors: const [[Colors.green], [Colors.redAccent]], - activeFgColor: Colors.white, - inactiveBgColor: Colors.grey, - inactiveFgColor: Colors.white, - initialLabelIndex: 1, - totalSwitches: VacationResponderStatus.values.length, - labels: VacationResponderStatus.values - .map((status) => status.getTitle(context)) - .toList(), - radiusStyle: true, - onToggle: (index) { - log('VacationView::build():ToggleSwitch:index: $index'); - final newStatus = index == 0 - ? VacationResponderStatus.activated - : VacationResponderStatus.deactivated; - controller.updateVacationPresentation(newStatus: newStatus); - }, - ), - ]), - const SizedBox(height: 20), - Obx(() => AbsorbPointer( - absorbing: controller.isVacationDeactivated, - child: Row(children: [ + body: GestureDetector( + onTap: () => FocusScope.of(context).unfocus(), + child: Container( + width: double.infinity, + color: Colors.white, + padding: const EdgeInsets.symmetric(vertical: 16, horizontal: 16), + child: ResponsiveWidget( + responsiveUtils: _responsiveUtils, + desktop: SingleChildScrollView( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row(children: [ Container( width: 200, color: Colors.white, child: Text( - AppLocalizations.of(context).startDate, - style: TextStyle( + AppLocalizations.of(context).vacationResponder, + style: const TextStyle( fontSize: 15, fontWeight: FontWeight.normal, - color: Colors.black.withOpacity( - controller.isVacationDeactivated ? 0.5 : 1.0)))), + color: Colors.black))), const SizedBox(width: 16), - Expanded(child: Obx(() => BorderButtonField( - value: controller.vacationPresentation.value.startDate, - mouseCursor: SystemMouseCursors.text, - textStyle: TextStyle( - fontSize: 16, - fontWeight: FontWeight.normal, - color: Colors.black.withOpacity( - controller.isVacationDeactivated ? 0.5 : 1.0)), - hintText: AppLocalizations.of(context).startDate, - icon: const Icon( - Icons.date_range, - color: AppColor.colorIconTextField, - size: 20), - tapActionCallback: (value) => - controller.selectDate(context, DateType.start, value)))), - const SizedBox(width: 16), - Expanded(child: Obx(() => BorderButtonField( - value: controller.vacationPresentation.value.startTime, - mouseCursor: SystemMouseCursors.text, - textStyle: TextStyle( - fontSize: 16, - fontWeight: FontWeight.normal, - color: Colors.black.withOpacity( - controller.isVacationDeactivated ? 0.5 : 1.0)), - hintText: AppLocalizations.of(context).noStartTime, - icon: const Icon( - Icons.timer, - color: AppColor.colorIconTextField, - size: 20), - tapActionCallback: (value) => - controller.selectTime(context, DateType.start, value)))), + Obx(() => ToggleSwitch( + minWidth: 150.0, + cornerRadius: 20.0, + activeBgColors: const [[Colors.green], [Colors.redAccent]], + activeFgColor: Colors.white, + inactiveBgColor: Colors.grey, + inactiveFgColor: Colors.white, + initialLabelIndex: controller.isVacationDeactivated ? 1 : 0, + totalSwitches: VacationResponderStatus.values.length, + labels: VacationResponderStatus.values + .map((status) => status.getTitle(context)) + .toList(), + radiusStyle: true, + onToggle: (index) { + final newStatus = index == 0 + ? VacationResponderStatus.activated + : VacationResponderStatus.deactivated; + controller.updateVacationPresentation(newStatus: newStatus); + }, + )), ]), - )), - const SizedBox(height: 20), - Obx(() => AbsorbPointer( - absorbing: controller.isVacationDeactivated, - child: Container( - width: 200, - color: Colors.white, - child: CheckboxListTile( - contentPadding: EdgeInsets.zero, - activeColor: AppColor.primaryColor.withOpacity( - controller.isVacationDeactivated ? 0.5 : 1.0), - controlAffinity: ListTileControlAffinity.leading, - value: controller.vacationPresentation.value.vacationStopEnabled, - onChanged: (value) => - controller.updateVacationPresentation(vacationStopEnabled: value), - title: Text(AppLocalizations.of(context).vacationStopsAt, - style: TextStyle( - fontSize: 15, - fontWeight: FontWeight.normal, - color: Colors.black.withOpacity( - controller.isVacationDeactivated ? 0.5 : 1.0))), - ), - ), - )), - Obx(() => AbsorbPointer( - absorbing: !controller.canChangeEndDate, - child: Row(children: [ - Container( - width: 200, - color: Colors.white, - child: Text( - AppLocalizations.of(context).endDate, - style: TextStyle( - fontSize: 15, - fontWeight: FontWeight.normal, - color: Colors.black.withOpacity( - !controller.canChangeEndDate ? 0.5 : 1.0)))), - const SizedBox(width: 16), - Expanded(child: Obx(() => BorderButtonField( - value: controller.vacationPresentation.value.endDate, - mouseCursor: SystemMouseCursors.text, - textStyle: TextStyle( - fontSize: 16, - fontWeight: FontWeight.normal, - color: Colors.black.withOpacity( - !controller.canChangeEndDate ? 0.5 : 1.0)), - hintText: AppLocalizations.of(context).noEndDate, - icon: const Icon( - Icons.date_range, - color: AppColor.colorIconTextField, - size: 20), - tapActionCallback: (value) => - controller.selectDate(context, DateType.end, value)))), - const SizedBox(width: 16), - Expanded(child: Obx(() => BorderButtonField( - value: controller.vacationPresentation.value.endTime, - mouseCursor: SystemMouseCursors.text, - textStyle: TextStyle( - fontSize: 16, - fontWeight: FontWeight.normal, - color: Colors.black.withOpacity( - !controller.canChangeEndDate ? 0.5 : 1.0)), - hintText: AppLocalizations.of(context).noEndTime, - icon: const Icon( - Icons.timer, - color: AppColor.colorIconTextField, - size: 20), - tapActionCallback: (value) => - controller.selectTime(context, DateType.end, value)))), - ]) - )), - const SizedBox(height: 20), - Obx(() => AbsorbPointer( - absorbing: controller.isVacationDeactivated, - child: Row(crossAxisAlignment: CrossAxisAlignment.start, children: [ - Container( - width: 200, - color: Colors.white, - child: Text( - AppLocalizations.of(context).messageBody, + const SizedBox(height: 20), + Obx(() => AbsorbPointer( + absorbing: controller.isVacationDeactivated, + child: Row(children: [ + Container( + width: 200, + color: Colors.white, + child: Text( + AppLocalizations.of(context).startDate, style: TextStyle( fontSize: 15, fontWeight: FontWeight.normal, color: Colors.black.withOpacity( controller.isVacationDeactivated ? 0.5 : 1.0)))), - const SizedBox(width: 16), - Expanded(child: _buildMessageBodyWidget( - context, - controller.isVacationDeactivated ? 0.5 : 1.0)) - ]), - )), - const SizedBox(height: 20), - Align( - alignment: Alignment.centerRight, - child: buildTextButton( - AppLocalizations.of(context).save, - width: 128, - height: 44, - radius: 10, - onTap: () => controller.saveVacation(context)), - ) - ] - ), - ), - mobile: SingleChildScrollView( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - if (_responsiveUtils.isPortraitMobile(context)) - Column(crossAxisAlignment: CrossAxisAlignment.start, children: [ - Text( - AppLocalizations.of(context).vacationResponder, - style: const TextStyle( - fontSize: 15, - fontWeight: FontWeight.normal, - color: Colors.black)), - const SizedBox(height: 16), - ToggleSwitch( - minWidth: 150.0, - cornerRadius: 20.0, - activeBgColors: const [[Colors.green], [Colors.redAccent]], - activeFgColor: Colors.white, - inactiveBgColor: Colors.grey, - inactiveFgColor: Colors.white, - initialLabelIndex: 1, - totalSwitches: VacationResponderStatus.values.length, - labels: VacationResponderStatus.values - .map((status) => status.getTitle(context)) - .toList(), - radiusStyle: true, - onToggle: (index) { - log('VacationView::build():ToggleSwitch:index: $index'); - final newStatus = index == 0 - ? VacationResponderStatus.activated - : VacationResponderStatus.deactivated; - controller.updateVacationPresentation(newStatus: newStatus); - }, - ) - ]) - else - Row(children: [ - Expanded( - child: Text( - AppLocalizations.of(context).vacationResponder, - style: const TextStyle( - fontSize: 15, - fontWeight: FontWeight.normal, - color: Colors.black)), - ), - const SizedBox(width: 16), - ToggleSwitch( - minWidth: 150.0, - cornerRadius: 20.0, - activeBgColors: const [[Colors.green], [Colors.redAccent]], - activeFgColor: Colors.white, - inactiveBgColor: Colors.grey, - inactiveFgColor: Colors.white, - initialLabelIndex: 1, - totalSwitches: VacationResponderStatus.values.length, - labels: VacationResponderStatus.values - .map((status) => status.getTitle(context)) - .toList(), - radiusStyle: true, - onToggle: (index) { - log('VacationView::build():ToggleSwitch:index: $index'); - final newStatus = index == 0 - ? VacationResponderStatus.activated - : VacationResponderStatus.deactivated; - controller.updateVacationPresentation(newStatus: newStatus); - }, - ) - ]), - const SizedBox(height: 20), - Obx(() => AbsorbPointer( - absorbing: controller.isVacationDeactivated, - child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [ - Text( - AppLocalizations.of(context).startDate, - style: TextStyle( - fontSize: 15, - fontWeight: FontWeight.normal, - color: Colors.black.withOpacity( - controller.isVacationDeactivated ? 0.5 : 1.0))), - const SizedBox(height: 16), - Row(children: [ - Expanded(child: Obx(() => BorderButtonField( - value: controller.vacationPresentation.value.startDate, - mouseCursor: SystemMouseCursors.text, - textStyle: TextStyle( - fontSize: 16, - fontWeight: FontWeight.normal, - color: Colors.black.withOpacity( - controller.isVacationDeactivated ? 0.5 : 1.0)), - hintText: AppLocalizations.of(context).startDate, - icon: const Icon( - Icons.date_range, - color: AppColor.colorIconTextField, - size: 20), - tapActionCallback: (value) => - controller.selectDate(context, DateType.start, value)))), - const SizedBox(width: 16), - Expanded(child: Obx(() => BorderButtonField( - value: controller.vacationPresentation.value.startTime, - mouseCursor: SystemMouseCursors.text, - textStyle: TextStyle( - fontSize: 16, - fontWeight: FontWeight.normal, - color: Colors.black.withOpacity( - controller.isVacationDeactivated ? 0.5 : 1.0)), - hintText: AppLocalizations.of(context).noStartTime, - icon: const Icon( - Icons.timer, - color: AppColor.colorIconTextField, - size: 20), - tapActionCallback: (value) => - controller.selectTime(context, DateType.start, value)))) - ]), - ]), - )), - const SizedBox(height: 20), - Obx(() => AbsorbPointer( - absorbing: controller.isVacationDeactivated, - child: CheckboxListTile( - contentPadding: EdgeInsets.zero, - activeColor: AppColor.primaryColor.withOpacity( - controller.isVacationDeactivated ? 0.5 : 1.0), - controlAffinity: ListTileControlAffinity.leading, - value: controller.vacationPresentation.value.vacationStopEnabled, - onChanged: (value) => - controller.updateVacationPresentation(vacationStopEnabled: value), - title: Text(AppLocalizations.of(context).vacationStopsAt, - style: TextStyle( - fontSize: 15, - fontWeight: FontWeight.normal, - color: Colors.black.withOpacity( - controller.isVacationDeactivated ? 0.5 : 1.0))), - ), - )), - Obx(() => AbsorbPointer( - absorbing: !controller.canChangeEndDate, - child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [ - Text( - AppLocalizations.of(context).endDate, - style: TextStyle( - fontSize: 15, - fontWeight: FontWeight.normal, - color: Colors.black.withOpacity( - !controller.canChangeEndDate ? 0.5 : 1.0))), - const SizedBox(height: 16), - Row(children: [ + const SizedBox(width: 16), + Expanded(child: Obx(() => BorderButtonField( + value: controller.vacationPresentation.value.startDate, + mouseCursor: SystemMouseCursors.text, + isEmpty: !controller.isVacationDeactivated && + controller.vacationPresentation.value.startDateIsNull, + textStyle: TextStyle( + fontSize: 16, + fontWeight: FontWeight.normal, + color: Colors.black.withOpacity( + controller.isVacationDeactivated ? 0.5 : 1.0)), + hintText: AppLocalizations.of(context).startDate, + icon: const Icon( + Icons.date_range, + color: AppColor.colorIconTextField, + size: 20), + tapActionCallback: (value) => + controller.selectDate(context, DateType.start, value)))), + const SizedBox(width: 16), + Expanded(child: Obx(() => BorderButtonField( + value: controller.vacationPresentation.value.startTime, + mouseCursor: SystemMouseCursors.text, + isEmpty: !controller.isVacationDeactivated && + controller.vacationPresentation.value.starTimeIsNull, + textStyle: TextStyle( + fontSize: 16, + fontWeight: FontWeight.normal, + color: Colors.black.withOpacity( + controller.isVacationDeactivated ? 0.5 : 1.0)), + hintText: AppLocalizations.of(context).noStartTime, + icon: const Icon( + Icons.timer, + color: AppColor.colorIconTextField, + size: 20), + tapActionCallback: (value) => + controller.selectTime(context, DateType.start, value)))), + ]), + )), + const SizedBox(height: 20), + Obx(() => AbsorbPointer( + absorbing: controller.isVacationDeactivated, + child: Container( + width: 200, + color: Colors.white, + child: CheckboxListTile( + contentPadding: EdgeInsets.zero, + activeColor: AppColor.primaryColor.withOpacity( + controller.isVacationDeactivated ? 0.5 : 1.0), + controlAffinity: ListTileControlAffinity.leading, + value: controller.vacationPresentation.value.vacationStopEnabled, + onChanged: (value) => + controller.updateVacationPresentation(vacationStopEnabled: value), + title: Text(AppLocalizations.of(context).vacationStopsAt, + style: TextStyle( + fontSize: 15, + fontWeight: FontWeight.normal, + color: Colors.black.withOpacity( + controller.isVacationDeactivated ? 0.5 : 1.0))), + ), + ), + )), + Obx(() => AbsorbPointer( + absorbing: !controller.canChangeEndDate, + child: Row(children: [ + Container( + width: 200, + color: Colors.white, + child: Text( + AppLocalizations.of(context).endDate, + style: TextStyle( + fontSize: 15, + fontWeight: FontWeight.normal, + color: Colors.black.withOpacity( + !controller.canChangeEndDate ? 0.5 : 1.0)))), + const SizedBox(width: 16), Expanded(child: Obx(() => BorderButtonField( value: controller.vacationPresentation.value.endDate, mouseCursor: SystemMouseCursors.text, + isEmpty: controller.canChangeEndDate && + controller.vacationPresentation.value.endDateIsNull, textStyle: TextStyle( fontSize: 16, fontWeight: FontWeight.normal, @@ -379,6 +174,8 @@ class VacationView extends GetWidget { Expanded(child: Obx(() => BorderButtonField( value: controller.vacationPresentation.value.endTime, mouseCursor: SystemMouseCursors.text, + isEmpty: controller.canChangeEndDate && + controller.vacationPresentation.value.endTimeIsNull, textStyle: TextStyle( fontSize: 16, fontWeight: FontWeight.normal, @@ -390,38 +187,249 @@ class VacationView extends GetWidget { color: AppColor.colorIconTextField, size: 20), tapActionCallback: (value) => - controller.selectTime(context, DateType.end, value)))) + controller.selectTime(context, DateType.end, value)))), + ]) + )), + const SizedBox(height: 20), + Obx(() => AbsorbPointer( + absorbing: controller.isVacationDeactivated, + child: Row(crossAxisAlignment: CrossAxisAlignment.start, children: [ + Container( + width: 200, + color: Colors.white, + child: Text( + AppLocalizations.of(context).messageBody, + style: TextStyle( + fontSize: 15, + fontWeight: FontWeight.normal, + color: Colors.black.withOpacity( + controller.isVacationDeactivated ? 0.5 : 1.0)))), + const SizedBox(width: 16), + Expanded(child: _buildMessageBodyWidget( + context, + controller.isVacationDeactivated ? 0.5 : 1.0)) ]), + )), + const SizedBox(height: 20), + Align( + alignment: Alignment.centerRight, + child: buildTextButton( + AppLocalizations.of(context).save, + width: 128, + height: 44, + radius: 10, + onTap: () => controller.saveVacation(context)), + ) + ] + ), + ), + mobile: SingleChildScrollView( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + if (_responsiveUtils.isPortraitMobile(context)) + Column(crossAxisAlignment: CrossAxisAlignment.start, children: [ + Text( + AppLocalizations.of(context).vacationResponder, + style: const TextStyle( + fontSize: 15, + fontWeight: FontWeight.normal, + color: Colors.black)), + const SizedBox(height: 16), + Obx(() => ToggleSwitch( + minWidth: 150.0, + cornerRadius: 20.0, + activeBgColors: const [[Colors.green], [Colors.redAccent]], + activeFgColor: Colors.white, + inactiveBgColor: Colors.grey, + inactiveFgColor: Colors.white, + initialLabelIndex: controller.isVacationDeactivated ? 1 : 0, + totalSwitches: VacationResponderStatus.values.length, + labels: VacationResponderStatus.values + .map((status) => status.getTitle(context)) + .toList(), + radiusStyle: true, + onToggle: (index) { + final newStatus = index == 0 + ? VacationResponderStatus.activated + : VacationResponderStatus.deactivated; + controller.updateVacationPresentation(newStatus: newStatus); + }, + )) ]) - )), - const SizedBox(height: 20), - Obx(() => AbsorbPointer( - absorbing: controller.isVacationDeactivated, - child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [ - Text( - AppLocalizations.of(context).messageBody, - style: TextStyle( - fontSize: 15, - fontWeight: FontWeight.normal, - color: Colors.black.withOpacity( - controller.isVacationDeactivated ? 0.5 : 1.0))), - const SizedBox(height: 16), - _buildMessageBodyWidget( - context, - controller.isVacationDeactivated ? 0.5 : 1.0) - ]), - )), - const SizedBox(height: 20), - Align( - alignment: Alignment.centerRight, - child: buildTextButton( - AppLocalizations.of(context).save, - width: 128, - height: 44, - radius: 10, - onTap: () => controller.saveVacation(context)), - ) - ] + else + Row(children: [ + Expanded( + child: Text( + AppLocalizations.of(context).vacationResponder, + style: const TextStyle( + fontSize: 15, + fontWeight: FontWeight.normal, + color: Colors.black)), + ), + const SizedBox(width: 16), + Obx(() => ToggleSwitch( + minWidth: 150.0, + cornerRadius: 20.0, + activeBgColors: const [[Colors.green], [Colors.redAccent]], + activeFgColor: Colors.white, + inactiveBgColor: Colors.grey, + inactiveFgColor: Colors.white, + initialLabelIndex: controller.isVacationDeactivated ? 1 : 0, + totalSwitches: VacationResponderStatus.values.length, + labels: VacationResponderStatus.values + .map((status) => status.getTitle(context)) + .toList(), + radiusStyle: true, + onToggle: (index) { + final newStatus = index == 0 + ? VacationResponderStatus.activated + : VacationResponderStatus.deactivated; + controller.updateVacationPresentation(newStatus: newStatus); + }, + )) + ]), + const SizedBox(height: 20), + Obx(() => AbsorbPointer( + absorbing: controller.isVacationDeactivated, + child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [ + Text( + AppLocalizations.of(context).startDate, + style: TextStyle( + fontSize: 15, + fontWeight: FontWeight.normal, + color: Colors.black.withOpacity( + controller.isVacationDeactivated ? 0.5 : 1.0))), + const SizedBox(height: 16), + Row(children: [ + Expanded(child: Obx(() => BorderButtonField( + value: controller.vacationPresentation.value.startDate, + mouseCursor: SystemMouseCursors.text, + textStyle: TextStyle( + fontSize: 16, + fontWeight: FontWeight.normal, + color: Colors.black.withOpacity( + controller.isVacationDeactivated ? 0.5 : 1.0)), + hintText: AppLocalizations.of(context).startDate, + icon: const Icon( + Icons.date_range, + color: AppColor.colorIconTextField, + size: 20), + tapActionCallback: (value) => + controller.selectDate(context, DateType.start, value)))), + const SizedBox(width: 16), + Expanded(child: Obx(() => BorderButtonField( + value: controller.vacationPresentation.value.startTime, + mouseCursor: SystemMouseCursors.text, + textStyle: TextStyle( + fontSize: 16, + fontWeight: FontWeight.normal, + color: Colors.black.withOpacity( + controller.isVacationDeactivated ? 0.5 : 1.0)), + hintText: AppLocalizations.of(context).noStartTime, + icon: const Icon( + Icons.timer, + color: AppColor.colorIconTextField, + size: 20), + tapActionCallback: (value) => + controller.selectTime(context, DateType.start, value)))) + ]), + ]), + )), + const SizedBox(height: 20), + Obx(() => AbsorbPointer( + absorbing: controller.isVacationDeactivated, + child: CheckboxListTile( + contentPadding: EdgeInsets.zero, + activeColor: AppColor.primaryColor.withOpacity( + controller.isVacationDeactivated ? 0.5 : 1.0), + controlAffinity: ListTileControlAffinity.leading, + value: controller.vacationPresentation.value.vacationStopEnabled, + onChanged: (value) => + controller.updateVacationPresentation(vacationStopEnabled: value), + title: Text(AppLocalizations.of(context).vacationStopsAt, + style: TextStyle( + fontSize: 15, + fontWeight: FontWeight.normal, + color: Colors.black.withOpacity( + controller.isVacationDeactivated ? 0.5 : 1.0))), + ), + )), + Obx(() => AbsorbPointer( + absorbing: !controller.canChangeEndDate, + child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [ + Text( + AppLocalizations.of(context).endDate, + style: TextStyle( + fontSize: 15, + fontWeight: FontWeight.normal, + color: Colors.black.withOpacity( + !controller.canChangeEndDate ? 0.5 : 1.0))), + const SizedBox(height: 16), + Row(children: [ + Expanded(child: Obx(() => BorderButtonField( + value: controller.vacationPresentation.value.endDate, + mouseCursor: SystemMouseCursors.text, + textStyle: TextStyle( + fontSize: 16, + fontWeight: FontWeight.normal, + color: Colors.black.withOpacity( + !controller.canChangeEndDate ? 0.5 : 1.0)), + hintText: AppLocalizations.of(context).noEndDate, + icon: const Icon( + Icons.date_range, + color: AppColor.colorIconTextField, + size: 20), + tapActionCallback: (value) => + controller.selectDate(context, DateType.end, value)))), + const SizedBox(width: 16), + Expanded(child: Obx(() => BorderButtonField( + value: controller.vacationPresentation.value.endTime, + mouseCursor: SystemMouseCursors.text, + textStyle: TextStyle( + fontSize: 16, + fontWeight: FontWeight.normal, + color: Colors.black.withOpacity( + !controller.canChangeEndDate ? 0.5 : 1.0)), + hintText: AppLocalizations.of(context).noEndTime, + icon: const Icon( + Icons.timer, + color: AppColor.colorIconTextField, + size: 20), + tapActionCallback: (value) => + controller.selectTime(context, DateType.end, value)))) + ]), + ]) + )), + const SizedBox(height: 20), + Obx(() => AbsorbPointer( + absorbing: controller.isVacationDeactivated, + child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [ + Text( + AppLocalizations.of(context).messageBody, + style: TextStyle( + fontSize: 15, + fontWeight: FontWeight.normal, + color: Colors.black.withOpacity( + controller.isVacationDeactivated ? 0.5 : 1.0))), + const SizedBox(height: 16), + _buildMessageBodyWidget( + context, + controller.isVacationDeactivated ? 0.5 : 1.0) + ]), + )), + const SizedBox(height: 20), + Align( + alignment: Alignment.centerRight, + child: buildTextButton( + AppLocalizations.of(context).save, + width: 128, + height: 44, + radius: 10, + onTap: () => controller.saveVacation(context)), + ) + ] + ), ), ), ), @@ -430,25 +438,33 @@ class VacationView extends GetWidget { } Widget _buildMessageBodyWidget(BuildContext context, double opacity) { - return (TextFieldBuilder() - ..key(const Key('message_body_editor')) - ..cursorColor(Colors.black) - ..addController(controller.messageBodyEditorController) - ..textStyle(TextStyle( - fontWeight: FontWeight.normal, - color: Colors.black.withOpacity(opacity), - fontSize: 16)) - ..textDecoration((VacationInputDecorationBuilder() - ..setContentPadding(const EdgeInsets.symmetric( - vertical: BuildUtils.isWeb ? 16 : 12, - horizontal: 12)) - ..setHintStyle(TextStyle( - color: AppColor.colorHintInputCreateMailbox.withOpacity(opacity), + return Obx(() { + return (TextFieldBuilder() + ..key(const Key('message_body_editor')) + ..cursorColor(Colors.black) + ..addController(controller.messageBodyEditorController) + ..textStyle(TextStyle( + fontWeight: FontWeight.normal, + color: Colors.black.withOpacity(opacity), fontSize: 16)) - ..setHintText(AppLocalizations.of(context).hintMessageBodyVacation)) - .build()) - ..minLines(8) - ..maxLines(null)) - .build(); + ..textDecoration((VacationInputDecorationBuilder() + ..setContentPadding(const EdgeInsets.symmetric( + vertical: BuildUtils.isWeb ? 16 : 12, + horizontal: 12)) + ..setHintStyle(TextStyle( + color: AppColor.colorHintInputCreateMailbox.withOpacity(opacity), + fontSize: 16)) + ..setErrorText(controller.isVacationDeactivated + ? null + : controller.errorMessageBody.value) + ..setHintText(AppLocalizations + .of(context) + .hintMessageBodyVacation)) + .build()) + ..onChange((value) => controller.updateMessageBody(context, value)) + ..minLines(8) + ..maxLines(null)) + .build(); + }); } } \ No newline at end of file diff --git a/lib/features/manage_account/presentation/extensions/datetime_extension.dart b/lib/features/manage_account/presentation/extensions/datetime_extension.dart index 9d6eb6a3c..6134d90e7 100644 --- a/lib/features/manage_account/presentation/extensions/datetime_extension.dart +++ b/lib/features/manage_account/presentation/extensions/datetime_extension.dart @@ -5,18 +5,26 @@ extension DateTimeExtension on DateTime? { String formatDate({String pattern = 'yyyy/MM/dd', String locale = 'en_US'}) { if (this != null) { - return DateFormat(pattern, locale).format(this!.toLocal()); + return DateFormat(pattern, locale).format(this!); } else { return ''; } } + + DateTime? applied(TimeOfDay? time) { + if (this != null && time != null) { + return DateTime.utc(this!.year, this!.month, this!.day, time.hour, time.minute); + } + return null; + } } extension TimeOfDayExtension on TimeOfDay? { String formatTime(BuildContext context) { if (this != null) { - return this!.format(context); + return MaterialLocalizations.of(context) + .formatTimeOfDay(this!, alwaysUse24HourFormat: true); } return ''; } diff --git a/lib/features/manage_account/presentation/extensions/vacation_response_extension.dart b/lib/features/manage_account/presentation/extensions/vacation_response_extension.dart new file mode 100644 index 000000000..4ee375faa --- /dev/null +++ b/lib/features/manage_account/presentation/extensions/vacation_response_extension.dart @@ -0,0 +1,26 @@ + +import 'package:flutter/material.dart'; +import 'package:jmap_dart_client/jmap/mail/vacation/vacation_response.dart'; +import 'package:tmail_ui_user/features/manage_account/presentation/model/vacation/vacation_presentation.dart'; +import 'package:tmail_ui_user/features/manage_account/presentation/model/vacation/vacation_responder_status.dart'; + +extension VacationResponseExtension on VacationResponse { + + VacationPresentation toVacationPresentation() { + return VacationPresentation( + status: isEnabled == true + ? VacationResponderStatus.activated + : VacationResponderStatus.deactivated, + startDate: fromDate?.value.toUtc(), + startTime: fromDate?.value != null + ? TimeOfDay.fromDateTime(fromDate!.value.toUtc()) + : null, + endDate: toDate?.value.toUtc(), + endTime: toDate?.value != null + ? TimeOfDay.fromDateTime(toDate!.value.toUtc()) + : null, + messageBody: textBody ?? htmlBody, + vacationStopEnabled: toDate != null + ); + } +} \ No newline at end of file diff --git a/lib/features/manage_account/presentation/model/vacation/vacation_presentation.dart b/lib/features/manage_account/presentation/model/vacation/vacation_presentation.dart index 5911e2dc3..08ade0039 100644 --- a/lib/features/manage_account/presentation/model/vacation/vacation_presentation.dart +++ b/lib/features/manage_account/presentation/model/vacation/vacation_presentation.dart @@ -1,8 +1,12 @@ +import 'package:equatable/equatable.dart'; import 'package:flutter/material.dart'; +import 'package:jmap_dart_client/jmap/core/utc_date.dart'; +import 'package:jmap_dart_client/jmap/mail/vacation/vacation_response.dart'; +import 'package:tmail_ui_user/features/manage_account/presentation/extensions/datetime_extension.dart'; import 'package:tmail_ui_user/features/manage_account/presentation/model/vacation/vacation_responder_status.dart'; -class VacationPresentation { +class VacationPresentation with EquatableMixin { final VacationResponderStatus status; final DateTime? startDate; final TimeOfDay? startTime; @@ -44,4 +48,40 @@ class VacationPresentation { vacationStopEnabled: vacationStopEnabled ?? this.vacationStopEnabled ); } + + bool get startDateIsNull => startDate == null; + + bool get starTimeIsNull => startTime == null; + + bool get endDateIsNull => endDate == null; + + bool get endTimeIsNull => endTime == null; + + DateTime? get fromDate => startDate.applied(startTime); + + DateTime? get toDate => endDate.applied(endTime); + + bool get isEnabled => status == VacationResponderStatus.activated; + + @override + List get props => [ + status, + startDate, + startTime, + endDate, + endTime, + vacationStopEnabled, + messageBody + ]; +} + +extension VacationPresentationExtension on VacationPresentation { + VacationResponse toVacationResponse() { + return VacationResponse( + isEnabled: isEnabled, + fromDate: fromDate != null ? UTCDate(fromDate!) : null, + toDate: toDate != null ? UTCDate(toDate!) : null, + textBody: messageBody + ); + } } \ No newline at end of file diff --git a/lib/l10n/intl_messages.arb b/lib/l10n/intl_messages.arb index 7b009576d..73fa82262 100644 --- a/lib/l10n/intl_messages.arb +++ b/lib/l10n/intl_messages.arb @@ -1,5 +1,5 @@ { - "@@last_modified": "2022-08-17T17:27:40.386002", + "@@last_modified": "2022-08-17T20:19:48.796318", "initializing_data": "Initializing data...", "@initializing_data": { "type": "text", @@ -1981,5 +1981,29 @@ "type": "text", "placeholders_order": [], "placeholders": {} + }, + "errorMessageWhenStartDateVacationIsEmpty": "Please enter a valid start date", + "@errorMessageWhenStartDateVacationIsEmpty": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "errorMessageWhenEndDateVacationIsInValid": "End date must be greater than start date", + "@errorMessageWhenEndDateVacationIsInValid": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "errorMessageWhenMessageVacationIsEmpty": "Message body cannot be blank", + "@errorMessageWhenMessageVacationIsEmpty": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "vacationSettingSaved": "Vacation settings saved", + "@vacationSettingSaved": { + "type": "text", + "placeholders_order": [], + "placeholders": {} } } \ No newline at end of file diff --git a/lib/main/localizations/app_localizations.dart b/lib/main/localizations/app_localizations.dart index 65b15c9c0..7f98fcbaf 100644 --- a/lib/main/localizations/app_localizations.dart +++ b/lib/main/localizations/app_localizations.dart @@ -2050,4 +2050,32 @@ class AppLocalizations { name: 'noEndDate', ); } + + String get errorMessageWhenStartDateVacationIsEmpty { + return Intl.message( + 'Please enter a valid start date', + name: 'errorMessageWhenStartDateVacationIsEmpty', + ); + } + + String get errorMessageWhenEndDateVacationIsInValid { + return Intl.message( + 'End date must be greater than start date', + name: 'errorMessageWhenEndDateVacationIsInValid', + ); + } + + String get errorMessageWhenMessageVacationIsEmpty { + return Intl.message( + 'Message body cannot be blank', + name: 'errorMessageWhenMessageVacationIsEmpty', + ); + } + + String get vacationSettingSaved { + return Intl.message( + 'Vacation settings saved', + name: 'vacationSettingSaved', + ); + } } \ No newline at end of file