From 26816b99cd782b5dd283794da9c451f8e58a5cda Mon Sep 17 00:00:00 2001 From: ashrafzarkanisala Date: Fri, 20 Sep 2024 00:41:59 +0300 Subject: [PATCH 01/10] push countdown logic --- .../water_heater/bloc/water_heater_bloc.dart | 342 +++++++++++------- .../water_heater/bloc/water_heater_event.dart | 31 +- .../water_heater/bloc/water_heater_state.dart | 60 +-- .../models/water_heater_status_model.dart | 21 +- .../view/water_heater_device_control.dart | 11 +- .../water_heater/widgets/schedual_view.dart | 127 +++++-- 6 files changed, 381 insertions(+), 211 deletions(-) diff --git a/lib/pages/device_managment/water_heater/bloc/water_heater_bloc.dart b/lib/pages/device_managment/water_heater/bloc/water_heater_bloc.dart index 0deb00b7..c3096216 100644 --- a/lib/pages/device_managment/water_heater/bloc/water_heater_bloc.dart +++ b/lib/pages/device_managment/water_heater/bloc/water_heater_bloc.dart @@ -1,3 +1,5 @@ +// water_heater_bloc.dart + import 'dart:async'; import 'package:bloc/bloc.dart'; import 'package:equatable/equatable.dart'; @@ -14,143 +16,147 @@ class WaterHeaterBloc extends Bloc { on(_controlWaterHeater); on(_updateScheduleEvent); on(_stopScheduleEvent); + on(_onDecrementCountdown); } late WaterHeaterStatusModel deviceStatus; - Timer? _timer; + Timer? _countdownTimer; FutureOr _updateScheduleEvent( UpdateScheduleEvent event, Emitter emit, ) async { - final currentState = state as WaterHeaterScheduleViewState; + final currentState = state; + if (currentState is WaterHeaterDeviceStatusLoaded) { + final countdownRemaining = + // currentState.isActive == true + // ? currentState.countdownRemaining + // : + Duration(hours: event.hours, minutes: event.minutes); - final countdownRemaining = currentState.isActive - ? currentState.countdownRemaining - : Duration(hours: event.hours, minutes: event.minutes); + emit(currentState.copyWith( + scheduleMode: event.scheduleMode, + hours: countdownRemaining.inHours, + minutes: countdownRemaining.inMinutes % 60, + isActive: currentState.isActive, + countdownRemaining: countdownRemaining, + )); - emit(WaterHeaterScheduleViewState( - scheduleMode: event.scheduleMode, - hours: countdownRemaining!.inHours, - minutes: countdownRemaining.inMinutes % 60, - isActive: currentState.isActive, - countdownRemaining: countdownRemaining, - )); - - if (currentState.isActive) { - _startCountdown(countdownRemaining, emit); + if (!currentState.isActive! && countdownRemaining > Duration.zero) { + _startCountdown(emit, countdownRemaining); + } } } FutureOr _controlWaterHeater( - ToggleWaterHeaterEvent event, Emitter emit) async { - final oldValue = _getValueByCode(event.code); + ToggleWaterHeaterEvent event, + Emitter emit, + ) async { + if (state is WaterHeaterDeviceStatusLoaded) { + final currentState = state as WaterHeaterDeviceStatusLoaded; - _updateLocalValue(event.code, event.value, emit); + final oldValue = _getValueByCode(event.code); - emit(WaterHeaterDeviceStatusLoaded(deviceStatus)); + _updateLocalValue(event.code, event.value); - final success = await _runDebounce( - deviceId: event.deviceId, - code: event.code, - value: event.value, - oldValue: oldValue, - emit: emit, - ); - - if (success && (event.code == "countdown_1" || event.code == "switch_inching")) { - final countdownDuration = Duration(seconds: event.value); - - emit(WaterHeaterScheduleViewState( - scheduleMode: deviceStatus.scheduleMode, - hours: countdownDuration.inHours, - minutes: (countdownDuration.inMinutes % 60), - isActive: true, - countdownRemaining: countdownDuration, + emit(currentState.copyWith( + status: deviceStatus, )); - _startCountdown(countdownDuration, emit); + final success = await _runDebounce( + deviceId: event.deviceId, + code: event.code, + value: event.value, + oldValue: oldValue, + emit: emit, + ); + + if (success && + (event.code == "countdown_1" || event.code == "switch_inching")) { + final countdownDuration = Duration(seconds: event.value); + + emit(currentState.copyWith( + status: deviceStatus, + scheduleMode: deviceStatus.scheduleMode, + hours: countdownDuration.inHours, + minutes: (countdownDuration.inMinutes % 60), + isActive: true, + countdownRemaining: countdownDuration, + )); + if (countdownDuration.inSeconds > 0) { + _startCountdown(emit, countdownDuration); + } else { + _countdownTimer?.cancel(); + emit(currentState.copyWith( + hours: 0, + minutes: 0, + isActive: false, + countdownRemaining: Duration.zero, + )); + } + } } } - Future _runDebounce({ - required String deviceId, - required String code, - required dynamic value, - required dynamic oldValue, - required Emitter emit, - }) async { - final completer = Completer(); + FutureOr _stopScheduleEvent( + StopScheduleEvent event, + Emitter emit, + ) async { + if (state is WaterHeaterDeviceStatusLoaded) { + final currentState = state as WaterHeaterDeviceStatusLoaded; - if (_timer != null) { - _timer!.cancel(); - } + _countdownTimer?.cancel(); + + deviceStatus = deviceStatus.copyWith( + countdownHours: 0, + countdownMinutes: 0, + scheduleMode: ScheduleModes.countdown, + ); + + emit(currentState.copyWith( + status: deviceStatus, + scheduleMode: ScheduleModes.countdown, + hours: 0, + minutes: 0, + isActive: false, + countdownRemaining: Duration.zero, + )); - _timer = Timer(const Duration(milliseconds: 500), () async { try { final status = await DevicesManagementApi().deviceControl( - deviceId, - Status(code: code, value: value), + event.deviceId, + Status(code: 'countdown_1', value: 0), ); - if (!status) { - _revertValueAndEmit(deviceId, code, oldValue, emit); - completer.complete(false); - } else { - completer.complete(true); + emit(const WaterHeaterFailedState(error: 'Failed to stop schedule.')); } } catch (e) { - _revertValueAndEmit(deviceId, code, oldValue, emit); - completer.complete(false); + emit(WaterHeaterFailedState(error: e.toString())); } - }); - - return completer.future; - } - - void _revertValueAndEmit( - String deviceId, String code, dynamic oldValue, Emitter emit) { - _updateLocalValue(code, oldValue, emit); - emit(WaterHeaterDeviceStatusLoaded(deviceStatus)); - } - - void _updateLocalValue(String code, dynamic value, Emitter emit) { - switch (code) { - case 'switch_1': - if (value is bool) { - deviceStatus = deviceStatus.copyWith(heaterSwitch: value); - } - break; - default: - break; - } - } - - dynamic _getValueByCode(String code) { - switch (code) { - case 'switch_1': - return deviceStatus.heaterSwitch; - - default: - return null; } } FutureOr _fetchWaterHeaterStatus( - WaterHeaterFetchStatusEvent event, Emitter emit) async { + WaterHeaterFetchStatusEvent event, + Emitter emit, + ) async { emit(WaterHeaterLoadingState()); try { - final status = await DevicesManagementApi().getDeviceStatus(event.deviceId); - deviceStatus = WaterHeaterStatusModel.fromJson(event.deviceId, status.status); + final status = + await DevicesManagementApi().getDeviceStatus(event.deviceId); + deviceStatus = + WaterHeaterStatusModel.fromJson(event.deviceId, status.status); - if (deviceStatus.countdownHours > 0 || deviceStatus.countdownMinutes > 0) { + if (deviceStatus.countdownHours > 0 || + deviceStatus.countdownMinutes > 0) { final remainingDuration = Duration( hours: deviceStatus.countdownHours, minutes: deviceStatus.countdownMinutes, ); - emit(WaterHeaterScheduleViewState( + emit(WaterHeaterDeviceStatusLoaded( + deviceStatus, scheduleMode: deviceStatus.scheduleMode, hours: deviceStatus.countdownHours, minutes: deviceStatus.countdownMinutes, @@ -158,9 +164,10 @@ class WaterHeaterBloc extends Bloc { countdownRemaining: remainingDuration, )); - // _startCountdown(remainingDuration, emit); + _startCountdown(emit, remainingDuration); } else { - emit(WaterHeaterScheduleViewState( + emit(WaterHeaterDeviceStatusLoaded( + deviceStatus, scheduleMode: deviceStatus.scheduleMode, hours: 0, minutes: 0, @@ -172,55 +179,124 @@ class WaterHeaterBloc extends Bloc { } } - void _startCountdown(Duration duration, Emitter emit) { - _timer?.cancel(); + _onDecrementCountdown( + DecrementCountdownEvent event, + Emitter emit, + ) { + if (state is WaterHeaterDeviceStatusLoaded) { + final currentState = state as WaterHeaterDeviceStatusLoaded; - _timer = Timer.periodic(const Duration(seconds: 1), (timer) { - final state = this.state as WaterHeaterScheduleViewState; - final remaining = state.countdownRemaining! - const Duration(seconds: 1); + if (currentState.countdownRemaining != null && + currentState.countdownRemaining! > Duration.zero) { + final newRemaining = + currentState.countdownRemaining! - const Duration(minutes: 1); - if (remaining.isNegative || remaining == Duration.zero) { - _timer?.cancel(); - emit(WaterHeaterScheduleViewState( - scheduleMode: state.scheduleMode, - hours: 0, - minutes: 0, - isActive: false, - countdownRemaining: Duration.zero, - )); - } else { - emit(WaterHeaterScheduleViewState( - scheduleMode: state.scheduleMode, - hours: remaining.inHours, - minutes: remaining.inMinutes % 60, - isActive: true, - countdownRemaining: remaining, + if (newRemaining <= Duration.zero) { + _countdownTimer?.cancel(); + emit(currentState.copyWith( + hours: 0, + minutes: 0, + isActive: false, + countdownRemaining: Duration.zero, + )); + return; + } + + int totalSeconds = newRemaining.inSeconds; + + int newHours = totalSeconds ~/ 3600; + int newMinutes = (totalSeconds % 3600) ~/ 60; + + emit(currentState.copyWith( + hours: newHours, + minutes: newMinutes, + countdownRemaining: newRemaining, )); } + } + } + + void _startCountdown( + Emitter emit, Duration countdownRemaining) { + _countdownTimer?.cancel(); + + _countdownTimer = Timer.periodic(const Duration(minutes: 1), (timer) { + add(DecrementCountdownEvent()); }); } - FutureOr _stopScheduleEvent( - StopScheduleEvent event, - Emitter emit, - ) { - _timer?.cancel(); - deviceStatus = deviceStatus.copyWith( - countdownHours: 0, - countdownMinutes: 0, - scheduleMode: ScheduleModes.countdown, - ); - emit(const WaterHeaterScheduleViewState( - scheduleMode: ScheduleModes.countdown, - hours: 0, - minutes: 0, - isActive: false, - )); + Future _runDebounce({ + required String deviceId, + required String code, + required dynamic value, + required dynamic oldValue, + required Emitter emit, + }) async { + try { + await Future.delayed(const Duration(milliseconds: 500)); + + final status = await DevicesManagementApi().deviceControl( + deviceId, + Status(code: code, value: value), + ); + + if (!status) { + _revertValue(code, oldValue, emit.call); + return false; + } else { + return true; + } + } catch (e) { + _revertValue(code, oldValue, emit.call); + return false; + } + } + + void _revertValue(String code, dynamic oldValue, + void Function(WaterHeaterState state) emit) { + _updateLocalValue(code, oldValue); + if (state is WaterHeaterDeviceStatusLoaded) { + final currentState = state as WaterHeaterDeviceStatusLoaded; + emit(currentState.copyWith( + status: deviceStatus, + )); + } + } + + void _updateLocalValue(String code, dynamic value) { + switch (code) { + case 'switch_1': + if (value is bool) { + deviceStatus = deviceStatus.copyWith(heaterSwitch: value); + } + break; + case 'countdown_1': + if (value is int) { + deviceStatus = deviceStatus.copyWith( + countdownHours: value ~/ 60, + countdownMinutes: value % 60, + ); + } + break; + default: + break; + } + } + + dynamic _getValueByCode(String code) { + switch (code) { + case 'switch_1': + return deviceStatus.heaterSwitch; + case 'countdown_1': + return deviceStatus.countdownHours * 60 + deviceStatus.countdownMinutes; + default: + return null; + } } @override Future close() { - _timer?.cancel(); + _countdownTimer?.cancel(); return super.close(); } } diff --git a/lib/pages/device_managment/water_heater/bloc/water_heater_event.dart b/lib/pages/device_managment/water_heater/bloc/water_heater_event.dart index a0164b3a..1b9c3f11 100644 --- a/lib/pages/device_managment/water_heater/bloc/water_heater_event.dart +++ b/lib/pages/device_managment/water_heater/bloc/water_heater_event.dart @@ -1,3 +1,4 @@ + part of 'water_heater_bloc.dart'; sealed class WaterHeaterEvent extends Equatable { @@ -12,11 +13,14 @@ final class ToggleWaterHeaterEvent extends WaterHeaterEvent { final String deviceId; final String code; - const ToggleWaterHeaterEvent( - {required this.value, required this.deviceId, required this.code}); + const ToggleWaterHeaterEvent({ + required this.value, + required this.deviceId, + required this.code, + }); @override - List get props => [value]; + List get props => [value, deviceId, code]; } final class UpdateScheduleEvent extends WaterHeaterEvent { @@ -24,16 +28,23 @@ final class UpdateScheduleEvent extends WaterHeaterEvent { final int hours; final int minutes; - const UpdateScheduleEvent( - {required this.scheduleMode, required this.hours, required this.minutes}); + const UpdateScheduleEvent({ + required this.scheduleMode, + required this.hours, + required this.minutes, + }); @override List get props => [scheduleMode, hours, minutes]; } -final class StopScheduleEvent extends WaterHeaterEvent {} +final class StopScheduleEvent extends WaterHeaterEvent { + final String deviceId; -class WaterHeaterFetchStatusEvent extends WaterHeaterEvent { + const StopScheduleEvent(this.deviceId); +} + +final class WaterHeaterFetchStatusEvent extends WaterHeaterEvent { final String deviceId; const WaterHeaterFetchStatusEvent(this.deviceId); @@ -42,7 +53,7 @@ class WaterHeaterFetchStatusEvent extends WaterHeaterEvent { List get props => [deviceId]; } -class WaterHeaterFetchBatchStatusEvent extends WaterHeaterEvent { +final class WaterHeaterFetchBatchStatusEvent extends WaterHeaterEvent { final String deviceId; const WaterHeaterFetchBatchStatusEvent(this.deviceId); @@ -51,6 +62,4 @@ class WaterHeaterFetchBatchStatusEvent extends WaterHeaterEvent { List get props => [deviceId]; } -// class ShowScheduleViewEvent extends WaterHeaterEvent { -// const ShowScheduleViewEvent(); -// } +final class DecrementCountdownEvent extends WaterHeaterEvent {} diff --git a/lib/pages/device_managment/water_heater/bloc/water_heater_state.dart b/lib/pages/device_managment/water_heater/bloc/water_heater_state.dart index 2dc45367..092cd90e 100644 --- a/lib/pages/device_managment/water_heater/bloc/water_heater_state.dart +++ b/lib/pages/device_managment/water_heater/bloc/water_heater_state.dart @@ -1,6 +1,6 @@ -part of 'water_heater_bloc.dart'; +// water_heater_state.dart -enum ScheduleType { countdown, schedule, circulate, inching } +part of 'water_heater_bloc.dart'; sealed class WaterHeaterState extends Equatable { const WaterHeaterState(); @@ -15,11 +15,43 @@ final class WaterHeaterLoadingState extends WaterHeaterState {} final class WaterHeaterDeviceStatusLoaded extends WaterHeaterState { final WaterHeaterStatusModel status; + final ScheduleModes? scheduleMode; + final int? hours; + final int? minutes; + final bool? isActive; + final Duration? countdownRemaining; - const WaterHeaterDeviceStatusLoaded(this.status); + const WaterHeaterDeviceStatusLoaded( + this.status, { + this.scheduleMode, + this.hours, + this.minutes, + this.isActive, + this.countdownRemaining, + }); @override - List get props => [status]; + List get props => + [status, scheduleMode, hours, minutes, isActive, countdownRemaining]; + + /// Creates a new instance with updated fields. + WaterHeaterDeviceStatusLoaded copyWith({ + WaterHeaterStatusModel? status, + ScheduleModes? scheduleMode, + int? hours, + int? minutes, + bool? isActive, + Duration? countdownRemaining, + }) { + return WaterHeaterDeviceStatusLoaded( + status ?? this.status, + scheduleMode: scheduleMode ?? this.scheduleMode, + hours: hours ?? this.hours, + minutes: minutes ?? this.minutes, + isActive: isActive ?? this.isActive, + countdownRemaining: countdownRemaining ?? this.countdownRemaining, + ); + } } final class WaterHeaterFailedState extends WaterHeaterState { @@ -39,23 +71,3 @@ final class WaterHeaterBatchFailedState extends WaterHeaterState { @override List get props => [error]; } - -class WaterHeaterScheduleViewState extends WaterHeaterState { - final ScheduleModes scheduleMode; - final int hours; - final int minutes; - final bool isActive; - final Duration? countdownRemaining; - - const WaterHeaterScheduleViewState({ - required this.scheduleMode, - required this.hours, - required this.minutes, - required this.isActive, - this.countdownRemaining, - }); - - @override - List get props => - [scheduleMode, hours, minutes, isActive, countdownRemaining]; -} diff --git a/lib/pages/device_managment/water_heater/models/water_heater_status_model.dart b/lib/pages/device_managment/water_heater/models/water_heater_status_model.dart index fd16df50..2eebefa9 100644 --- a/lib/pages/device_managment/water_heater/models/water_heater_status_model.dart +++ b/lib/pages/device_managment/water_heater/models/water_heater_status_model.dart @@ -1,8 +1,9 @@ +import 'package:equatable/equatable.dart'; import 'package:syncrow_web/pages/device_managment/all_devices/models/device_status.dart'; enum ScheduleModes { countdown, schedule, circulate, inching } -class WaterHeaterStatusModel { +class WaterHeaterStatusModel extends Equatable { final String uuid; final bool heaterSwitch; final int countdownHours; @@ -34,7 +35,7 @@ class WaterHeaterStatusModel { heaterSwitch = status.value ?? false; break; case 'countdown_1': - countdownInSeconds = status.value ?? 0; + countdownInSeconds = status.value ?? 0; break; case 'relay_status': relayStatus = status.value ?? 'memory'; @@ -48,9 +49,8 @@ class WaterHeaterStatusModel { } } - final countdownHours = countdownInSeconds ~/ 3600; - final countdownMinutes = - (countdownInSeconds % 3600) ~/ 60; + final countdownHours = countdownInSeconds ~/ 3600; + final countdownMinutes = (countdownInSeconds % 3600) ~/ 60; return WaterHeaterStatusModel( uuid: id, @@ -97,4 +97,15 @@ class WaterHeaterStatusModel { return ScheduleModes.countdown; } } + + @override + List get props => [ + uuid, + heaterSwitch, + countdownHours, + countdownMinutes, + scheduleMode, + relayStatus, + cycleTiming, + ]; } diff --git a/lib/pages/device_managment/water_heater/view/water_heater_device_control.dart b/lib/pages/device_managment/water_heater/view/water_heater_device_control.dart index 6063a544..e30303ac 100644 --- a/lib/pages/device_managment/water_heater/view/water_heater_device_control.dart +++ b/lib/pages/device_managment/water_heater/view/water_heater_device_control.dart @@ -29,10 +29,12 @@ class WaterHeaterDeviceControl extends StatelessWidget return const Center(child: CircularProgressIndicator()); } else if (state is WaterHeaterDeviceStatusLoaded) { return _buildStatusControls(context, state.status); - } else if (state is WaterHeaterScheduleViewState) { - final status = context.read().deviceStatus; - return _buildStatusControls(context, status); - } else if (state is WaterHeaterFailedState || + } + // else if (state is WaterHeaterScheduleViewState) { + // final status = context.read().deviceStatus; + // return _buildStatusControls(context, status); + // } + else if (state is WaterHeaterFailedState || state is WaterHeaterBatchFailedState) { return const Center(child: Text('Error fetching status')); } else { @@ -75,7 +77,6 @@ class WaterHeaterDeviceControl extends StatelessWidget ), GestureDetector( onTap: () { - // context.read().add(const ShowScheduleViewEvent()); showDialog( context: context, builder: (ctx) => BlocProvider.value( diff --git a/lib/pages/device_managment/water_heater/widgets/schedual_view.dart b/lib/pages/device_managment/water_heater/widgets/schedual_view.dart index 3d221789..5010eaf4 100644 --- a/lib/pages/device_managment/water_heater/widgets/schedual_view.dart +++ b/lib/pages/device_managment/water_heater/widgets/schedual_view.dart @@ -7,11 +7,63 @@ import 'package:syncrow_web/pages/device_managment/water_heater/models/water_hea import 'package:syncrow_web/utils/color_manager.dart'; import 'package:syncrow_web/utils/extension/build_context_x.dart'; -class BuildScheduleView extends StatelessWidget { +class BuildScheduleView extends StatefulWidget { const BuildScheduleView({super.key, required this.status}); final WaterHeaterStatusModel status; + @override + _BuildScheduleViewState createState() => _BuildScheduleViewState(); +} + +class _BuildScheduleViewState extends State { + // late FixedExtentScrollController hoursController; + // late FixedExtentScrollController minutesController; + + // @override + // void initState() { + // super.initState(); + // _initializeControllers(); + // } + + // @override + // void didUpdateWidget(covariant BuildScheduleView oldWidget) { + // super.didUpdateWidget(oldWidget); + // final state = context.read().state; + // if (state is WaterHeaterDeviceStatusLoaded) { + // _initializeControllers(); + // } + // } + + // void _initializeControllers() { + // final state = context.read().state; + // if (state is WaterHeaterDeviceStatusLoaded) { + // hoursController = + // FixedExtentScrollController(initialItem: state.hours ?? 0); + // minutesController = + // FixedExtentScrollController(initialItem: state.minutes ?? 0); + // } else { + // hoursController = FixedExtentScrollController(initialItem: 0); + // minutesController = FixedExtentScrollController(initialItem: 0); + // } + // } + + // void _updateControllers(int hours, int minutes) { + // if (hoursController.hasClients) { + // hoursController.jumpToItem(hours); + // } + // if (minutesController.hasClients) { + // minutesController.jumpToItem(minutes); + // } + // } + + // @override + // void dispose() { + // hoursController.dispose(); + // minutesController.dispose(); + // super.dispose(); + // } + @override Widget build(BuildContext context) { return Dialog( @@ -27,7 +79,8 @@ class BuildScheduleView extends StatelessWidget { padding: const EdgeInsets.symmetric(horizontal: 40.0, vertical: 20), child: BlocBuilder( builder: (context, state) { - if (state is WaterHeaterScheduleViewState) { + if (state is WaterHeaterDeviceStatusLoaded) { + //_updateControllers(state.hours ?? 0, state.minutes ?? 0); return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ @@ -75,9 +128,7 @@ class BuildScheduleView extends StatelessWidget { color: ColorsManager.grayColor, ), ), - const SizedBox( - height: 4, - ), + const SizedBox(height: 4), SizedBox( child: Row( mainAxisAlignment: MainAxisAlignment.spaceEvenly, @@ -101,8 +152,8 @@ class BuildScheduleView extends StatelessWidget { .read() .add(UpdateScheduleEvent( scheduleMode: value, - hours: state.hours, - minutes: state.minutes, + hours: state.hours ?? 0, + minutes: state.minutes ?? 0, )); } }, @@ -128,8 +179,8 @@ class BuildScheduleView extends StatelessWidget { .read() .add(UpdateScheduleEvent( scheduleMode: value, - hours: state.hours, - minutes: state.minutes, + hours: state.hours ?? 0, + minutes: state.minutes ?? 0, )); } }, @@ -154,8 +205,8 @@ class BuildScheduleView extends StatelessWidget { .read() .add(UpdateScheduleEvent( scheduleMode: value, - hours: state.hours, - minutes: state.minutes, + hours: state.hours ?? 0, + minutes: state.minutes ?? 0, )); } }, @@ -180,8 +231,8 @@ class BuildScheduleView extends StatelessWidget { .read() .add(UpdateScheduleEvent( scheduleMode: value, - hours: state.hours, - minutes: state.minutes, + hours: state.hours ?? 0, + minutes: state.minutes ?? 0, )); } }, @@ -230,7 +281,7 @@ class BuildScheduleView extends StatelessWidget { const SizedBox(width: 20), Expanded( child: (state.countdownRemaining != null && - state.isActive) + state.isActive == true) ? DefaultButton( height: 40, onPressed: () { @@ -242,12 +293,13 @@ class BuildScheduleView extends StatelessWidget { ScheduleModes.inching) { code = 'switch_inching'; } - context - .read() - .add(StopScheduleEvent()); + context.read().add( + StopScheduleEvent( + widget.status.uuid)); context.read().add( ToggleWaterHeaterEvent( - deviceId: status.uuid, + deviceId: + widget.status.uuid, code: code, value: 0, ), @@ -269,13 +321,16 @@ class BuildScheduleView extends StatelessWidget { } context.read().add( ToggleWaterHeaterEvent( - deviceId: status.uuid, + deviceId: + widget.status.uuid, code: code, - // value is time in seconds value: Duration( - hours: state.hours, + hours: + state.hours ?? + 0, minutes: - state.minutes) + state.minutes ?? + 0) .inSeconds, ), ); @@ -303,7 +358,7 @@ class BuildScheduleView extends StatelessWidget { } Row _hourMinutesWheel( - WaterHeaterScheduleViewState state, BuildContext context) { + WaterHeaterDeviceStatusLoaded state, BuildContext context) { return Row( mainAxisAlignment: MainAxisAlignment.start, children: [ @@ -320,16 +375,19 @@ class BuildScheduleView extends StatelessWidget { borderRadius: BorderRadius.circular(8), ), child: ListWheelScrollView.useDelegate( - controller: - FixedExtentScrollController(initialItem: state.hours), + key: ValueKey('hours_${state.hours}'), + controller: FixedExtentScrollController( + initialItem: state.hours ?? 0, + ), itemExtent: 40.0, - physics: FixedExtentScrollPhysics(), + physics: const FixedExtentScrollPhysics(), onSelectedItemChanged: (int value) { context.read().add( UpdateScheduleEvent( - scheduleMode: state.scheduleMode, + scheduleMode: + state.scheduleMode ?? ScheduleModes.countdown, hours: value, - minutes: state.minutes, + minutes: state.minutes ?? 0, ), ); }, @@ -370,15 +428,18 @@ class BuildScheduleView extends StatelessWidget { borderRadius: BorderRadius.circular(8), ), child: ListWheelScrollView.useDelegate( - controller: - FixedExtentScrollController(initialItem: state.minutes), + key: ValueKey('minutes_${state.minutes}'), + controller: FixedExtentScrollController( + initialItem: state.minutes ?? 0, + ), itemExtent: 40.0, - physics: FixedExtentScrollPhysics(), + physics: const FixedExtentScrollPhysics(), onSelectedItemChanged: (int value) { context.read().add( UpdateScheduleEvent( - scheduleMode: state.scheduleMode, - hours: state.hours, + scheduleMode: + state.scheduleMode ?? ScheduleModes.countdown, + hours: state.hours ?? 0, minutes: value, ), ); From 921ccf0132a96b888176066385290d1ad6657672 Mon Sep 17 00:00:00 2001 From: ashrafzarkanisala Date: Fri, 20 Sep 2024 02:27:07 +0300 Subject: [PATCH 02/10] push schedule basic design and bloc manegment --- .../water_heater/bloc/water_heater_bloc.dart | 54 +- .../water_heater/bloc/water_heater_event.dart | 42 +- .../water_heater/bloc/water_heater_state.dart | 32 +- .../water_heater/widgets/schedual_view.dart | 1036 ++++++++++------- 4 files changed, 725 insertions(+), 439 deletions(-) diff --git a/lib/pages/device_managment/water_heater/bloc/water_heater_bloc.dart b/lib/pages/device_managment/water_heater/bloc/water_heater_bloc.dart index c3096216..fcf52732 100644 --- a/lib/pages/device_managment/water_heater/bloc/water_heater_bloc.dart +++ b/lib/pages/device_managment/water_heater/bloc/water_heater_bloc.dart @@ -3,6 +3,7 @@ import 'dart:async'; import 'package:bloc/bloc.dart'; import 'package:equatable/equatable.dart'; +import 'package:flutter/material.dart'; import 'package:syncrow_web/pages/device_managment/all_devices/models/device_status.dart'; import 'package:syncrow_web/pages/device_managment/water_heater/models/water_heater_status_model.dart'; import 'package:syncrow_web/services/devices_mang_api.dart'; @@ -17,6 +18,9 @@ class WaterHeaterBloc extends Bloc { on(_updateScheduleEvent); on(_stopScheduleEvent); on(_onDecrementCountdown); + on(_onAddSchedule); + on(_onDeleteSchedule); + on(_onUpdateSchedule); } late WaterHeaterStatusModel deviceStatus; @@ -179,7 +183,7 @@ class WaterHeaterBloc extends Bloc { } } - _onDecrementCountdown( + _onDecrementCountdown( DecrementCountdownEvent event, Emitter emit, ) { @@ -294,6 +298,54 @@ class WaterHeaterBloc extends Bloc { } } + FutureOr _onAddSchedule( + AddScheduleEvent event, + Emitter emit, + ) { + if (state is WaterHeaterDeviceStatusLoaded) { + final currentState = state as WaterHeaterDeviceStatusLoaded; + final newSchedule = ScheduleEntry( + selectedDays: event.selectedDays, + time: event.time, + functionOn: event.functionOn, + ); + final updatedSchedules = List.from(currentState.schedules) + ..add(newSchedule); + + emit(currentState.copyWith(schedules: updatedSchedules)); + } + } + + FutureOr _onDeleteSchedule( + DeleteScheduleEvent event, + Emitter emit, + ) { + if (state is WaterHeaterDeviceStatusLoaded) { + final currentState = state as WaterHeaterDeviceStatusLoaded; + final updatedSchedules = List.from(currentState.schedules) + ..removeAt(event.index); + + emit(currentState.copyWith(schedules: updatedSchedules)); + } + } + + FutureOr _onUpdateSchedule( + UpdateScheduleEntryEvent event, + Emitter emit, + ) { + if (state is WaterHeaterDeviceStatusLoaded) { + final currentState = state as WaterHeaterDeviceStatusLoaded; + final updatedSchedules = List.from(currentState.schedules); + updatedSchedules[event.index] = ScheduleEntry( + selectedDays: event.selectedDays, + time: event.time, + functionOn: event.functionOn, + ); + + emit(currentState.copyWith(schedules: updatedSchedules)); + } + } + @override Future close() { _countdownTimer?.cancel(); diff --git a/lib/pages/device_managment/water_heater/bloc/water_heater_event.dart b/lib/pages/device_managment/water_heater/bloc/water_heater_event.dart index 1b9c3f11..360f4a4f 100644 --- a/lib/pages/device_managment/water_heater/bloc/water_heater_event.dart +++ b/lib/pages/device_managment/water_heater/bloc/water_heater_event.dart @@ -1,4 +1,3 @@ - part of 'water_heater_bloc.dart'; sealed class WaterHeaterEvent extends Equatable { @@ -63,3 +62,44 @@ final class WaterHeaterFetchBatchStatusEvent extends WaterHeaterEvent { } final class DecrementCountdownEvent extends WaterHeaterEvent {} + +final class AddScheduleEvent extends WaterHeaterEvent { + final List selectedDays; + final TimeOfDay time; + final bool functionOn; + + const AddScheduleEvent({ + required this.selectedDays, + required this.time, + required this.functionOn, + }); + + @override + List get props => [selectedDays, time, functionOn]; +} + +final class DeleteScheduleEvent extends WaterHeaterEvent { + final int index; + + const DeleteScheduleEvent(this.index); + + @override + List get props => [index]; +} + +final class UpdateScheduleEntryEvent extends WaterHeaterEvent { + final int index; + final List selectedDays; + final TimeOfDay time; + final bool functionOn; + + const UpdateScheduleEntryEvent({ + required this.index, + required this.selectedDays, + required this.time, + required this.functionOn, + }); + + @override + List get props => [index, selectedDays, time, functionOn]; +} diff --git a/lib/pages/device_managment/water_heater/bloc/water_heater_state.dart b/lib/pages/device_managment/water_heater/bloc/water_heater_state.dart index 092cd90e..95c68f24 100644 --- a/lib/pages/device_managment/water_heater/bloc/water_heater_state.dart +++ b/lib/pages/device_managment/water_heater/bloc/water_heater_state.dart @@ -20,6 +20,7 @@ final class WaterHeaterDeviceStatusLoaded extends WaterHeaterState { final int? minutes; final bool? isActive; final Duration? countdownRemaining; + final List schedules; const WaterHeaterDeviceStatusLoaded( this.status, { @@ -28,13 +29,20 @@ final class WaterHeaterDeviceStatusLoaded extends WaterHeaterState { this.minutes, this.isActive, this.countdownRemaining, + this.schedules = const [], }); @override - List get props => - [status, scheduleMode, hours, minutes, isActive, countdownRemaining]; + List get props => [ + status, + scheduleMode, + hours, + minutes, + isActive, + countdownRemaining, + schedules, + ]; - /// Creates a new instance with updated fields. WaterHeaterDeviceStatusLoaded copyWith({ WaterHeaterStatusModel? status, ScheduleModes? scheduleMode, @@ -42,6 +50,7 @@ final class WaterHeaterDeviceStatusLoaded extends WaterHeaterState { int? minutes, bool? isActive, Duration? countdownRemaining, + List? schedules, }) { return WaterHeaterDeviceStatusLoaded( status ?? this.status, @@ -50,10 +59,27 @@ final class WaterHeaterDeviceStatusLoaded extends WaterHeaterState { minutes: minutes ?? this.minutes, isActive: isActive ?? this.isActive, countdownRemaining: countdownRemaining ?? this.countdownRemaining, + schedules: schedules ?? this.schedules, ); } } +class ScheduleEntry { + final List selectedDays; + final TimeOfDay time; + final bool functionOn; + + ScheduleEntry({ + required this.selectedDays, + required this.time, + required this.functionOn, + }); + + @override + String toString() => + 'ScheduleEntry(selectedDays: $selectedDays, time: $time, functionOn: $functionOn)'; +} + final class WaterHeaterFailedState extends WaterHeaterState { final String error; diff --git a/lib/pages/device_managment/water_heater/widgets/schedual_view.dart b/lib/pages/device_managment/water_heater/widgets/schedual_view.dart index 5010eaf4..74cdb94b 100644 --- a/lib/pages/device_managment/water_heater/widgets/schedual_view.dart +++ b/lib/pages/device_managment/water_heater/widgets/schedual_view.dart @@ -1,4 +1,3 @@ -import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:syncrow_web/pages/common/buttons/default_button.dart'; @@ -17,339 +16,46 @@ class BuildScheduleView extends StatefulWidget { } class _BuildScheduleViewState extends State { - // late FixedExtentScrollController hoursController; - // late FixedExtentScrollController minutesController; - - // @override - // void initState() { - // super.initState(); - // _initializeControllers(); - // } - - // @override - // void didUpdateWidget(covariant BuildScheduleView oldWidget) { - // super.didUpdateWidget(oldWidget); - // final state = context.read().state; - // if (state is WaterHeaterDeviceStatusLoaded) { - // _initializeControllers(); - // } - // } - - // void _initializeControllers() { - // final state = context.read().state; - // if (state is WaterHeaterDeviceStatusLoaded) { - // hoursController = - // FixedExtentScrollController(initialItem: state.hours ?? 0); - // minutesController = - // FixedExtentScrollController(initialItem: state.minutes ?? 0); - // } else { - // hoursController = FixedExtentScrollController(initialItem: 0); - // minutesController = FixedExtentScrollController(initialItem: 0); - // } - // } - - // void _updateControllers(int hours, int minutes) { - // if (hoursController.hasClients) { - // hoursController.jumpToItem(hours); - // } - // if (minutesController.hasClients) { - // minutesController.jumpToItem(minutes); - // } - // } - - // @override - // void dispose() { - // hoursController.dispose(); - // minutesController.dispose(); - // super.dispose(); - // } - @override Widget build(BuildContext context) { - return Dialog( - backgroundColor: Colors.white, - insetPadding: const EdgeInsets.all(20), - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(20), - ), - child: SizedBox( - width: 700, - child: SingleChildScrollView( - child: Padding( - padding: const EdgeInsets.symmetric(horizontal: 40.0, vertical: 20), - child: BlocBuilder( - builder: (context, state) { - if (state is WaterHeaterDeviceStatusLoaded) { - //_updateControllers(state.hours ?? 0, state.minutes ?? 0); - return Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - const SizedBox(), - Text( - 'Scheduling', - style: TextStyle( - fontWeight: FontWeight.bold, - fontSize: 22, - color: ColorsManager.dialogBlueTitle, - ), - ), - Container( - width: 25, - decoration: BoxDecoration( - color: Colors.transparent, - shape: BoxShape.circle, - border: Border.all( - color: Colors.grey, - width: 1.0, - ), - ), - child: IconButton( - padding: EdgeInsets.all(1), - icon: const Icon( - Icons.close, - color: Colors.grey, - size: 18, - ), - onPressed: () { - Navigator.of(context).pop(); - }, - ), - ), - ], - ), - const SizedBox(height: 20), - Text( - 'Type:', - style: context.textTheme.bodySmall!.copyWith( - fontSize: 13, - color: ColorsManager.grayColor, - ), - ), - const SizedBox(height: 4), - SizedBox( - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceEvenly, - children: [ - Flexible( - child: ListTile( - contentPadding: EdgeInsets.zero, - title: Text( - 'Countdown', - style: context.textTheme.bodySmall!.copyWith( - fontSize: 13, - color: ColorsManager.blackColor, - ), - ), - leading: Radio( - value: ScheduleModes.countdown, - groupValue: state.scheduleMode, - onChanged: (ScheduleModes? value) { - if (value != null) { - context - .read() - .add(UpdateScheduleEvent( - scheduleMode: value, - hours: state.hours ?? 0, - minutes: state.minutes ?? 0, - )); - } - }, - ), - ), - ), - Flexible( - child: ListTile( - contentPadding: EdgeInsets.zero, - title: Text( - 'Schedule', - style: context.textTheme.bodySmall!.copyWith( - fontSize: 13, - color: ColorsManager.blackColor, - ), - ), - leading: Radio( - value: ScheduleModes.schedule, - groupValue: state.scheduleMode, - onChanged: (ScheduleModes? value) { - if (value != null) { - context - .read() - .add(UpdateScheduleEvent( - scheduleMode: value, - hours: state.hours ?? 0, - minutes: state.minutes ?? 0, - )); - } - }, - ), - ), - ), - Flexible( - child: ListTile( - title: Text( - 'Circulate', - style: context.textTheme.bodySmall!.copyWith( - fontSize: 13, - color: ColorsManager.blackColor, - ), - ), - leading: Radio( - value: ScheduleModes.circulate, - groupValue: state.scheduleMode, - onChanged: (ScheduleModes? value) { - if (value != null) { - context - .read() - .add(UpdateScheduleEvent( - scheduleMode: value, - hours: state.hours ?? 0, - minutes: state.minutes ?? 0, - )); - } - }, - ), - ), - ), - Flexible( - child: ListTile( - title: Text( - 'Inching', - style: context.textTheme.bodySmall!.copyWith( - fontSize: 13, - color: ColorsManager.blackColor, - ), - ), - leading: Radio( - value: ScheduleModes.inching, - groupValue: state.scheduleMode, - onChanged: (ScheduleModes? value) { - if (value != null) { - context - .read() - .add(UpdateScheduleEvent( - scheduleMode: value, - hours: state.hours ?? 0, - minutes: state.minutes ?? 0, - )); - } - }, - ), - ), - ), - ], - ), - ), - const SizedBox(height: 20), - if (state.scheduleMode == ScheduleModes.countdown || - state.scheduleMode == ScheduleModes.inching) ...[ - Text( - 'Countdown:', - style: context.textTheme.bodySmall!.copyWith( - fontSize: 13, - color: ColorsManager.grayColor, - ), - ), - const SizedBox(height: 4), - _hourMinutesWheel(state, context) + final bloc = BlocProvider.of(context); + return BlocProvider.value( + value: bloc, + child: Dialog( + backgroundColor: Colors.white, + insetPadding: const EdgeInsets.all(20), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(20), + ), + child: SizedBox( + width: 700, + child: SingleChildScrollView( + child: Padding( + padding: + const EdgeInsets.symmetric(horizontal: 40.0, vertical: 20), + child: BlocBuilder( + builder: (context, state) { + if (state is WaterHeaterDeviceStatusLoaded) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + _scheduleHeader(context), + const SizedBox(height: 20), + _buildScheduleModeSelector(context, state), + const SizedBox(height: 20), + if (state.scheduleMode == ScheduleModes.schedule) + _buildScheduleManagementUI(state), + if (state.scheduleMode == ScheduleModes.countdown || + state.scheduleMode == ScheduleModes.inching) + ..._buildCountDownAngInchingView(context, state), + const SizedBox(height: 20), + _buildSaveStopCancelButtons(context, state), ], - const SizedBox(height: 20), - Center( - child: SizedBox( - width: 400, - height: 50, - child: Center( - child: Row( - mainAxisAlignment: MainAxisAlignment.center, - mainAxisSize: MainAxisSize.max, - children: [ - Expanded( - child: DefaultButton( - height: 40, - onPressed: () { - Navigator.pop(context); - }, - backgroundColor: ColorsManager.boxColor, - child: Text( - 'Cancel', - style: context.textTheme.bodyMedium, - ), - ), - ), - const SizedBox(width: 20), - Expanded( - child: (state.countdownRemaining != null && - state.isActive == true) - ? DefaultButton( - height: 40, - onPressed: () { - late String code; - if (state.scheduleMode == - ScheduleModes.countdown) { - code = 'countdown_1'; - } else if (state.scheduleMode == - ScheduleModes.inching) { - code = 'switch_inching'; - } - context.read().add( - StopScheduleEvent( - widget.status.uuid)); - context.read().add( - ToggleWaterHeaterEvent( - deviceId: - widget.status.uuid, - code: code, - value: 0, - ), - ); - }, - backgroundColor: Colors.red, - child: const Text('Stop'), - ) - : DefaultButton( - height: 40, - onPressed: () { - late String code; - if (state.scheduleMode == - ScheduleModes.countdown) { - code = 'countdown_1'; - } else if (state.scheduleMode == - ScheduleModes.inching) { - code = 'switch_inching'; - } - context.read().add( - ToggleWaterHeaterEvent( - deviceId: - widget.status.uuid, - code: code, - value: Duration( - hours: - state.hours ?? - 0, - minutes: - state.minutes ?? - 0) - .inSeconds, - ), - ); - }, - backgroundColor: - ColorsManager.primaryColor, - child: const Text('Save'), - ), - ), - ], - ), - ), - ), - ), - ], - ); - } - return const SizedBox(); - }, + ); + } + return const SizedBox(); + }, + ), ), ), ), @@ -357,117 +63,579 @@ class _BuildScheduleViewState extends State { ); } - Row _hourMinutesWheel( - WaterHeaterDeviceStatusLoaded state, BuildContext context) { + Row _scheduleHeader(BuildContext context) { return Row( - mainAxisAlignment: MainAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ - // Hours Picker - Row( - mainAxisSize: MainAxisSize.min, - children: [ - Container( - height: 50, - width: 80, - padding: const EdgeInsets.symmetric(horizontal: 16), - decoration: BoxDecoration( - color: ColorsManager.boxColor, - borderRadius: BorderRadius.circular(8), - ), - child: ListWheelScrollView.useDelegate( - key: ValueKey('hours_${state.hours}'), - controller: FixedExtentScrollController( - initialItem: state.hours ?? 0, - ), - itemExtent: 40.0, - physics: const FixedExtentScrollPhysics(), - onSelectedItemChanged: (int value) { - context.read().add( - UpdateScheduleEvent( - scheduleMode: - state.scheduleMode ?? ScheduleModes.countdown, - hours: value, - minutes: state.minutes ?? 0, - ), - ); - }, - childDelegate: ListWheelChildBuilderDelegate( - builder: (context, index) { - return Center( - child: Text( - index.toString().padLeft(2, '0'), - style: const TextStyle(fontSize: 24), - ), - ); - }, - childCount: 24, - ), - ), - ), - const SizedBox(height: 8), - const Text( - 'h', - style: TextStyle( - color: ColorsManager.grayColor, - fontSize: 18, - ), - ), - ], + const SizedBox(), + Text( + 'Scheduling', + style: TextStyle( + fontWeight: FontWeight.bold, + fontSize: 22, + color: ColorsManager.dialogBlueTitle, + ), ), - const SizedBox(width: 10), - // Minutes Picker + Container( + width: 25, + decoration: BoxDecoration( + color: Colors.transparent, + shape: BoxShape.circle, + border: Border.all( + color: Colors.grey, + width: 1.0, + ), + ), + child: IconButton( + padding: EdgeInsets.all(1), + icon: const Icon( + Icons.close, + color: Colors.grey, + size: 18, + ), + onPressed: () { + Navigator.of(context).pop(); + }, + ), + ), + ], + ); + } + + Widget _buildScheduleModeSelector( + BuildContext context, WaterHeaterDeviceStatusLoaded state) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'Type:', + style: context.textTheme.bodySmall!.copyWith( + fontSize: 13, + color: ColorsManager.grayColor, + ), + ), + const SizedBox(height: 4), Row( - mainAxisSize: MainAxisSize.min, + mainAxisAlignment: MainAxisAlignment.spaceEvenly, children: [ - Container( - height: 50, - width: 80, - padding: const EdgeInsets.symmetric(horizontal: 16), - decoration: BoxDecoration( - color: ColorsManager.boxColor, - borderRadius: BorderRadius.circular(8), - ), - child: ListWheelScrollView.useDelegate( - key: ValueKey('minutes_${state.minutes}'), - controller: FixedExtentScrollController( - initialItem: state.minutes ?? 0, - ), - itemExtent: 40.0, - physics: const FixedExtentScrollPhysics(), - onSelectedItemChanged: (int value) { - context.read().add( - UpdateScheduleEvent( - scheduleMode: - state.scheduleMode ?? ScheduleModes.countdown, - hours: state.hours ?? 0, - minutes: value, - ), - ); - }, - childDelegate: ListWheelChildBuilderDelegate( - builder: (context, index) { - return Center( - child: Text( - index.toString().padLeft(2, '0'), - style: const TextStyle(fontSize: 24), - ), - ); - }, - childCount: 60, - ), - ), - ), - const SizedBox(height: 8), - const Text( - 'm', - style: TextStyle( - color: ColorsManager.grayColor, - fontSize: 18, - ), - ), + _buildRadioTile( + context, 'Countdown', ScheduleModes.countdown, state), + _buildRadioTile(context, 'Schedule', ScheduleModes.schedule, state), + _buildRadioTile( + context, 'Circulate', ScheduleModes.circulate, state), + _buildRadioTile(context, 'Inching', ScheduleModes.inching, state), ], ), ], ); } + + Widget _buildRadioTile(BuildContext context, String label, ScheduleModes mode, + WaterHeaterDeviceStatusLoaded state) { + return Flexible( + child: ListTile( + contentPadding: EdgeInsets.zero, + title: Text( + label, + style: context.textTheme.bodySmall!.copyWith( + fontSize: 13, + color: ColorsManager.blackColor, + ), + ), + leading: Radio( + value: mode, + groupValue: state.scheduleMode, + onChanged: (ScheduleModes? value) { + if (value != null) { + context.read().add(UpdateScheduleEvent( + scheduleMode: value, + hours: state.hours ?? 0, + minutes: state.minutes ?? 0, + )); + } + }, + ), + ), + ); + } + + Widget _buildScheduleManagementUI(WaterHeaterDeviceStatusLoaded state) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + ElevatedButton( + onPressed: () => _showAddScheduleDialog(context), + child: Text('+ Add new schedule'), + ), + const SizedBox(height: 20), + _buildScheduleTable(state), + ], + ); + } + + Widget _buildScheduleTable(WaterHeaterDeviceStatusLoaded state) { + return Table( + border: TableBorder.all(color: Colors.grey), + children: [ + TableRow( + children: [ + _buildTableHeader('Active'), + _buildTableHeader('Days'), + _buildTableHeader('Time'), + _buildTableHeader('Function'), + _buildTableHeader('Action'), + ], + ), + if (state.schedules.isEmpty) + TableRow( + children: [ + Center( + child: Padding( + padding: const EdgeInsets.all(8.0), + child: Text( + 'No schedules added yet', + style: context.textTheme.bodySmall!.copyWith( + fontSize: 13, + color: ColorsManager.grayColor, + ), + ), + ), + ), + const SizedBox(), + const SizedBox(), + const SizedBox(), + const SizedBox(), + ], + ), + for (int i = 0; i < state.schedules.length; i++) + _buildScheduleRow(state.schedules[i], i, context), + ], + ); + } + + TableRow _buildScheduleRow( + ScheduleEntry schedule, int index, BuildContext context) { + return TableRow( + children: [ + Center( + child: schedule.functionOn + ? Icon(Icons.radio_button_checked) + : Icon(Icons.radio_button_unchecked)), + Center(child: Text(_getSelectedDays(schedule.selectedDays))), + Center(child: Text(schedule.time.format(context))), + Center(child: Text(schedule.functionOn ? 'On' : 'Off')), + Center( + child: Wrap( + runAlignment: WrapAlignment.center, + children: [ + TextButton( + onPressed: () { + _showEditScheduleDialog(context, schedule, index); + }, + child: Text( + 'Edit', + style: context.textTheme.bodySmall! + .copyWith(color: ColorsManager.blueColor), + ), + ), + TextButton( + onPressed: () { + context + .read() + .add(DeleteScheduleEvent(index)); + }, + child: Text( + 'Delete', + style: context.textTheme.bodySmall! + .copyWith(color: ColorsManager.blueColor), + ), + ), + ], + ), + ), + ], + ); + } + + String _getSelectedDays(List selectedDays) { + final days = ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun']; + List selectedDaysStr = []; + for (int i = 0; i < selectedDays.length; i++) { + if (selectedDays[i]) { + selectedDaysStr.add(days[i]); + } + } + return selectedDaysStr.join(', '); + } + + void _showEditScheduleDialog( + BuildContext context, ScheduleEntry schedule, int index) { + List selectedDays = List.from(schedule.selectedDays); + TimeOfDay selectedTime = schedule.time; + bool isOn = schedule.functionOn; + + showDialog( + context: context, + builder: (ctx) { + return StatefulBuilder( + builder: (context, setState) { + return AlertDialog( + title: const Text('Edit Schedule'), + content: Column( + mainAxisSize: MainAxisSize.min, + children: [ + ElevatedButton( + onPressed: () async { + TimeOfDay? time = await showTimePicker( + context: context, + initialTime: selectedTime, + ); + if (time != null) { + setState(() { + selectedTime = time; + }); + } + }, + child: Text(selectedTime.format(context)), + ), + // Same checkboxes and function on/off logic as before + ], + ), + actions: [ + TextButton( + onPressed: () { + Navigator.pop(context); + }, + child: const Text('Cancel'), + ), + TextButton( + onPressed: () { + context.read().add( + UpdateScheduleEntryEvent( + index: index, + selectedDays: selectedDays, + time: selectedTime, + functionOn: isOn, + ), + ); + Navigator.pop(context); + }, + child: const Text('Save'), + ), + ], + ); + }, + ); + }, + ); + } + + Widget _buildTableHeader(String label) { + return Padding( + padding: const EdgeInsets.all(8.0), + child: Text( + label, + style: const TextStyle(fontWeight: FontWeight.bold), + ), + ); + } + + void _showAddScheduleDialog(BuildContext context) { + showDialog( + context: context, + builder: (ctx) { + List selectedDays = [ + false, + false, + false, + false, + false, + false, + false + ]; // Mon - Sun + TimeOfDay? selectedTime; + bool isOn = false; + + return BlocProvider.value( + value: BlocProvider.of(context), + child: StatefulBuilder( + builder: (ctx, setState) { + return AlertDialog( + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(20)), + title: const Text('Scheduling'), + content: Column( + mainAxisSize: MainAxisSize.min, + children: [ + ElevatedButton( + onPressed: () async { + TimeOfDay? time = await showTimePicker( + context: context, + initialTime: TimeOfDay.now(), + ); + if (time != null) { + setState(() { + selectedTime = time; + }); + } + }, + child: Text( + selectedTime == null + ? 'Time' + : '${selectedTime!.format(context)}', + ), + ), + const SizedBox(height: 10), + Row( + children: [ + _buildDayCheckbox(setState, 'Mon', 0, selectedDays), + _buildDayCheckbox(setState, 'Tue', 1, selectedDays), + _buildDayCheckbox(setState, 'Wed', 2, selectedDays), + _buildDayCheckbox(setState, 'Thu', 3, selectedDays), + _buildDayCheckbox(setState, 'Fri', 4, selectedDays), + _buildDayCheckbox(setState, 'Sat', 5, selectedDays), + _buildDayCheckbox(setState, 'Sun', 6, selectedDays), + ], + ), + const SizedBox(height: 10), + Row( + children: [ + const Text('Function:'), + Radio( + value: true, + groupValue: isOn, + onChanged: (bool? value) { + setState(() { + isOn = value!; + }); + }, + ), + const Text('On'), + Radio( + value: false, + groupValue: isOn, + onChanged: (bool? value) { + setState(() { + isOn = value!; + }); + }, + ), + const Text('Off'), + ], + ), + ], + ), + actions: [ + TextButton( + onPressed: () { + Navigator.pop(context); + }, + child: const Text('Cancel'), + ), + TextButton( + onPressed: () { + // Dispatch Bloc Event to Add Schedule + if (selectedTime != null) { + context.read().add(AddScheduleEvent( + time: selectedTime!, + selectedDays: selectedDays, + functionOn: isOn, + )); + Navigator.pop(context); // Close the dialog + } + }, + child: const Text('Save'), + ), + ], + ); + }, + ), + ); + }, + ); + } + + Widget _buildDayCheckbox(void Function(void Function()) setState, String day, + int index, List selectedDays) { + return Row( + children: [ + Checkbox( + value: selectedDays[index], + onChanged: (bool? value) { + setState(() { + selectedDays[index] = value!; + }); + }, + ), + Text(day), + ], + ); + } + + Center _buildSaveStopCancelButtons( + BuildContext context, WaterHeaterDeviceStatusLoaded state) { + return Center( + child: SizedBox( + width: 400, + height: 50, + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + mainAxisSize: MainAxisSize.max, + children: [ + Expanded( + child: DefaultButton( + height: 40, + onPressed: () { + Navigator.pop(context); + }, + backgroundColor: ColorsManager.boxColor, + child: Text( + 'Cancel', + style: context.textTheme.bodyMedium, + ), + ), + ), + const SizedBox(width: 20), + Expanded( + child: + (state.countdownRemaining != null && state.isActive == true) + ? DefaultButton( + height: 40, + onPressed: () { + late String code; + if (state.scheduleMode == ScheduleModes.countdown) { + code = 'countdown_1'; + } else if (state.scheduleMode == + ScheduleModes.inching) { + code = 'switch_inching'; + } + context + .read() + .add(StopScheduleEvent(widget.status.uuid)); + context.read().add( + ToggleWaterHeaterEvent( + deviceId: widget.status.uuid, + code: code, + value: 0, + ), + ); + }, + backgroundColor: Colors.red, + child: const Text('Stop'), + ) + : DefaultButton( + height: 40, + onPressed: () { + late String code; + if (state.scheduleMode == ScheduleModes.countdown) { + code = 'countdown_1'; + } else if (state.scheduleMode == + ScheduleModes.inching) { + code = 'switch_inching'; + } + context.read().add( + ToggleWaterHeaterEvent( + deviceId: widget.status.uuid, + code: code, + value: Duration( + hours: state.hours ?? 0, + minutes: state.minutes ?? 0) + .inSeconds, + ), + ); + }, + backgroundColor: ColorsManager.primaryColor, + child: const Text('Save'), + ), + ), + ], + ), + ), + ); + } + + List _buildCountDownAngInchingView( + BuildContext context, WaterHeaterDeviceStatusLoaded state) { + return [ + Text( + 'Countdown:', + style: context.textTheme.bodySmall!.copyWith( + fontSize: 13, + color: ColorsManager.grayColor, + ), + ), + const SizedBox(height: 4), + _hourMinutesWheel(state, context) + ]; + } + + Row _hourMinutesWheel( + WaterHeaterDeviceStatusLoaded state, BuildContext context) { + return Row( + mainAxisAlignment: MainAxisAlignment.start, + children: [ + _buildPickerColumn(context, 'h', state.hours ?? 0, 24, (value) { + context.read().add(UpdateScheduleEvent( + scheduleMode: state.scheduleMode ?? ScheduleModes.countdown, + hours: value, + minutes: state.minutes ?? 0, + )); + }), + const SizedBox(width: 10), + _buildPickerColumn(context, 'm', state.minutes ?? 0, 60, (value) { + context.read().add(UpdateScheduleEvent( + scheduleMode: state.scheduleMode ?? ScheduleModes.countdown, + hours: state.hours ?? 0, + minutes: value, + )); + }), + ], + ); + } + + Widget _buildPickerColumn(BuildContext context, String label, + int initialValue, int itemCount, ValueChanged onSelected) { + return Row( + mainAxisSize: MainAxisSize.min, + children: [ + Container( + height: 50, + width: 80, + padding: const EdgeInsets.symmetric(horizontal: 16), + decoration: BoxDecoration( + color: ColorsManager.boxColor, + borderRadius: BorderRadius.circular(8), + ), + child: ListWheelScrollView.useDelegate( + key: ValueKey('$label-$initialValue'), + controller: FixedExtentScrollController( + initialItem: initialValue, + ), + itemExtent: 40.0, + physics: const FixedExtentScrollPhysics(), + onSelectedItemChanged: onSelected, + childDelegate: ListWheelChildBuilderDelegate( + builder: (context, index) { + return Center( + child: Text( + index.toString().padLeft(2, '0'), + style: const TextStyle(fontSize: 24), + ), + ); + }, + childCount: itemCount, + ), + ), + ), + const SizedBox(height: 8), + Text( + label, + style: const TextStyle( + color: ColorsManager.grayColor, + fontSize: 18, + ), + ), + ], + ); + } } From 66f45721e51036d42b749a02c1a13c4b9a51184d Mon Sep 17 00:00:00 2001 From: ashrafzarkanisala Date: Sun, 22 Sep 2024 00:16:44 +0300 Subject: [PATCH 03/10] working on schedual table --- lib/pages/common/buttons/default_button.dart | 4 + .../water_heater/widgets/schedual_view.dart | 222 +++++++++++++----- lib/utils/theme/theme.dart | 7 +- 3 files changed, 165 insertions(+), 68 deletions(-) diff --git a/lib/pages/common/buttons/default_button.dart b/lib/pages/common/buttons/default_button.dart index 5aa506f8..77eb9bd7 100644 --- a/lib/pages/common/buttons/default_button.dart +++ b/lib/pages/common/buttons/default_button.dart @@ -17,6 +17,7 @@ class DefaultButton extends StatelessWidget { this.borderRadius, this.height, this.padding, + this.borderColor, }); final void Function()? onPressed; final Widget child; @@ -31,6 +32,8 @@ class DefaultButton extends StatelessWidget { final ButtonStyle? customButtonStyle; final Color? backgroundColor; final Color? foregroundColor; + final Color? borderColor; + @override Widget build(BuildContext context) { return ElevatedButton( @@ -61,6 +64,7 @@ class DefaultButton extends StatelessWidget { }), shape: MaterialStateProperty.all( RoundedRectangleBorder( + side: BorderSide(color: borderColor ?? Colors.transparent), borderRadius: BorderRadius.circular(borderRadius ?? 20), ), ), diff --git a/lib/pages/device_managment/water_heater/widgets/schedual_view.dart b/lib/pages/device_managment/water_heater/widgets/schedual_view.dart index 74cdb94b..125a9483 100644 --- a/lib/pages/device_managment/water_heater/widgets/schedual_view.dart +++ b/lib/pages/device_managment/water_heater/widgets/schedual_view.dart @@ -163,9 +163,27 @@ class _BuildScheduleViewState extends State { return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - ElevatedButton( - onPressed: () => _showAddScheduleDialog(context), - child: Text('+ Add new schedule'), + SizedBox( + width: 170, + height: 40, + child: DefaultButton( + borderColor: ColorsManager.boxColor, + padding: 2, + backgroundColor: ColorsManager.graysColor, + borderRadius: 15, + onPressed: () => _showAddScheduleDialog(context), + child: Row( + children: [ + const Icon(Icons.add, color: ColorsManager.primaryColor), + Text( + ' Add new schedule', + style: context.textTheme.bodySmall!.copyWith( + color: ColorsManager.blackColor, + ), + ), + ], + ), + ), ), const SizedBox(height: 20), _buildScheduleTable(state), @@ -219,8 +237,8 @@ class _BuildScheduleViewState extends State { children: [ Center( child: schedule.functionOn - ? Icon(Icons.radio_button_checked) - : Icon(Icons.radio_button_unchecked)), + ? const Icon(Icons.radio_button_checked) + : const Icon(Icons.radio_button_unchecked)), Center(child: Text(_getSelectedDays(schedule.selectedDays))), Center(child: Text(schedule.time.format(context))), Center(child: Text(schedule.functionOn ? 'On' : 'Off')), @@ -230,7 +248,8 @@ class _BuildScheduleViewState extends State { children: [ TextButton( onPressed: () { - _showEditScheduleDialog(context, schedule, index); + _showAddScheduleDialog(context); + // _showEditScheduleDialog(context, schedule, index); }, child: Text( 'Edit', @@ -298,7 +317,6 @@ class _BuildScheduleViewState extends State { }, child: Text(selectedTime.format(context)), ), - // Same checkboxes and function on/off logic as before ], ), actions: [ @@ -340,7 +358,8 @@ class _BuildScheduleViewState extends State { ); } - void _showAddScheduleDialog(BuildContext context) { + void _showAddScheduleDialog( + BuildContext context) { showDialog( context: context, builder: (ctx) { @@ -352,10 +371,10 @@ class _BuildScheduleViewState extends State { false, false, false - ]; // Mon - Sun + ]; + TimeOfDay? selectedTime; bool isOn = false; - return BlocProvider.value( value: BlocProvider.of(context), child: StatefulBuilder( @@ -363,44 +382,88 @@ class _BuildScheduleViewState extends State { return AlertDialog( shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(20)), - title: const Text('Scheduling'), content: Column( mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, children: [ - ElevatedButton( - onPressed: () async { - TimeOfDay? time = await showTimePicker( - context: context, - initialTime: TimeOfDay.now(), - ); - if (time != null) { - setState(() { - selectedTime = time; - }); - } - }, - child: Text( - selectedTime == null - ? 'Time' - : '${selectedTime!.format(context)}', - ), - ), - const SizedBox(height: 10), Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ - _buildDayCheckbox(setState, 'Mon', 0, selectedDays), - _buildDayCheckbox(setState, 'Tue', 1, selectedDays), - _buildDayCheckbox(setState, 'Wed', 2, selectedDays), - _buildDayCheckbox(setState, 'Thu', 3, selectedDays), - _buildDayCheckbox(setState, 'Fri', 4, selectedDays), - _buildDayCheckbox(setState, 'Sat', 5, selectedDays), - _buildDayCheckbox(setState, 'Sun', 6, selectedDays), + const SizedBox(), + Text( + 'Scheduling', + style: context.textTheme.titleLarge!.copyWith( + color: ColorsManager.dialogBlueTitle, + fontWeight: FontWeight.bold, + ), + ), + const SizedBox(), ], ), - const SizedBox(height: 10), + const SizedBox( + height: 24, + ), + SizedBox( + width: 150, + height: 40, + child: DefaultButton( + padding: 8, + backgroundColor: ColorsManager.boxColor, + borderRadius: 15, + onPressed: () async { + TimeOfDay? time = await showTimePicker( + context: context, + initialTime: TimeOfDay.now(), + ); + if (time != null) { + setState(() { + selectedTime = time; + }); + } + }, + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + selectedTime == null + ? 'Time' + : selectedTime!.format(context), + style: context.textTheme.bodySmall!.copyWith( + color: ColorsManager.grayColor, + ), + ), + const Icon( + Icons.access_time, + color: ColorsManager.grayColor, + size: 18, + ), + ], + ), + ), + ), + const SizedBox(height: 16), Row( children: [ - const Text('Function:'), + _buildDayCheckbox(setState, 'Mon.', 0, selectedDays), + _buildDayCheckbox(setState, 'Tue.', 1, selectedDays), + _buildDayCheckbox(setState, 'Wed.', 2, selectedDays), + _buildDayCheckbox(setState, 'Thu.', 3, selectedDays), + _buildDayCheckbox(setState, 'Fri.', 4, selectedDays), + _buildDayCheckbox(setState, 'Sat.', 5, selectedDays), + _buildDayCheckbox(setState, 'Sun.', 6, selectedDays), + ], + ), + const SizedBox(height: 16), + Row( + children: [ + Text( + 'Function:', + style: context.textTheme.bodySmall! + .copyWith(color: ColorsManager.grayColor), + ), + const SizedBox( + width: 10, + ), Radio( value: true, groupValue: isOn, @@ -411,6 +474,9 @@ class _BuildScheduleViewState extends State { }, ), const Text('On'), + const SizedBox( + width: 10, + ), Radio( value: false, groupValue: isOn, @@ -426,25 +492,37 @@ class _BuildScheduleViewState extends State { ], ), actions: [ - TextButton( - onPressed: () { - Navigator.pop(context); - }, - child: const Text('Cancel'), + SizedBox( + width: 200, + child: DefaultButton( + height: 40, + onPressed: () { + Navigator.pop(context); + }, + backgroundColor: ColorsManager.boxColor, + child: Text( + 'Cancel', + style: context.textTheme.bodyMedium, + ), + ), ), - TextButton( - onPressed: () { - // Dispatch Bloc Event to Add Schedule - if (selectedTime != null) { - context.read().add(AddScheduleEvent( - time: selectedTime!, - selectedDays: selectedDays, - functionOn: isOn, - )); - Navigator.pop(context); // Close the dialog - } - }, - child: const Text('Save'), + SizedBox( + width: 200, + child: DefaultButton( + height: 40, + onPressed: () { + if (selectedTime != null) { + context.read().add(AddScheduleEvent( + time: selectedTime!, + selectedDays: selectedDays, + functionOn: isOn, + )); + Navigator.pop(context); + } + }, + backgroundColor: ColorsManager.primaryColor, + child: const Text('Save'), + ), ), ], ); @@ -556,21 +634,31 @@ class _BuildScheduleViewState extends State { List _buildCountDownAngInchingView( BuildContext context, WaterHeaterDeviceStatusLoaded state) { + final isCountDown = + state.scheduleMode?.name == ScheduleModes.countdown.name; return [ Text( - 'Countdown:', + isCountDown ? 'Countdown:' : 'Inching:', style: context.textTheme.bodySmall!.copyWith( fontSize: 13, color: ColorsManager.grayColor, ), ), - const SizedBox(height: 4), + const SizedBox(height: 8), + Visibility( + visible: !isCountDown, + child: const Text( + 'Once enabled this feature, each time the device is turned on, it will automatically turn of after a period time as pre-set.'), + ), + const SizedBox(height: 8), _hourMinutesWheel(state, context) ]; } Row _hourMinutesWheel( WaterHeaterDeviceStatusLoaded state, BuildContext context) { + final isActive = + (state.countdownRemaining != null && state.isActive == true); return Row( mainAxisAlignment: MainAxisAlignment.start, children: [ @@ -580,7 +668,7 @@ class _BuildScheduleViewState extends State { hours: value, minutes: state.minutes ?? 0, )); - }), + }, isActive: isActive), const SizedBox(width: 10), _buildPickerColumn(context, 'm', state.minutes ?? 0, 60, (value) { context.read().add(UpdateScheduleEvent( @@ -588,18 +676,19 @@ class _BuildScheduleViewState extends State { hours: state.hours ?? 0, minutes: value, )); - }), + }, isActive: isActive), ], ); } Widget _buildPickerColumn(BuildContext context, String label, - int initialValue, int itemCount, ValueChanged onSelected) { + int initialValue, int itemCount, ValueChanged onSelected, + {required bool isActive}) { return Row( mainAxisSize: MainAxisSize.min, children: [ Container( - height: 50, + height: 40, width: 80, padding: const EdgeInsets.symmetric(horizontal: 16), decoration: BoxDecoration( @@ -619,7 +708,10 @@ class _BuildScheduleViewState extends State { return Center( child: Text( index.toString().padLeft(2, '0'), - style: const TextStyle(fontSize: 24), + style: TextStyle( + fontSize: 24, + color: isActive ? ColorsManager.grayColor : Colors.black, + ), ), ); }, @@ -627,7 +719,7 @@ class _BuildScheduleViewState extends State { ), ), ), - const SizedBox(height: 8), + const SizedBox(width: 8), Text( label, style: const TextStyle( diff --git a/lib/utils/theme/theme.dart b/lib/utils/theme/theme.dart index ee868c8d..413f3243 100644 --- a/lib/utils/theme/theme.dart +++ b/lib/utils/theme/theme.dart @@ -5,9 +5,10 @@ final myTheme = ThemeData( fontFamily: 'Aftika', textTheme: const TextTheme( bodySmall: TextStyle( - fontSize: 13, - color: ColorsManager.whiteColors, - fontWeight: FontWeight.bold), + fontSize: 13, + color: ColorsManager.whiteColors, + fontWeight: FontWeight.normal, + ), bodyMedium: TextStyle(color: Colors.black87, fontSize: 14), bodyLarge: TextStyle(fontSize: 16, color: Colors.white), headlineSmall: TextStyle(color: Colors.black87, fontSize: 18), From f99744338c02a7b5986ae75b7099c7650450a3f9 Mon Sep 17 00:00:00 2001 From: ashrafzarkanisala Date: Sun, 22 Sep 2024 14:17:52 +0300 Subject: [PATCH 04/10] connecting apis and changes the table design to match figma --- assets/icons/empty_records.svg | 15 + .../water_heater/bloc/water_heater_bloc.dart | 141 ++++- .../water_heater/bloc/water_heater_event.dart | 60 +- .../water_heater/bloc/water_heater_state.dart | 38 +- .../water_heater/models/schedule_entry.dart | 19 + .../water_heater/models/send_schedule.dart | 93 +++ .../water_heater/widgets/schedual_view.dart | 528 +++++++++--------- lib/services/devices_mang_api.dart | 60 +- lib/utils/constants/api_const.dart | 6 + lib/utils/constants/assets.dart | 3 + lib/utils/format_date_time.dart | 15 + 11 files changed, 674 insertions(+), 304 deletions(-) create mode 100644 assets/icons/empty_records.svg create mode 100644 lib/pages/device_managment/water_heater/models/schedule_entry.dart create mode 100644 lib/pages/device_managment/water_heater/models/send_schedule.dart diff --git a/assets/icons/empty_records.svg b/assets/icons/empty_records.svg new file mode 100644 index 00000000..662a3e47 --- /dev/null +++ b/assets/icons/empty_records.svg @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + diff --git a/lib/pages/device_managment/water_heater/bloc/water_heater_bloc.dart b/lib/pages/device_managment/water_heater/bloc/water_heater_bloc.dart index fcf52732..07f73569 100644 --- a/lib/pages/device_managment/water_heater/bloc/water_heater_bloc.dart +++ b/lib/pages/device_managment/water_heater/bloc/water_heater_bloc.dart @@ -5,8 +5,11 @@ import 'package:bloc/bloc.dart'; import 'package:equatable/equatable.dart'; import 'package:flutter/material.dart'; import 'package:syncrow_web/pages/device_managment/all_devices/models/device_status.dart'; +import 'package:syncrow_web/pages/device_managment/water_heater/models/schedule_entry.dart'; +import 'package:syncrow_web/pages/device_managment/water_heater/models/send_schedule.dart'; import 'package:syncrow_web/pages/device_managment/water_heater/models/water_heater_status_model.dart'; import 'package:syncrow_web/services/devices_mang_api.dart'; +import 'package:syncrow_web/utils/format_date_time.dart'; part 'water_heater_event.dart'; part 'water_heater_state.dart'; @@ -21,11 +24,63 @@ class WaterHeaterBloc extends Bloc { on(_onAddSchedule); on(_onDeleteSchedule); on(_onUpdateSchedule); + on(_initializeAddSchedule); + on(_updateSelectedTime); + on(_updateSelectedDay); + on(_updateFunctionOn); } late WaterHeaterStatusModel deviceStatus; Timer? _countdownTimer; + FutureOr _initializeAddSchedule( + InitializeAddScheduleEvent event, + Emitter emit, + ) { + final currentState = state as WaterHeaterDeviceStatusLoaded; + if (event.isEditing) { + emit(currentState.copyWith( + selectedTime: event.selectedTime, + selectedDays: event.selectedDays ?? List.filled(7, false), + functionOn: event.functionOn ?? false, + isEditing: event.isEditing, + )); + } else { + emit(currentState.copyWith( + selectedTime: null, + selectedDays: List.filled(7, false), + functionOn: false, + isEditing: false, + )); + } + } + + FutureOr _updateSelectedTime( + UpdateSelectedTimeEvent event, + Emitter emit, + ) { + final currentState = state as WaterHeaterDeviceStatusLoaded; + emit(currentState.copyWith(selectedTime: event.selectedTime)); + } + + FutureOr _updateSelectedDay( + UpdateSelectedDayEvent event, + Emitter emit, + ) { + final currentState = state as WaterHeaterDeviceStatusLoaded; + final updatedDays = List.from(currentState.selectedDays); + updatedDays[event.index] = event.value; + emit(currentState.copyWith(selectedDays: updatedDays)); + } + + FutureOr _updateFunctionOn( + UpdateFunctionOnEvent event, + Emitter emit, + ) { + final currentState = state as WaterHeaterDeviceStatusLoaded; + emit(currentState.copyWith(functionOn: event.isOn)); + } + FutureOr _updateScheduleEvent( UpdateScheduleEvent event, Emitter emit, @@ -301,21 +356,50 @@ class WaterHeaterBloc extends Bloc { FutureOr _onAddSchedule( AddScheduleEvent event, Emitter emit, - ) { + ) async { if (state is WaterHeaterDeviceStatusLoaded) { final currentState = state as WaterHeaterDeviceStatusLoaded; - final newSchedule = ScheduleEntry( - selectedDays: event.selectedDays, - time: event.time, - functionOn: event.functionOn, - ); - final updatedSchedules = List.from(currentState.schedules) - ..add(newSchedule); - emit(currentState.copyWith(schedules: updatedSchedules)); + SendSchedule sendSchedule = SendSchedule( + category: event.category, + time: formatTimeOfDayToISO(event.time), + function: Status(code: 'switch_1', value: event.functionOn), + days: _getSelectedDaysString(event.selectedDays), + ); + + bool success = await DevicesManagementApi() + .addScheduleRecord(sendSchedule, currentState.status.uuid); + + if (success) { + final newSchedule = ScheduleEntry( + selectedDays: event.selectedDays, + time: event.time, + functionOn: event.functionOn, + category: event.category, + ); + + final updatedSchedules = + List.from(currentState.schedules)..add(newSchedule); + + emit(currentState.copyWith(schedules: updatedSchedules)); + } else { + emit(const WaterHeaterFailedState( + error: 'Failed to add schedule. Please try again.')); + } } } + List _getSelectedDaysString(List selectedDays) { + final days = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat']; + List selectedDaysStr = []; + for (int i = 0; i < selectedDays.length; i++) { + if (selectedDays[i]) { + selectedDaysStr.add(days[i]); + } + } + return selectedDaysStr; + } + FutureOr _onDeleteSchedule( DeleteScheduleEvent event, Emitter emit, @@ -332,17 +416,42 @@ class WaterHeaterBloc extends Bloc { FutureOr _onUpdateSchedule( UpdateScheduleEntryEvent event, Emitter emit, - ) { + ) async { if (state is WaterHeaterDeviceStatusLoaded) { final currentState = state as WaterHeaterDeviceStatusLoaded; - final updatedSchedules = List.from(currentState.schedules); - updatedSchedules[event.index] = ScheduleEntry( - selectedDays: event.selectedDays, - time: event.time, - functionOn: event.functionOn, + + // Get the current schedule ID or UUID (assuming it's stored in the schedules) + String scheduleId = + ""; // Retrieve the actual schedule ID based on your model + + // Call the API to update the schedule + bool success = await DevicesManagementApi().updateScheduleRecord( + enable: event.functionOn, + uuid: event.deviceId, + scheduleId: scheduleId, ); - emit(currentState.copyWith(schedules: updatedSchedules)); + if (success) { + final updatedSchedules = + List.from(currentState.schedules); + + final updatedScheduleIndex = updatedSchedules.indexWhere((schedule) { + return schedule.category == event.category; + }); + + if (updatedScheduleIndex != -1) { + updatedSchedules[updatedScheduleIndex] = ScheduleEntry( + category: event.category, + selectedDays: updatedSchedules[updatedScheduleIndex].selectedDays, + time: updatedSchedules[updatedScheduleIndex].time, + functionOn: event.functionOn, + ); + emit(currentState.copyWith(schedules: updatedSchedules)); + } + } else { + emit(const WaterHeaterFailedState( + error: 'Failed to update schedule. Please try again.')); + } } } diff --git a/lib/pages/device_managment/water_heater/bloc/water_heater_event.dart b/lib/pages/device_managment/water_heater/bloc/water_heater_event.dart index 360f4a4f..0c9cd794 100644 --- a/lib/pages/device_managment/water_heater/bloc/water_heater_event.dart +++ b/lib/pages/device_managment/water_heater/bloc/water_heater_event.dart @@ -67,15 +67,17 @@ final class AddScheduleEvent extends WaterHeaterEvent { final List selectedDays; final TimeOfDay time; final bool functionOn; + final String category; const AddScheduleEvent({ required this.selectedDays, required this.time, required this.functionOn, + required this.category, }); @override - List get props => [selectedDays, time, functionOn]; + List get props => [selectedDays, time, functionOn, category]; } final class DeleteScheduleEvent extends WaterHeaterEvent { @@ -88,18 +90,60 @@ final class DeleteScheduleEvent extends WaterHeaterEvent { } final class UpdateScheduleEntryEvent extends WaterHeaterEvent { - final int index; - final List selectedDays; - final TimeOfDay time; final bool functionOn; + final String category; + final String deviceId; const UpdateScheduleEntryEvent({ - required this.index, - required this.selectedDays, - required this.time, required this.functionOn, + required this.category, + required this.deviceId, }); @override - List get props => [index, selectedDays, time, functionOn]; + List get props => [category, functionOn, deviceId]; +} + +class InitializeAddScheduleEvent extends WaterHeaterEvent { + final TimeOfDay? selectedTime; + final List? selectedDays; + final bool? functionOn; + final bool isEditing; + final int? index; + + const InitializeAddScheduleEvent({ + this.selectedTime, + this.selectedDays, + this.functionOn, + this.isEditing = false, + this.index, + }); +} + +class UpdateSelectedTimeEvent extends WaterHeaterEvent { + final TimeOfDay selectedTime; + + const UpdateSelectedTimeEvent(this.selectedTime); + + @override + List get props => [selectedTime]; +} + +class UpdateSelectedDayEvent extends WaterHeaterEvent { + final int index; + final bool value; + + const UpdateSelectedDayEvent(this.index, this.value); + + @override + List get props => [index, value]; +} + +class UpdateFunctionOnEvent extends WaterHeaterEvent { + final bool isOn; + + const UpdateFunctionOnEvent(this.isOn); + + @override + List get props => [isOn]; } diff --git a/lib/pages/device_managment/water_heater/bloc/water_heater_state.dart b/lib/pages/device_managment/water_heater/bloc/water_heater_state.dart index 95c68f24..1054104f 100644 --- a/lib/pages/device_managment/water_heater/bloc/water_heater_state.dart +++ b/lib/pages/device_managment/water_heater/bloc/water_heater_state.dart @@ -13,7 +13,7 @@ final class WaterHeaterInitial extends WaterHeaterState {} final class WaterHeaterLoadingState extends WaterHeaterState {} -final class WaterHeaterDeviceStatusLoaded extends WaterHeaterState { +class WaterHeaterDeviceStatusLoaded extends WaterHeaterState { final WaterHeaterStatusModel status; final ScheduleModes? scheduleMode; final int? hours; @@ -21,6 +21,10 @@ final class WaterHeaterDeviceStatusLoaded extends WaterHeaterState { final bool? isActive; final Duration? countdownRemaining; final List schedules; + final List selectedDays; + final TimeOfDay? selectedTime; + final bool functionOn; + final bool isEditing; const WaterHeaterDeviceStatusLoaded( this.status, { @@ -30,6 +34,10 @@ final class WaterHeaterDeviceStatusLoaded extends WaterHeaterState { this.isActive, this.countdownRemaining, this.schedules = const [], + this.selectedDays = const [false, false, false, false, false, false, false], + this.selectedTime, + this.functionOn = false, + this.isEditing = false, }); @override @@ -41,6 +49,10 @@ final class WaterHeaterDeviceStatusLoaded extends WaterHeaterState { isActive, countdownRemaining, schedules, + selectedDays, + selectedTime, + functionOn, + isEditing ]; WaterHeaterDeviceStatusLoaded copyWith({ @@ -51,6 +63,10 @@ final class WaterHeaterDeviceStatusLoaded extends WaterHeaterState { bool? isActive, Duration? countdownRemaining, List? schedules, + List? selectedDays, + TimeOfDay? selectedTime, + bool? functionOn, + bool? isEditing, }) { return WaterHeaterDeviceStatusLoaded( status ?? this.status, @@ -60,26 +76,14 @@ final class WaterHeaterDeviceStatusLoaded extends WaterHeaterState { isActive: isActive ?? this.isActive, countdownRemaining: countdownRemaining ?? this.countdownRemaining, schedules: schedules ?? this.schedules, + selectedDays: selectedDays ?? this.selectedDays, + selectedTime: selectedTime ?? this.selectedTime, + functionOn: functionOn ?? this.functionOn, + isEditing: isEditing ?? this.isEditing, ); } } -class ScheduleEntry { - final List selectedDays; - final TimeOfDay time; - final bool functionOn; - - ScheduleEntry({ - required this.selectedDays, - required this.time, - required this.functionOn, - }); - - @override - String toString() => - 'ScheduleEntry(selectedDays: $selectedDays, time: $time, functionOn: $functionOn)'; -} - final class WaterHeaterFailedState extends WaterHeaterState { final String error; diff --git a/lib/pages/device_managment/water_heater/models/schedule_entry.dart b/lib/pages/device_managment/water_heater/models/schedule_entry.dart new file mode 100644 index 00000000..2d02b617 --- /dev/null +++ b/lib/pages/device_managment/water_heater/models/schedule_entry.dart @@ -0,0 +1,19 @@ +import 'package:flutter/material.dart'; + +class ScheduleEntry { + final List selectedDays; + final TimeOfDay time; + final bool functionOn; + final String category; + + ScheduleEntry({ + required this.selectedDays, + required this.time, + required this.functionOn, + required this.category, + }); + + @override + String toString() => + 'ScheduleEntry(selectedDays: $selectedDays, time: $time, functionOn: $functionOn)'; +} \ No newline at end of file diff --git a/lib/pages/device_managment/water_heater/models/send_schedule.dart b/lib/pages/device_managment/water_heater/models/send_schedule.dart new file mode 100644 index 00000000..650f3a04 --- /dev/null +++ b/lib/pages/device_managment/water_heater/models/send_schedule.dart @@ -0,0 +1,93 @@ +import 'dart:convert'; + +import 'package:flutter/foundation.dart'; +import 'package:syncrow_web/pages/device_managment/all_devices/models/device_status.dart'; + +/* +{ + "category": "kg", + "time": "2024-09-22T10:31:54Z", + "function": { + "code": "switch_1", + "value": true + }, + "days": [ + "Sun" + ] +} +*/ + +class SendSchedule { + final String category; + final String time; + final Status function; + final List days; + + SendSchedule({ + required this.category, + required this.time, + required this.function, + required this.days, + }); + + SendSchedule copyWith({ + String? category, + String? time, + Status? function, + List? days, + }) { + return SendSchedule( + category: category ?? this.category, + time: time ?? this.time, + function: function ?? this.function, + days: days ?? this.days, + ); + } + + Map toMap() { + return { + 'category': category, + 'time': time, + 'function': function.toMap(), + 'days': days, + }; + } + + factory SendSchedule.fromMap(Map map) { + return SendSchedule( + category: map['category'] ?? '', + time: map['time'] ?? '', + function: Status.fromMap(map['function']), + days: List.from(map['days']), + ); + } + + String toJson() => json.encode(toMap()); + + factory SendSchedule.fromJson(String source) => + SendSchedule.fromMap(json.decode(source)); + + @override + String toString() { + return 'SendSchedule(category: $category, time: $time, function: $function, days: $days)'; + } + + @override + bool operator ==(Object other) { + if (identical(this, other)) return true; + + return other is SendSchedule && + other.category == category && + other.time == time && + other.function == function && + listEquals(other.days, days); + } + + @override + int get hashCode { + return category.hashCode ^ + time.hashCode ^ + function.hashCode ^ + days.hashCode; + } +} diff --git a/lib/pages/device_managment/water_heater/widgets/schedual_view.dart b/lib/pages/device_managment/water_heater/widgets/schedual_view.dart index 125a9483..7ef86bac 100644 --- a/lib/pages/device_managment/water_heater/widgets/schedual_view.dart +++ b/lib/pages/device_managment/water_heater/widgets/schedual_view.dart @@ -1,9 +1,12 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:flutter_svg/flutter_svg.dart'; import 'package:syncrow_web/pages/common/buttons/default_button.dart'; import 'package:syncrow_web/pages/device_managment/water_heater/bloc/water_heater_bloc.dart'; +import 'package:syncrow_web/pages/device_managment/water_heater/models/schedule_entry.dart'; import 'package:syncrow_web/pages/device_managment/water_heater/models/water_heater_status_model.dart'; import 'package:syncrow_web/utils/color_manager.dart'; +import 'package:syncrow_web/utils/constants/assets.dart'; import 'package:syncrow_web/utils/extension/build_context_x.dart'; class BuildScheduleView extends StatefulWidget { @@ -171,7 +174,8 @@ class _BuildScheduleViewState extends State { padding: 2, backgroundColor: ColorsManager.graysColor, borderRadius: 15, - onPressed: () => _showAddScheduleDialog(context), + onPressed: () => + _showAddScheduleDialog(context, schedule: null, index: null), child: Row( children: [ const Icon(Icons.add, color: ColorsManager.primaryColor), @@ -192,52 +196,92 @@ class _BuildScheduleViewState extends State { } Widget _buildScheduleTable(WaterHeaterDeviceStatusLoaded state) { - return Table( - border: TableBorder.all(color: Colors.grey), + return Column( children: [ - TableRow( + Table( + border: TableBorder.all( + color: ColorsManager.graysColor, + borderRadius: const BorderRadius.only( + topLeft: Radius.circular(20), topRight: Radius.circular(20)), + ), children: [ - _buildTableHeader('Active'), - _buildTableHeader('Days'), - _buildTableHeader('Time'), - _buildTableHeader('Function'), - _buildTableHeader('Action'), - ], - ), - if (state.schedules.isEmpty) - TableRow( - children: [ - Center( - child: Padding( - padding: const EdgeInsets.all(8.0), - child: Text( - 'No schedules added yet', - style: context.textTheme.bodySmall!.copyWith( - fontSize: 13, - color: ColorsManager.grayColor, - ), - ), + TableRow( + decoration: const BoxDecoration( + color: ColorsManager.boxColor, + borderRadius: BorderRadius.only( + topLeft: Radius.circular(20), + topRight: Radius.circular(20), ), ), - const SizedBox(), - const SizedBox(), - const SizedBox(), - const SizedBox(), - ], + children: [ + _buildTableHeader('Active'), + _buildTableHeader('Days'), + _buildTableHeader('Time'), + _buildTableHeader('Function'), + _buildTableHeader('Action'), + ], + ), + ], + ), + Container( + height: 200, + decoration: BoxDecoration( + border: Border.all(color: ColorsManager.graysColor), + borderRadius: const BorderRadius.only( + bottomLeft: Radius.circular(20), + bottomRight: Radius.circular(20)), ), - for (int i = 0; i < state.schedules.length; i++) - _buildScheduleRow(state.schedules[i], i, context), + child: state.schedules.isEmpty + ? _buildEmptyState(context) + : _buildTableBody(state), + ), ], ); } + Widget _buildEmptyState(BuildContext context) { + return Center( + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + SvgPicture.asset(Assets.emptyRecords, width: 40, height: 40), + const SizedBox(height: 8), + Padding( + padding: const EdgeInsets.all(8.0), + child: Text( + 'No schedules added yet', + style: context.textTheme.bodySmall!.copyWith( + fontSize: 13, + color: ColorsManager.grayColor, + ), + ), + ), + ], + ), + ); + } + + Widget _buildTableBody(WaterHeaterDeviceStatusLoaded state) { + return SingleChildScrollView( + child: Table( + border: TableBorder.all(color: ColorsManager.graysColor), + defaultVerticalAlignment: TableCellVerticalAlignment.middle, + children: [ + for (int i = 0; i < state.schedules.length; i++) + _buildScheduleRow(state.schedules[i], i, context), + ], + ), + ); + } + TableRow _buildScheduleRow( ScheduleEntry schedule, int index, BuildContext context) { return TableRow( children: [ Center( child: schedule.functionOn - ? const Icon(Icons.radio_button_checked) + ? const Icon(Icons.radio_button_checked, + color: ColorsManager.blueColor) : const Icon(Icons.radio_button_unchecked)), Center(child: Text(_getSelectedDays(schedule.selectedDays))), Center(child: Text(schedule.time.format(context))), @@ -247,9 +291,10 @@ class _BuildScheduleViewState extends State { runAlignment: WrapAlignment.center, children: [ TextButton( + style: TextButton.styleFrom(padding: EdgeInsets.zero), onPressed: () { - _showAddScheduleDialog(context); - // _showEditScheduleDialog(context, schedule, index); + _showAddScheduleDialog(context, + schedule: schedule, index: index); }, child: Text( 'Edit', @@ -258,6 +303,7 @@ class _BuildScheduleViewState extends State { ), ), TextButton( + style: TextButton.styleFrom(padding: EdgeInsets.zero), onPressed: () { context .read() @@ -287,245 +333,165 @@ class _BuildScheduleViewState extends State { return selectedDaysStr.join(', '); } - void _showEditScheduleDialog( - BuildContext context, ScheduleEntry schedule, int index) { - List selectedDays = List.from(schedule.selectedDays); - TimeOfDay selectedTime = schedule.time; - bool isOn = schedule.functionOn; - - showDialog( - context: context, - builder: (ctx) { - return StatefulBuilder( - builder: (context, setState) { - return AlertDialog( - title: const Text('Edit Schedule'), - content: Column( - mainAxisSize: MainAxisSize.min, - children: [ - ElevatedButton( - onPressed: () async { - TimeOfDay? time = await showTimePicker( - context: context, - initialTime: selectedTime, - ); - if (time != null) { - setState(() { - selectedTime = time; - }); - } - }, - child: Text(selectedTime.format(context)), - ), - ], - ), - actions: [ - TextButton( - onPressed: () { - Navigator.pop(context); - }, - child: const Text('Cancel'), - ), - TextButton( - onPressed: () { - context.read().add( - UpdateScheduleEntryEvent( - index: index, - selectedDays: selectedDays, - time: selectedTime, - functionOn: isOn, - ), - ); - Navigator.pop(context); - }, - child: const Text('Save'), - ), - ], - ); - }, - ); - }, - ); - } - Widget _buildTableHeader(String label) { - return Padding( - padding: const EdgeInsets.all(8.0), - child: Text( - label, - style: const TextStyle(fontWeight: FontWeight.bold), + return TableCell( + child: Padding( + padding: const EdgeInsets.all(12), + child: Text( + label, + style: context.textTheme.bodySmall!.copyWith( + fontSize: 13, + color: ColorsManager.grayColor, + ), + ), ), ); } - void _showAddScheduleDialog( - BuildContext context) { + void _showAddScheduleDialog(BuildContext context, + {ScheduleEntry? schedule, int? index}) { + final bloc = context.read(); + + if (schedule != null) { + bloc.add(InitializeAddScheduleEvent( + selectedTime: schedule.time, + selectedDays: schedule.selectedDays, + functionOn: schedule.functionOn, + isEditing: true, + index: index, + )); + } else { + bloc.add( + const InitializeAddScheduleEvent( + selectedDays: [false, false, false, false, false, false, false], + functionOn: false, + isEditing: false, + index: null, + selectedTime: null, + ), + ); + } + showDialog( context: context, builder: (ctx) { - List selectedDays = [ - false, - false, - false, - false, - false, - false, - false - ]; - - TimeOfDay? selectedTime; - bool isOn = false; return BlocProvider.value( - value: BlocProvider.of(context), - child: StatefulBuilder( - builder: (ctx, setState) { - return AlertDialog( - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(20)), - content: Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - const SizedBox(), - Text( - 'Scheduling', - style: context.textTheme.titleLarge!.copyWith( - color: ColorsManager.dialogBlueTitle, - fontWeight: FontWeight.bold, + value: bloc, + child: BlocBuilder( + builder: (context, state) { + if (state is WaterHeaterDeviceStatusLoaded) { + return AlertDialog( + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(20), + ), + content: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + const SizedBox(), + Text( + 'Scheduling', + style: context.textTheme.titleLarge!.copyWith( + color: ColorsManager.dialogBlueTitle, + fontWeight: FontWeight.bold, + ), + ), + const SizedBox(), + ], + ), + const SizedBox(height: 24), + SizedBox( + width: 150, + height: 40, + child: DefaultButton( + padding: 8, + backgroundColor: ColorsManager.boxColor, + borderRadius: 15, + onPressed: () async { + TimeOfDay? time = await showTimePicker( + context: context, + initialTime: + state.selectedTime ?? TimeOfDay.now(), + ); + if (time != null) { + bloc.add(UpdateSelectedTimeEvent(time)); + } + }, + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + state.selectedTime == null + ? 'Time' + : state.selectedTime!.format(context), + style: context.textTheme.bodySmall!.copyWith( + color: ColorsManager.grayColor, + ), + ), + const Icon( + Icons.access_time, + color: ColorsManager.grayColor, + size: 18, + ), + ], ), ), - const SizedBox(), - ], - ), - const SizedBox( - height: 24, + ), + const SizedBox(height: 16), + _buildDayCheckboxes(context, state.selectedDays), + const SizedBox(height: 16), + _buildFunctionSwitch(context, state.functionOn), + ], + ), + actions: [ + SizedBox( + width: 200, + child: DefaultButton( + height: 40, + onPressed: () { + Navigator.pop(context); + }, + backgroundColor: ColorsManager.boxColor, + child: Text( + 'Cancel', + style: context.textTheme.bodyMedium, + ), + ), ), SizedBox( - width: 150, - height: 40, + width: 200, child: DefaultButton( - padding: 8, - backgroundColor: ColorsManager.boxColor, - borderRadius: 15, - onPressed: () async { - TimeOfDay? time = await showTimePicker( - context: context, - initialTime: TimeOfDay.now(), - ); - if (time != null) { - setState(() { - selectedTime = time; - }); + height: 40, + onPressed: () { + if (state.selectedTime != null) { + if (state.isEditing && index != null) { + bloc.add(UpdateScheduleEntryEvent( + deviceId: state.status.uuid, + category: 'kg', + functionOn: state.functionOn, + )); + } else { + bloc.add(AddScheduleEvent( + category: 'kg', + time: state.selectedTime!, + selectedDays: state.selectedDays, + functionOn: state.functionOn, + )); + } + Navigator.pop(context); } }, - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Text( - selectedTime == null - ? 'Time' - : selectedTime!.format(context), - style: context.textTheme.bodySmall!.copyWith( - color: ColorsManager.grayColor, - ), - ), - const Icon( - Icons.access_time, - color: ColorsManager.grayColor, - size: 18, - ), - ], - ), + backgroundColor: ColorsManager.primaryColor, + child: const Text('Save'), ), ), - const SizedBox(height: 16), - Row( - children: [ - _buildDayCheckbox(setState, 'Mon.', 0, selectedDays), - _buildDayCheckbox(setState, 'Tue.', 1, selectedDays), - _buildDayCheckbox(setState, 'Wed.', 2, selectedDays), - _buildDayCheckbox(setState, 'Thu.', 3, selectedDays), - _buildDayCheckbox(setState, 'Fri.', 4, selectedDays), - _buildDayCheckbox(setState, 'Sat.', 5, selectedDays), - _buildDayCheckbox(setState, 'Sun.', 6, selectedDays), - ], - ), - const SizedBox(height: 16), - Row( - children: [ - Text( - 'Function:', - style: context.textTheme.bodySmall! - .copyWith(color: ColorsManager.grayColor), - ), - const SizedBox( - width: 10, - ), - Radio( - value: true, - groupValue: isOn, - onChanged: (bool? value) { - setState(() { - isOn = value!; - }); - }, - ), - const Text('On'), - const SizedBox( - width: 10, - ), - Radio( - value: false, - groupValue: isOn, - onChanged: (bool? value) { - setState(() { - isOn = value!; - }); - }, - ), - const Text('Off'), - ], - ), ], - ), - actions: [ - SizedBox( - width: 200, - child: DefaultButton( - height: 40, - onPressed: () { - Navigator.pop(context); - }, - backgroundColor: ColorsManager.boxColor, - child: Text( - 'Cancel', - style: context.textTheme.bodyMedium, - ), - ), - ), - SizedBox( - width: 200, - child: DefaultButton( - height: 40, - onPressed: () { - if (selectedTime != null) { - context.read().add(AddScheduleEvent( - time: selectedTime!, - selectedDays: selectedDays, - functionOn: isOn, - )); - Navigator.pop(context); - } - }, - backgroundColor: ColorsManager.primaryColor, - child: const Text('Save'), - ), - ), - ], - ); + ); + } + return const SizedBox(); }, ), ); @@ -533,19 +499,57 @@ class _BuildScheduleViewState extends State { ); } - Widget _buildDayCheckbox(void Function(void Function()) setState, String day, - int index, List selectedDays) { + Widget _buildDayCheckboxes(BuildContext context, List selectedDays) { + return Row( + children: List.generate(7, (index) { + final dayLabels = ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun']; + return Row( + children: [ + Checkbox( + value: selectedDays[index], + onChanged: (bool? value) { + context + .read() + .add(UpdateSelectedDayEvent(index, value!)); + }, + ), + Text(dayLabels[index]), + ], + ); + }), + ); + } + + Widget _buildFunctionSwitch(BuildContext context, bool isOn) { return Row( children: [ - Checkbox( - value: selectedDays[index], + Text( + 'Function:', + style: context.textTheme.bodySmall! + .copyWith(color: ColorsManager.grayColor), + ), + const SizedBox(width: 10), + Radio( + value: true, + groupValue: isOn, onChanged: (bool? value) { - setState(() { - selectedDays[index] = value!; - }); + context + .read() + .add(const UpdateFunctionOnEvent(true)); }, ), - Text(day), + const Text('On'), + const SizedBox(width: 10), + Radio( + value: false, + groupValue: isOn, + onChanged: (bool? value) { + context + .read() + .add(const UpdateFunctionOnEvent(false)); + }, + ), + const Text('Off'), ], ); } diff --git a/lib/services/devices_mang_api.dart b/lib/services/devices_mang_api.dart index 4be915a2..4445d5c1 100644 --- a/lib/services/devices_mang_api.dart +++ b/lib/services/devices_mang_api.dart @@ -2,6 +2,7 @@ import 'package:flutter/material.dart'; import 'package:syncrow_web/pages/device_managment/all_devices/models/device_reports.dart'; import 'package:syncrow_web/pages/device_managment/all_devices/models/device_status.dart'; import 'package:syncrow_web/pages/device_managment/all_devices/models/devices_model.dart'; +import 'package:syncrow_web/pages/device_managment/water_heater/models/send_schedule.dart'; import 'package:syncrow_web/pages/visitor_password/model/device_model.dart'; import 'package:syncrow_web/services/api/http_service.dart'; import 'package:syncrow_web/utils/constants/api_const.dart'; @@ -79,7 +80,7 @@ class DevicesManagementApi { body: body, showServerMessage: true, expectedResponseModel: (json) { - return (json['successResults'] as List).isNotEmpty ; + return (json['successResults'] as List).isNotEmpty; }, ); @@ -175,4 +176,61 @@ class DevicesManagementApi { ); } } + + Future addScheduleRecord(SendSchedule sendSchedule, String uuid) async { + try { + final response = await HTTPService().post( + path: ApiEndpoints.scheduleByDeviceId.replaceAll('{deviceUuid}', uuid), + body: sendSchedule.toMap(), + showServerMessage: true, + expectedResponseModel: (json) { + return json['success'] ?? false; + }, + ); + return response; + } catch (e) { + debugPrint('Error fetching $e'); + return false; + } + } + + Future updateScheduleRecord( + {required bool enable, + required String uuid, + required String scheduleId}) async { + try { + final response = await HTTPService().put( + path: ApiEndpoints.scheduleByDeviceId + .replaceAll('{deviceUuid}', uuid) + .replaceAll('{scheduleUuid}', scheduleId), + body: { + 'scheduleId': scheduleId, + 'enable': enable, + }, + expectedResponseModel: (json) { + return json['success'] ?? false; + }, + ); + return response; + } catch (e) { + debugPrint('Error fetching $e'); + return false; + } + } + + Future deleteScheduleRecord(String uuid) async { + try { + final response = await HTTPService().delete( + path: ApiEndpoints.scheduleByDeviceId.replaceAll('{deviceUuid}', uuid), + showServerMessage: true, + expectedResponseModel: (json) { + return json['success'] ?? false; + }, + ); + return response; + } catch (e) { + debugPrint('Error fetching $e'); + return false; + } + } } diff --git a/lib/utils/constants/api_const.dart b/lib/utils/constants/api_const.dart index 3138c502..0c17e2bc 100644 --- a/lib/utils/constants/api_const.dart +++ b/lib/utils/constants/api_const.dart @@ -38,4 +38,10 @@ abstract class ApiEndpoints { static const String getDeviceLogs = '/device/report-logs/{uuid}?code={code}'; static const String getDeviceLogsByDate = '/device/report-logs/{uuid}?code={code}&startTime={startTime}&endTime={endTime}'; + + static const String scheduleByDeviceId = '/schedule/{deviceUuid}'; + static const String deleteScheduleByDeviceId = + '/schedule/{deviceUuid}/{scheduleUuid}'; + static const String updateScheduleByDeviceId = + '/schedule/enable/{deviceUuid}'; } diff --git a/lib/utils/constants/assets.dart b/lib/utils/constants/assets.dart index 3c22e031..c334d2d1 100644 --- a/lib/utils/constants/assets.dart +++ b/lib/utils/constants/assets.dart @@ -154,4 +154,7 @@ class Assets { static const String mainDoorReports = 'assets/icons/main_door_reports.svg'; //assets/icons/main_door.svg static const String mainDoor = 'assets/icons/main_door.svg'; + + //assets/icons/empty_records.svg + static const String emptyRecords = 'assets/icons/empty_records.svg'; } diff --git a/lib/utils/format_date_time.dart b/lib/utils/format_date_time.dart index 5c089a2c..e214b46c 100644 --- a/lib/utils/format_date_time.dart +++ b/lib/utils/format_date_time.dart @@ -1,3 +1,4 @@ +import 'package:flutter/material.dart'; import 'package:intl/intl.dart'; String formatDateTime(DateTime? dateTime) { @@ -9,3 +10,17 @@ String formatDateTime(DateTime? dateTime) { return '${dateFormatter.format(dateTime)} ${timeFormatter.format(dateTime)}'; } + +String formatTimeOfDayToISO(TimeOfDay time, {DateTime? currentDate}) { + final now = currentDate ?? DateTime.now(); + + final dateTime = DateTime( + now.year, + now.month, + now.day, + time.hour, + time.minute, + ); + + return dateTime.toUtc().toIso8601String(); +} \ No newline at end of file From b3d891b2c808a13d33d6076ec22625019e3a1b9c Mon Sep 17 00:00:00 2001 From: ashrafzarkanisala Date: Sun, 22 Sep 2024 14:51:15 +0300 Subject: [PATCH 05/10] add the delete api --- .../water_heater/bloc/water_heater_bloc.dart | 4 ++-- ...{send_schedule.dart => schedule_model.dart} | 18 +++++++++--------- lib/services/devices_mang_api.dart | 11 +++++++---- 3 files changed, 18 insertions(+), 15 deletions(-) rename lib/pages/device_managment/water_heater/models/{send_schedule.dart => schedule_model.dart} (84%) diff --git a/lib/pages/device_managment/water_heater/bloc/water_heater_bloc.dart b/lib/pages/device_managment/water_heater/bloc/water_heater_bloc.dart index 07f73569..a96213db 100644 --- a/lib/pages/device_managment/water_heater/bloc/water_heater_bloc.dart +++ b/lib/pages/device_managment/water_heater/bloc/water_heater_bloc.dart @@ -6,7 +6,7 @@ import 'package:equatable/equatable.dart'; import 'package:flutter/material.dart'; import 'package:syncrow_web/pages/device_managment/all_devices/models/device_status.dart'; import 'package:syncrow_web/pages/device_managment/water_heater/models/schedule_entry.dart'; -import 'package:syncrow_web/pages/device_managment/water_heater/models/send_schedule.dart'; +import 'package:syncrow_web/pages/device_managment/water_heater/models/schedule_model.dart'; import 'package:syncrow_web/pages/device_managment/water_heater/models/water_heater_status_model.dart'; import 'package:syncrow_web/services/devices_mang_api.dart'; import 'package:syncrow_web/utils/format_date_time.dart'; @@ -360,7 +360,7 @@ class WaterHeaterBloc extends Bloc { if (state is WaterHeaterDeviceStatusLoaded) { final currentState = state as WaterHeaterDeviceStatusLoaded; - SendSchedule sendSchedule = SendSchedule( + ScheduleModel sendSchedule = ScheduleModel( category: event.category, time: formatTimeOfDayToISO(event.time), function: Status(code: 'switch_1', value: event.functionOn), diff --git a/lib/pages/device_managment/water_heater/models/send_schedule.dart b/lib/pages/device_managment/water_heater/models/schedule_model.dart similarity index 84% rename from lib/pages/device_managment/water_heater/models/send_schedule.dart rename to lib/pages/device_managment/water_heater/models/schedule_model.dart index 650f3a04..57386136 100644 --- a/lib/pages/device_managment/water_heater/models/send_schedule.dart +++ b/lib/pages/device_managment/water_heater/models/schedule_model.dart @@ -17,26 +17,26 @@ import 'package:syncrow_web/pages/device_managment/all_devices/models/device_sta } */ -class SendSchedule { +class ScheduleModel { final String category; final String time; final Status function; final List days; - SendSchedule({ + ScheduleModel({ required this.category, required this.time, required this.function, required this.days, }); - SendSchedule copyWith({ + ScheduleModel copyWith({ String? category, String? time, Status? function, List? days, }) { - return SendSchedule( + return ScheduleModel( category: category ?? this.category, time: time ?? this.time, function: function ?? this.function, @@ -53,8 +53,8 @@ class SendSchedule { }; } - factory SendSchedule.fromMap(Map map) { - return SendSchedule( + factory ScheduleModel.fromMap(Map map) { + return ScheduleModel( category: map['category'] ?? '', time: map['time'] ?? '', function: Status.fromMap(map['function']), @@ -64,8 +64,8 @@ class SendSchedule { String toJson() => json.encode(toMap()); - factory SendSchedule.fromJson(String source) => - SendSchedule.fromMap(json.decode(source)); + factory ScheduleModel.fromJson(String source) => + ScheduleModel.fromMap(json.decode(source)); @override String toString() { @@ -76,7 +76,7 @@ class SendSchedule { bool operator ==(Object other) { if (identical(this, other)) return true; - return other is SendSchedule && + return other is ScheduleModel && other.category == category && other.time == time && other.function == function && diff --git a/lib/services/devices_mang_api.dart b/lib/services/devices_mang_api.dart index 4445d5c1..049538d3 100644 --- a/lib/services/devices_mang_api.dart +++ b/lib/services/devices_mang_api.dart @@ -2,7 +2,7 @@ import 'package:flutter/material.dart'; import 'package:syncrow_web/pages/device_managment/all_devices/models/device_reports.dart'; import 'package:syncrow_web/pages/device_managment/all_devices/models/device_status.dart'; import 'package:syncrow_web/pages/device_managment/all_devices/models/devices_model.dart'; -import 'package:syncrow_web/pages/device_managment/water_heater/models/send_schedule.dart'; +import 'package:syncrow_web/pages/device_managment/water_heater/models/schedule_model.dart'; import 'package:syncrow_web/pages/visitor_password/model/device_model.dart'; import 'package:syncrow_web/services/api/http_service.dart'; import 'package:syncrow_web/utils/constants/api_const.dart'; @@ -177,7 +177,8 @@ class DevicesManagementApi { } } - Future addScheduleRecord(SendSchedule sendSchedule, String uuid) async { + Future addScheduleRecord( + ScheduleModel sendSchedule, String uuid) async { try { final response = await HTTPService().post( path: ApiEndpoints.scheduleByDeviceId.replaceAll('{deviceUuid}', uuid), @@ -218,10 +219,12 @@ class DevicesManagementApi { } } - Future deleteScheduleRecord(String uuid) async { + Future deleteScheduleRecord(String uuid, String scheduleId) async { try { final response = await HTTPService().delete( - path: ApiEndpoints.scheduleByDeviceId.replaceAll('{deviceUuid}', uuid), + path: ApiEndpoints.scheduleByDeviceId + .replaceAll('{deviceUuid}', uuid) + .replaceAll('{scheduleUuid}', scheduleId), showServerMessage: true, expectedResponseModel: (json) { return json['success'] ?? false; From 3a28f0ef9a8d8ae57115d8c9b83181de62b4bc9e Mon Sep 17 00:00:00 2001 From: ashrafzarkanisala Date: Sun, 22 Sep 2024 20:47:34 +0300 Subject: [PATCH 06/10] refactor schedule view and cleaning --- .../water_heater/bloc/water_heater_bloc.dart | 404 ++++++---- .../water_heater/bloc/water_heater_event.dart | 24 +- .../water_heater/bloc/water_heater_state.dart | 56 +- .../helper/add_schedule_dialog_helper.dart | 246 ++++++ .../water_heater/models/schedule_entry.dart | 32 +- .../water_heater/models/schedule_model.dart | 100 ++- .../models/water_heater_status_model.dart | 12 +- .../view/water_heater_device_control.dart | 10 +- .../widgets/count_down_button.dart | 74 ++ .../widgets/count_down_inching_view.dart | 152 ++++ .../widgets/inching_mode_buttons.dart | 74 ++ .../water_heater/widgets/schedual_view.dart | 725 ++---------------- .../water_heater/widgets/schedule_header.dart | 46 ++ .../widgets/schedule_managment_ui.dart | 50 ++ .../widgets/schedule_mode_buttons.dart | 45 ++ .../widgets/schedule_mode_selector.dart | 86 +++ .../widgets/schedule_row_widget.dart | 76 ++ .../water_heater/widgets/schedule_table.dart | 198 +++++ lib/services/devices_mang_api.dart | 23 + lib/utils/constants/api_const.dart | 2 + lib/utils/format_date_time.dart | 7 +- 21 files changed, 1540 insertions(+), 902 deletions(-) create mode 100644 lib/pages/device_managment/water_heater/helper/add_schedule_dialog_helper.dart create mode 100644 lib/pages/device_managment/water_heater/widgets/count_down_button.dart create mode 100644 lib/pages/device_managment/water_heater/widgets/count_down_inching_view.dart create mode 100644 lib/pages/device_managment/water_heater/widgets/inching_mode_buttons.dart create mode 100644 lib/pages/device_managment/water_heater/widgets/schedule_header.dart create mode 100644 lib/pages/device_managment/water_heater/widgets/schedule_managment_ui.dart create mode 100644 lib/pages/device_managment/water_heater/widgets/schedule_mode_buttons.dart create mode 100644 lib/pages/device_managment/water_heater/widgets/schedule_mode_selector.dart create mode 100644 lib/pages/device_managment/water_heater/widgets/schedule_row_widget.dart create mode 100644 lib/pages/device_managment/water_heater/widgets/schedule_table.dart diff --git a/lib/pages/device_managment/water_heater/bloc/water_heater_bloc.dart b/lib/pages/device_managment/water_heater/bloc/water_heater_bloc.dart index a96213db..1eb88912 100644 --- a/lib/pages/device_managment/water_heater/bloc/water_heater_bloc.dart +++ b/lib/pages/device_managment/water_heater/bloc/water_heater_bloc.dart @@ -5,7 +5,6 @@ import 'package:bloc/bloc.dart'; import 'package:equatable/equatable.dart'; import 'package:flutter/material.dart'; import 'package:syncrow_web/pages/device_managment/all_devices/models/device_status.dart'; -import 'package:syncrow_web/pages/device_managment/water_heater/models/schedule_entry.dart'; import 'package:syncrow_web/pages/device_managment/water_heater/models/schedule_model.dart'; import 'package:syncrow_web/pages/device_managment/water_heater/models/water_heater_status_model.dart'; import 'package:syncrow_web/services/devices_mang_api.dart'; @@ -21,17 +20,20 @@ class WaterHeaterBloc extends Bloc { on(_updateScheduleEvent); on(_stopScheduleEvent); on(_onDecrementCountdown); - on(_onAddSchedule); - on(_onDeleteSchedule); - on(_onUpdateSchedule); on(_initializeAddSchedule); on(_updateSelectedTime); on(_updateSelectedDay); on(_updateFunctionOn); + + on(_getSchedule); + on(_onAddSchedule); + on(_onDeleteSchedule); + on(_onUpdateSchedule); } late WaterHeaterStatusModel deviceStatus; Timer? _countdownTimer; + // Timer? _inchingTimer; FutureOr _initializeAddSchedule( InitializeAddScheduleEvent event, @@ -87,22 +89,37 @@ class WaterHeaterBloc extends Bloc { ) async { final currentState = state; if (currentState is WaterHeaterDeviceStatusLoaded) { - final countdownRemaining = - // currentState.isActive == true - // ? currentState.countdownRemaining - // : - Duration(hours: event.hours, minutes: event.minutes); + if (event.scheduleMode == ScheduleModes.schedule) { + emit(currentState.copyWith( + scheduleMode: ScheduleModes.schedule, + )); + } + if (event.scheduleMode == ScheduleModes.countdown) { + final countdownRemaining = + Duration(hours: event.hours, minutes: event.minutes); - emit(currentState.copyWith( - scheduleMode: event.scheduleMode, - hours: countdownRemaining.inHours, - minutes: countdownRemaining.inMinutes % 60, - isActive: currentState.isActive, - countdownRemaining: countdownRemaining, - )); + emit(currentState.copyWith( + scheduleMode: ScheduleModes.countdown, + countdownHours: countdownRemaining.inHours, + countdownMinutes: countdownRemaining.inMinutes % 60, + isCountdownActive: currentState.isCountdownActive, + countdownRemaining: countdownRemaining, + )); - if (!currentState.isActive! && countdownRemaining > Duration.zero) { - _startCountdown(emit, countdownRemaining); + if (!currentState.isCountdownActive! && + countdownRemaining > Duration.zero) { + _startCountdownTimer(emit, countdownRemaining); + } + } else if (event.scheduleMode == ScheduleModes.inching) { + final inchingDuration = + Duration(hours: event.hours, minutes: event.minutes); + + emit(currentState.copyWith( + scheduleMode: ScheduleModes.inching, + inchingHours: inchingDuration.inHours, + inchingMinutes: inchingDuration.inMinutes % 60, + isInchingActive: currentState.isInchingActive, + )); } } } @@ -130,28 +147,39 @@ class WaterHeaterBloc extends Bloc { emit: emit, ); - if (success && - (event.code == "countdown_1" || event.code == "switch_inching")) { - final countdownDuration = Duration(seconds: event.value); + if (success) { + if (event.code == "countdown_1") { + final countdownDuration = Duration(seconds: event.value); - emit(currentState.copyWith( - status: deviceStatus, - scheduleMode: deviceStatus.scheduleMode, - hours: countdownDuration.inHours, - minutes: (countdownDuration.inMinutes % 60), - isActive: true, - countdownRemaining: countdownDuration, - )); - if (countdownDuration.inSeconds > 0) { - _startCountdown(emit, countdownDuration); - } else { - _countdownTimer?.cancel(); emit(currentState.copyWith( - hours: 0, - minutes: 0, - isActive: false, - countdownRemaining: Duration.zero, + countdownHours: countdownDuration.inHours, + countdownMinutes: countdownDuration.inMinutes % 60, + countdownRemaining: countdownDuration, + isCountdownActive: true, )); + + if (countdownDuration.inSeconds > 0) { + _startCountdownTimer(emit, countdownDuration); + } else { + _countdownTimer?.cancel(); + emit(currentState.copyWith( + countdownHours: 0, + countdownMinutes: 0, + countdownRemaining: Duration.zero, + isCountdownActive: false, + )); + } + } else if (event.code == "switch_inching") { + final inchingDuration = Duration(seconds: event.value); + //if (inchingDuration.inSeconds > 0) { + // _startInchingTimer(emit, inchingDuration); + // } else { + emit(currentState.copyWith( + inchingHours: inchingDuration.inHours, + inchingMinutes: inchingDuration.inMinutes % 60, + isInchingActive: true, + )); + // } } } } @@ -163,28 +191,30 @@ class WaterHeaterBloc extends Bloc { ) async { if (state is WaterHeaterDeviceStatusLoaded) { final currentState = state as WaterHeaterDeviceStatusLoaded; + final isCountDown = currentState.scheduleMode == ScheduleModes.countdown; _countdownTimer?.cancel(); - deviceStatus = deviceStatus.copyWith( - countdownHours: 0, - countdownMinutes: 0, - scheduleMode: ScheduleModes.countdown, - ); - - emit(currentState.copyWith( - status: deviceStatus, - scheduleMode: ScheduleModes.countdown, - hours: 0, - minutes: 0, - isActive: false, - countdownRemaining: Duration.zero, - )); + if (isCountDown) { + emit(currentState.copyWith( + countdownHours: 0, + countdownMinutes: 0, + countdownRemaining: Duration.zero, + isCountdownActive: false, + )); + } else if (currentState.scheduleMode == ScheduleModes.inching) { + emit(currentState.copyWith( + inchingHours: 0, + inchingMinutes: 0, + isInchingActive: false, + )); + } try { final status = await DevicesManagementApi().deviceControl( event.deviceId, - Status(code: 'countdown_1', value: 0), + Status( + code: isCountDown ? 'countdown_1' : 'switch_inching', value: 0), ); if (!status) { emit(const WaterHeaterFailedState(error: 'Failed to stop schedule.')); @@ -207,30 +237,66 @@ class WaterHeaterBloc extends Bloc { deviceStatus = WaterHeaterStatusModel.fromJson(event.deviceId, status.status); - if (deviceStatus.countdownHours > 0 || - deviceStatus.countdownMinutes > 0) { - final remainingDuration = Duration( + if (deviceStatus.scheduleMode == ScheduleModes.countdown) { + final countdownRemaining = Duration( hours: deviceStatus.countdownHours, minutes: deviceStatus.countdownMinutes, ); - emit(WaterHeaterDeviceStatusLoaded( - deviceStatus, - scheduleMode: deviceStatus.scheduleMode, - hours: deviceStatus.countdownHours, - minutes: deviceStatus.countdownMinutes, - isActive: true, - countdownRemaining: remainingDuration, - )); + if (countdownRemaining > Duration.zero) { + emit(WaterHeaterDeviceStatusLoaded( + deviceStatus, + scheduleMode: ScheduleModes.countdown, + countdownHours: deviceStatus.countdownHours, + countdownMinutes: deviceStatus.countdownMinutes, + isCountdownActive: true, + countdownRemaining: countdownRemaining, + )); + _startCountdownTimer(emit, countdownRemaining); + } else { + emit(WaterHeaterDeviceStatusLoaded( + deviceStatus, + scheduleMode: ScheduleModes.countdown, + countdownHours: 0, + countdownMinutes: 0, + isCountdownActive: false, + countdownRemaining: Duration.zero, + )); + } + } else if (deviceStatus.scheduleMode == ScheduleModes.inching) { + final inchingDuration = Duration( + hours: deviceStatus.inchingHours, + minutes: deviceStatus.inchingMinutes, + ); - _startCountdown(emit, remainingDuration); + if (inchingDuration > Duration.zero) { + emit(WaterHeaterDeviceStatusLoaded( + deviceStatus, + scheduleMode: ScheduleModes.inching, + inchingHours: deviceStatus.inchingHours, + inchingMinutes: deviceStatus.inchingMinutes, + isInchingActive: true, + )); +//_startInchingTimer(emit, inchingDuration); + } else { + emit(WaterHeaterDeviceStatusLoaded( + deviceStatus, + scheduleMode: ScheduleModes.inching, + inchingHours: 0, + inchingMinutes: 0, + isInchingActive: false, + )); + } } else { emit(WaterHeaterDeviceStatusLoaded( deviceStatus, scheduleMode: deviceStatus.scheduleMode, - hours: 0, - minutes: 0, - isActive: false, + countdownHours: 0, + countdownMinutes: 0, + inchingHours: 0, + inchingMinutes: 0, + isCountdownActive: false, + isInchingActive: false, )); } } catch (e) { @@ -238,6 +304,28 @@ class WaterHeaterBloc extends Bloc { } } + void _startCountdownTimer( + Emitter emit, + Duration countdownRemaining, + ) { + _countdownTimer?.cancel(); + + _countdownTimer = Timer.periodic(const Duration(minutes: 1), (timer) { + add(DecrementCountdownEvent()); + }); + } + + // void _startInchingTimer( + // Emitter emit, + // Duration inchingDuration, + // ) { + // _inchingTimer?.cancel(); + + // _inchingTimer = Timer.periodic(const Duration(minutes: 1), (timer) { + // add(DecrementInchingEvent()); + // }); + // } + _onDecrementCountdown( DecrementCountdownEvent event, Emitter emit, @@ -253,9 +341,9 @@ class WaterHeaterBloc extends Bloc { if (newRemaining <= Duration.zero) { _countdownTimer?.cancel(); emit(currentState.copyWith( - hours: 0, - minutes: 0, - isActive: false, + countdownHours: 0, + countdownMinutes: 0, + isCountdownActive: false, countdownRemaining: Duration.zero, )); return; @@ -267,22 +355,44 @@ class WaterHeaterBloc extends Bloc { int newMinutes = (totalSeconds % 3600) ~/ 60; emit(currentState.copyWith( - hours: newHours, - minutes: newMinutes, + countdownHours: newHours, + countdownMinutes: newMinutes, countdownRemaining: newRemaining, )); } } } - void _startCountdown( - Emitter emit, Duration countdownRemaining) { - _countdownTimer?.cancel(); + // FutureOr _onDecrementInching( + // DecrementInchingEvent event, + // Emitter emit, + // ) { + // if (state is WaterHeaterDeviceStatusLoaded) { + // final currentState = state as WaterHeaterDeviceStatusLoaded; - _countdownTimer = Timer.periodic(const Duration(minutes: 1), (timer) { - add(DecrementCountdownEvent()); - }); - } + // if (currentState.inchingHours > 0 || currentState.inchingMinutes > 0) { + // final newRemaining = Duration( + // hours: currentState.inchingHours, + // minutes: currentState.inchingMinutes, + // ) - + // const Duration(minutes: 1); + + // if (newRemaining <= Duration.zero) { + // _inchingTimer?.cancel(); + // emit(currentState.copyWith( + // inchingHours: 0, + // inchingMinutes: 0, + // isInchingActive: false, + // )); + // } else { + // emit(currentState.copyWith( + // inchingHours: newRemaining.inHours, + // inchingMinutes: newRemaining.inMinutes % 60, + // )); + // } + // } + // } + // } Future _runDebounce({ required String deviceId, @@ -353,6 +463,36 @@ class WaterHeaterBloc extends Bloc { } } + @override + Future close() { + _countdownTimer?.cancel(); + return super.close(); + } + + FutureOr _getSchedule( + GetSchedulesEvent event, Emitter emit) async { + emit(ScheduleLoadingState()); + + try { + // List schedules = await DevicesManagementApi() + // .getDeviceSchedules(deviceStatus.uuid, event.category); + + List schedules = const []; + + emit(WaterHeaterDeviceStatusLoaded( + deviceStatus, + schedules: schedules, + scheduleMode: ScheduleModes.schedule, + )); + } catch (e) { + //(const WaterHeaterFailedState(error: 'Failed to fetch schedules.')); + emit(WaterHeaterDeviceStatusLoaded( + deviceStatus, + schedules: const [], + )); + } + } + FutureOr _onAddSchedule( AddScheduleEvent event, Emitter emit, @@ -360,59 +500,30 @@ class WaterHeaterBloc extends Bloc { if (state is WaterHeaterDeviceStatusLoaded) { final currentState = state as WaterHeaterDeviceStatusLoaded; - ScheduleModel sendSchedule = ScheduleModel( + ScheduleModel newSchedule = ScheduleModel( category: event.category, time: formatTimeOfDayToISO(event.time), function: Status(code: 'switch_1', value: event.functionOn), - days: _getSelectedDaysString(event.selectedDays), + days: ScheduleModel.convertSelectedDaysToStrings(event.selectedDays), ); + // emit(ScheduleLoadingState()); + bool success = await DevicesManagementApi() - .addScheduleRecord(sendSchedule, currentState.status.uuid); + .addScheduleRecord(newSchedule, currentState.status.uuid); if (success) { - final newSchedule = ScheduleEntry( - selectedDays: event.selectedDays, - time: event.time, - functionOn: event.functionOn, - category: event.category, - ); - final updatedSchedules = - List.from(currentState.schedules)..add(newSchedule); + List.from(currentState.schedules)..add(newSchedule); emit(currentState.copyWith(schedules: updatedSchedules)); } else { - emit(const WaterHeaterFailedState( - error: 'Failed to add schedule. Please try again.')); + emit(currentState); + //emit(const WaterHeaterFailedState(error: 'Failed to add schedule.')); } } } - List _getSelectedDaysString(List selectedDays) { - final days = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat']; - List selectedDaysStr = []; - for (int i = 0; i < selectedDays.length; i++) { - if (selectedDays[i]) { - selectedDaysStr.add(days[i]); - } - } - return selectedDaysStr; - } - - FutureOr _onDeleteSchedule( - DeleteScheduleEvent event, - Emitter emit, - ) { - if (state is WaterHeaterDeviceStatusLoaded) { - final currentState = state as WaterHeaterDeviceStatusLoaded; - final updatedSchedules = List.from(currentState.schedules) - ..removeAt(event.index); - - emit(currentState.copyWith(schedules: updatedSchedules)); - } - } - FutureOr _onUpdateSchedule( UpdateScheduleEntryEvent event, Emitter emit, @@ -420,44 +531,53 @@ class WaterHeaterBloc extends Bloc { if (state is WaterHeaterDeviceStatusLoaded) { final currentState = state as WaterHeaterDeviceStatusLoaded; - // Get the current schedule ID or UUID (assuming it's stored in the schedules) - String scheduleId = - ""; // Retrieve the actual schedule ID based on your model + ScheduleModel updatedSchedule = currentState.schedules[event.index] + .copyWith( + function: Status(code: 'switch_1', value: event.functionOn)); + + // emit(ScheduleLoadingState()); - // Call the API to update the schedule bool success = await DevicesManagementApi().updateScheduleRecord( enable: event.functionOn, - uuid: event.deviceId, - scheduleId: scheduleId, + uuid: currentState.status.uuid, + scheduleId: event.scheduleId, ); if (success) { final updatedSchedules = - List.from(currentState.schedules); + List.from(currentState.schedules) + ..[event.index] = updatedSchedule; - final updatedScheduleIndex = updatedSchedules.indexWhere((schedule) { - return schedule.category == event.category; - }); - - if (updatedScheduleIndex != -1) { - updatedSchedules[updatedScheduleIndex] = ScheduleEntry( - category: event.category, - selectedDays: updatedSchedules[updatedScheduleIndex].selectedDays, - time: updatedSchedules[updatedScheduleIndex].time, - functionOn: event.functionOn, - ); - emit(currentState.copyWith(schedules: updatedSchedules)); - } + emit(currentState.copyWith(schedules: updatedSchedules)); } else { - emit(const WaterHeaterFailedState( - error: 'Failed to update schedule. Please try again.')); + emit(currentState); + // emit(const WaterHeaterFailedState(error: 'Failed to update schedule.')); } } } - @override - Future close() { - _countdownTimer?.cancel(); - return super.close(); + FutureOr _onDeleteSchedule( + DeleteScheduleEvent event, + Emitter emit, + ) async { + if (state is WaterHeaterDeviceStatusLoaded) { + final currentState = state as WaterHeaterDeviceStatusLoaded; + + // emit(ScheduleLoadingState()); + + bool success = await DevicesManagementApi() + .deleteScheduleRecord(currentState.status.uuid, event.scheduleId); + + if (success) { + final updatedSchedules = + List.from(currentState.schedules) + ..removeAt(event.index); + + emit(currentState.copyWith(schedules: updatedSchedules)); + } else { + emit(currentState); + // emit(const WaterHeaterFailedState(error: 'Failed to delete schedule.')); + } + } } } diff --git a/lib/pages/device_managment/water_heater/bloc/water_heater_event.dart b/lib/pages/device_managment/water_heater/bloc/water_heater_event.dart index 0c9cd794..bc0909ab 100644 --- a/lib/pages/device_managment/water_heater/bloc/water_heater_event.dart +++ b/lib/pages/device_managment/water_heater/bloc/water_heater_event.dart @@ -82,26 +82,44 @@ final class AddScheduleEvent extends WaterHeaterEvent { final class DeleteScheduleEvent extends WaterHeaterEvent { final int index; + final String scheduleId; - const DeleteScheduleEvent(this.index); + const DeleteScheduleEvent({required this.index, required this.scheduleId}); @override - List get props => [index]; + List get props => [index, scheduleId]; } final class UpdateScheduleEntryEvent extends WaterHeaterEvent { final bool functionOn; final String category; final String deviceId; + final int index; + final String scheduleId; const UpdateScheduleEntryEvent({ required this.functionOn, required this.category, required this.deviceId, + required this.scheduleId, + required this.index, }); @override - List get props => [category, functionOn, deviceId]; + List get props => [category, functionOn, deviceId, scheduleId, index]; +} + +class GetSchedulesEvent extends WaterHeaterEvent { + final String uuid; + final String category; + + const GetSchedulesEvent({ + required this.uuid, + required this.category, + }); + + @override + List get props => [uuid, category]; } class InitializeAddScheduleEvent extends WaterHeaterEvent { diff --git a/lib/pages/device_managment/water_heater/bloc/water_heater_state.dart b/lib/pages/device_managment/water_heater/bloc/water_heater_state.dart index 1054104f..5ff5ba3d 100644 --- a/lib/pages/device_managment/water_heater/bloc/water_heater_state.dart +++ b/lib/pages/device_managment/water_heater/bloc/water_heater_state.dart @@ -13,14 +13,24 @@ final class WaterHeaterInitial extends WaterHeaterState {} final class WaterHeaterLoadingState extends WaterHeaterState {} +final class ScheduleLoadingState extends WaterHeaterState {} + class WaterHeaterDeviceStatusLoaded extends WaterHeaterState { final WaterHeaterStatusModel status; final ScheduleModes? scheduleMode; - final int? hours; - final int? minutes; - final bool? isActive; + + // Countdown-specific + final int? countdownHours; + final int? countdownMinutes; final Duration? countdownRemaining; - final List schedules; + final bool? isCountdownActive; + + // Inching-specific + final int? inchingHours; + final int? inchingMinutes; + final bool? isInchingActive; + + final List schedules; final List selectedDays; final TimeOfDay? selectedTime; final bool functionOn; @@ -29,10 +39,13 @@ class WaterHeaterDeviceStatusLoaded extends WaterHeaterState { const WaterHeaterDeviceStatusLoaded( this.status, { this.scheduleMode, - this.hours, - this.minutes, - this.isActive, + this.countdownHours, + this.countdownMinutes, this.countdownRemaining, + this.isCountdownActive, + this.inchingHours, + this.inchingMinutes, + this.isInchingActive, this.schedules = const [], this.selectedDays = const [false, false, false, false, false, false, false], this.selectedTime, @@ -44,10 +57,13 @@ class WaterHeaterDeviceStatusLoaded extends WaterHeaterState { List get props => [ status, scheduleMode, - hours, - minutes, - isActive, + countdownHours, + countdownMinutes, countdownRemaining, + isCountdownActive, + inchingHours, + inchingMinutes, + isInchingActive, schedules, selectedDays, selectedTime, @@ -58,11 +74,14 @@ class WaterHeaterDeviceStatusLoaded extends WaterHeaterState { WaterHeaterDeviceStatusLoaded copyWith({ WaterHeaterStatusModel? status, ScheduleModes? scheduleMode, - int? hours, - int? minutes, - bool? isActive, + int? countdownHours, + int? countdownMinutes, Duration? countdownRemaining, - List? schedules, + bool? isCountdownActive, + int? inchingHours, + int? inchingMinutes, + bool? isInchingActive, + List? schedules, List? selectedDays, TimeOfDay? selectedTime, bool? functionOn, @@ -71,10 +90,13 @@ class WaterHeaterDeviceStatusLoaded extends WaterHeaterState { return WaterHeaterDeviceStatusLoaded( status ?? this.status, scheduleMode: scheduleMode ?? this.scheduleMode, - hours: hours ?? this.hours, - minutes: minutes ?? this.minutes, - isActive: isActive ?? this.isActive, + countdownHours: countdownHours ?? this.countdownHours, + countdownMinutes: countdownMinutes ?? this.countdownMinutes, countdownRemaining: countdownRemaining ?? this.countdownRemaining, + isCountdownActive: isCountdownActive ?? this.isCountdownActive, + inchingHours: inchingHours ?? this.inchingHours, + inchingMinutes: inchingMinutes ?? this.inchingMinutes, + isInchingActive: isInchingActive ?? this.isInchingActive, schedules: schedules ?? this.schedules, selectedDays: selectedDays ?? this.selectedDays, selectedTime: selectedTime ?? this.selectedTime, diff --git a/lib/pages/device_managment/water_heater/helper/add_schedule_dialog_helper.dart b/lib/pages/device_managment/water_heater/helper/add_schedule_dialog_helper.dart new file mode 100644 index 00000000..68e364d7 --- /dev/null +++ b/lib/pages/device_managment/water_heater/helper/add_schedule_dialog_helper.dart @@ -0,0 +1,246 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:syncrow_web/pages/device_managment/water_heater/bloc/water_heater_bloc.dart'; +import 'package:syncrow_web/pages/device_managment/water_heater/models/schedule_model.dart'; +import 'package:syncrow_web/utils/color_manager.dart'; +import 'package:syncrow_web/utils/extension/build_context_x.dart'; +import 'package:syncrow_web/pages/common/buttons/default_button.dart'; + +class ScheduleDialogHelper { + static void showAddScheduleDialog(BuildContext context, + {ScheduleModel? schedule, int? index, bool? isEdit}) { + final bloc = context.read(); + + if (schedule != null) { + final time = _convertStringToTimeOfDay(schedule.time); + final selectedDays = _convertDaysStringToBooleans(schedule.days); + + bloc.add(InitializeAddScheduleEvent( + selectedTime: time, + selectedDays: selectedDays, + functionOn: schedule.function.value, + isEditing: true, + index: index, + )); + } else { + bloc.add( + const InitializeAddScheduleEvent( + selectedDays: [false, false, false, false, false, false, false], + functionOn: false, + isEditing: false, + index: null, + selectedTime: null, + ), + ); + } + + showDialog( + context: context, + builder: (ctx) { + return BlocProvider.value( + value: bloc, + child: BlocBuilder( + builder: (context, state) { + if (state is WaterHeaterDeviceStatusLoaded) { + return AlertDialog( + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(20), + ), + content: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + const SizedBox(), + Text( + 'Scheduling', + style: context.textTheme.titleLarge!.copyWith( + color: ColorsManager.dialogBlueTitle, + fontWeight: FontWeight.bold, + ), + ), + const SizedBox(), + ], + ), + const SizedBox(height: 24), + SizedBox( + width: 150, + height: 40, + child: DefaultButton( + padding: 8, + backgroundColor: ColorsManager.boxColor, + borderRadius: 15, + onPressed: isEdit == true + ? null + : () async { + TimeOfDay? time = await showTimePicker( + context: context, + initialTime: + state.selectedTime ?? TimeOfDay.now(), + ); + if (time != null) { + bloc.add(UpdateSelectedTimeEvent(time)); + } + }, + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + state.selectedTime == null + ? 'Time' + : state.selectedTime!.format(context), + style: context.textTheme.bodySmall!.copyWith( + color: ColorsManager.grayColor, + ), + ), + const Icon( + Icons.access_time, + color: ColorsManager.grayColor, + size: 18, + ), + ], + ), + ), + ), + const SizedBox(height: 16), + _buildDayCheckboxes(context, state.selectedDays, + isEdit: isEdit), + const SizedBox(height: 16), + _buildFunctionSwitch(context, state.functionOn), + ], + ), + actions: [ + SizedBox( + width: 200, + child: DefaultButton( + height: 40, + onPressed: () { + Navigator.pop(context); + }, + backgroundColor: ColorsManager.boxColor, + child: Text( + 'Cancel', + style: context.textTheme.bodyMedium, + ), + ), + ), + SizedBox( + width: 200, + child: DefaultButton( + height: 40, + onPressed: () { + if (state.selectedTime != null) { + if (state.isEditing && index != null) { + bloc.add(UpdateScheduleEntryEvent( + index: index, + deviceId: state.status.uuid, + category: 'kg', + functionOn: state.functionOn, + scheduleId: state.schedules[index].scheduleId, + )); + } else { + bloc.add(AddScheduleEvent( + category: 'kg', + time: state.selectedTime!, + selectedDays: state.selectedDays, + functionOn: state.functionOn, + )); + } + Navigator.pop(context); + } + }, + backgroundColor: ColorsManager.primaryColor, + child: const Text('Save'), + ), + ), + ], + ); + } + return const SizedBox(); + }, + ), + ); + }, + ); + } + + static TimeOfDay _convertStringToTimeOfDay(String timeString) { + final DateTime dateTime = DateTime.parse(timeString); + return TimeOfDay(hour: dateTime.hour, minute: dateTime.minute); + } + + static List _convertDaysStringToBooleans(List selectedDays) { + final daysOfWeek = ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun']; + List daysBoolean = List.filled(7, false); + + for (int i = 0; i < daysOfWeek.length; i++) { + if (selectedDays.contains(daysOfWeek[i])) { + daysBoolean[i] = true; + } + } + + return daysBoolean; + } + + static Widget _buildDayCheckboxes( + BuildContext context, List selectedDays, + {bool? isEdit}) { + final dayLabels = ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun']; + + return Row( + children: List.generate(7, (index) { + return Row( + children: [ + Checkbox( + value: selectedDays[index], + onChanged: isEdit == true + ? null + : (bool? value) { + context + .read() + .add(UpdateSelectedDayEvent(index, value!)); + }, + ), + Text(dayLabels[index]), + ], + ); + }), + ); + } + + static Widget _buildFunctionSwitch(BuildContext context, bool isOn) { + return Row( + children: [ + Text( + 'Function:', + style: context.textTheme.bodySmall! + .copyWith(color: ColorsManager.grayColor), + ), + const SizedBox(width: 10), + Radio( + value: true, + groupValue: isOn, + onChanged: (bool? value) { + context + .read() + .add(const UpdateFunctionOnEvent(true)); + }, + ), + const Text('On'), + const SizedBox(width: 10), + Radio( + value: false, + groupValue: isOn, + onChanged: (bool? value) { + context + .read() + .add(const UpdateFunctionOnEvent(false)); + }, + ), + const Text('Off'), + ], + ); + } +} diff --git a/lib/pages/device_managment/water_heater/models/schedule_entry.dart b/lib/pages/device_managment/water_heater/models/schedule_entry.dart index 2d02b617..8b79f563 100644 --- a/lib/pages/device_managment/water_heater/models/schedule_entry.dart +++ b/lib/pages/device_managment/water_heater/models/schedule_entry.dart @@ -1,19 +1,19 @@ -import 'package:flutter/material.dart'; +// import 'package:flutter/material.dart'; -class ScheduleEntry { - final List selectedDays; - final TimeOfDay time; - final bool functionOn; - final String category; +// class ScheduleEntry { +// final List selectedDays; +// final TimeOfDay time; +// final bool functionOn; +// final String category; - ScheduleEntry({ - required this.selectedDays, - required this.time, - required this.functionOn, - required this.category, - }); +// ScheduleEntry({ +// required this.selectedDays, +// required this.time, +// required this.functionOn, +// required this.category, +// }); - @override - String toString() => - 'ScheduleEntry(selectedDays: $selectedDays, time: $time, functionOn: $functionOn)'; -} \ No newline at end of file +// @override +// String toString() => +// 'ScheduleEntry(selectedDays: $selectedDays, time: $time, functionOn: $functionOn)'; +// } \ No newline at end of file diff --git a/lib/pages/device_managment/water_heater/models/schedule_model.dart b/lib/pages/device_managment/water_heater/models/schedule_model.dart index 57386136..7e9410be 100644 --- a/lib/pages/device_managment/water_heater/models/schedule_model.dart +++ b/lib/pages/device_managment/water_heater/models/schedule_model.dart @@ -1,49 +1,27 @@ import 'dart:convert'; - -import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; import 'package:syncrow_web/pages/device_managment/all_devices/models/device_status.dart'; - -/* -{ - "category": "kg", - "time": "2024-09-22T10:31:54Z", - "function": { - "code": "switch_1", - "value": true - }, - "days": [ - "Sun" - ] -} -*/ +import 'package:flutter/foundation.dart'; class ScheduleModel { + final String scheduleId; final String category; final String time; final Status function; - final List days; + final List days; + final TimeOfDay? timeOfDay; + final List? selectedDays; ScheduleModel({ required this.category, required this.time, required this.function, required this.days, + this.timeOfDay, + this.selectedDays, + this.scheduleId = '', }); - ScheduleModel copyWith({ - String? category, - String? time, - Status? function, - List? days, - }) { - return ScheduleModel( - category: category ?? this.category, - time: time ?? this.time, - function: function ?? this.function, - days: days ?? this.days, - ); - } - Map toMap() { return { 'category': category, @@ -55,10 +33,15 @@ class ScheduleModel { factory ScheduleModel.fromMap(Map map) { return ScheduleModel( + scheduleId: map['scheduleId'] ?? '', category: map['category'] ?? '', time: map['time'] ?? '', function: Status.fromMap(map['function']), days: List.from(map['days']), + timeOfDay: + parseTimeOfDay(map['time']), + selectedDays: + parseSelectedDays(map['days']), ); } @@ -67,9 +50,54 @@ class ScheduleModel { factory ScheduleModel.fromJson(String source) => ScheduleModel.fromMap(json.decode(source)); + ScheduleModel copyWith({ + String? category, + String? time, + Status? function, + List? days, + TimeOfDay? timeOfDay, + List? selectedDays, + String? scheduleId, + }) { + return ScheduleModel( + category: category ?? this.category, + time: time ?? this.time, + function: function ?? this.function, + days: days ?? this.days, + timeOfDay: timeOfDay ?? this.timeOfDay, + selectedDays: selectedDays ?? this.selectedDays, + scheduleId: scheduleId ?? this.scheduleId, + ); + } + + static TimeOfDay? parseTimeOfDay(String isoTime) { + try { + final dateTime = DateTime.parse(isoTime); + return TimeOfDay(hour: dateTime.hour, minute: dateTime.minute); + } catch (e) { + return null; + } + } + + static List parseSelectedDays(List days) { + const allDays = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat']; + return allDays.map((day) => days.contains(day)).toList(); + } + + static List convertSelectedDaysToStrings(List selectedDays) { + const allDays = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat']; + List result = []; + for (int i = 0; i < selectedDays.length; i++) { + if (selectedDays[i]) { + result.add(allDays[i]); + } + } + return result; + } + @override String toString() { - return 'SendSchedule(category: $category, time: $time, function: $function, days: $days)'; + return 'ScheduleModel(category: $category, time: $time, function: $function, days: $days, timeOfDay: $timeOfDay, selectedDays: $selectedDays)'; } @override @@ -80,7 +108,9 @@ class ScheduleModel { other.category == category && other.time == time && other.function == function && - listEquals(other.days, days); + listEquals(other.days, days) && + timeOfDay == other.timeOfDay && + listEquals(other.selectedDays, selectedDays); } @override @@ -88,6 +118,8 @@ class ScheduleModel { return category.hashCode ^ time.hashCode ^ function.hashCode ^ - days.hashCode; + days.hashCode ^ + timeOfDay.hashCode ^ + selectedDays.hashCode; } } diff --git a/lib/pages/device_managment/water_heater/models/water_heater_status_model.dart b/lib/pages/device_managment/water_heater/models/water_heater_status_model.dart index 2eebefa9..c535bda2 100644 --- a/lib/pages/device_managment/water_heater/models/water_heater_status_model.dart +++ b/lib/pages/device_managment/water_heater/models/water_heater_status_model.dart @@ -1,5 +1,6 @@ import 'package:equatable/equatable.dart'; import 'package:syncrow_web/pages/device_managment/all_devices/models/device_status.dart'; +import 'package:syncrow_web/pages/device_managment/water_heater/models/schedule_model.dart'; enum ScheduleModes { countdown, schedule, circulate, inching } @@ -8,11 +9,14 @@ class WaterHeaterStatusModel extends Equatable { final bool heaterSwitch; final int countdownHours; final int countdownMinutes; + final int inchingHours; + final int inchingMinutes; final ScheduleModes scheduleMode; final String relayStatus; final String cycleTiming; + final List schedules; - WaterHeaterStatusModel({ + const WaterHeaterStatusModel({ required this.uuid, required this.heaterSwitch, required this.countdownHours, @@ -20,6 +24,9 @@ class WaterHeaterStatusModel extends Equatable { required this.relayStatus, required this.cycleTiming, required this.scheduleMode, + required this.schedules, + this.inchingHours = 0, + this.inchingMinutes = 0, }); factory WaterHeaterStatusModel.fromJson(String id, List jsonList) { @@ -60,6 +67,7 @@ class WaterHeaterStatusModel extends Equatable { relayStatus: relayStatus, cycleTiming: cycleTiming, scheduleMode: scheduleMode, + schedules: const [], ); } @@ -71,6 +79,7 @@ class WaterHeaterStatusModel extends Equatable { String? relayStatus, String? cycleTiming, ScheduleModes? scheduleMode, + List? schedules, }) { return WaterHeaterStatusModel( uuid: uuid ?? this.uuid, @@ -80,6 +89,7 @@ class WaterHeaterStatusModel extends Equatable { relayStatus: relayStatus ?? this.relayStatus, cycleTiming: cycleTiming ?? this.cycleTiming, scheduleMode: scheduleMode ?? this.scheduleMode, + schedules: schedules ?? this.schedules, ); } diff --git a/lib/pages/device_managment/water_heater/view/water_heater_device_control.dart b/lib/pages/device_managment/water_heater/view/water_heater_device_control.dart index e30303ac..c3f9d360 100644 --- a/lib/pages/device_managment/water_heater/view/water_heater_device_control.dart +++ b/lib/pages/device_managment/water_heater/view/water_heater_device_control.dart @@ -29,16 +29,12 @@ class WaterHeaterDeviceControl extends StatelessWidget return const Center(child: CircularProgressIndicator()); } else if (state is WaterHeaterDeviceStatusLoaded) { return _buildStatusControls(context, state.status); - } - // else if (state is WaterHeaterScheduleViewState) { - // final status = context.read().deviceStatus; - // return _buildStatusControls(context, status); - // } - else if (state is WaterHeaterFailedState || + } else if (state is WaterHeaterFailedState || state is WaterHeaterBatchFailedState) { return const Center(child: Text('Error fetching status')); } else { - return const Center(child: CircularProgressIndicator()); + return const SizedBox( + height: 200, child: Center(child: SizedBox())); } }, )); diff --git a/lib/pages/device_managment/water_heater/widgets/count_down_button.dart b/lib/pages/device_managment/water_heater/widgets/count_down_button.dart new file mode 100644 index 00000000..e60c7def --- /dev/null +++ b/lib/pages/device_managment/water_heater/widgets/count_down_button.dart @@ -0,0 +1,74 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:syncrow_web/utils/color_manager.dart'; +import 'package:syncrow_web/pages/common/buttons/default_button.dart'; +import 'package:syncrow_web/pages/device_managment/water_heater/bloc/water_heater_bloc.dart'; +import 'package:syncrow_web/utils/extension/build_context_x.dart'; + +class CountdownModeButtons extends StatelessWidget { + final bool isActive; + final String deviceId; + final int hours; + final int minutes; + + const CountdownModeButtons({ + super.key, + required this.isActive, + required this.deviceId, + required this.hours, + required this.minutes, + }); + + @override + Widget build(BuildContext context) { + return Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Expanded( + child: DefaultButton( + height: 40, + onPressed: () => Navigator.pop(context), + backgroundColor: ColorsManager.boxColor, + child: Text('Cancel', style: context.textTheme.bodyMedium), + ), + ), + const SizedBox(width: 20), + Expanded( + child: isActive + ? DefaultButton( + height: 40, + onPressed: () { + context + .read() + .add(StopScheduleEvent(deviceId)); + context.read().add( + ToggleWaterHeaterEvent( + deviceId: deviceId, + code: 'countdown_1', + value: 0, + ), + ); + }, + backgroundColor: Colors.red, + child: const Text('Stop'), + ) + : DefaultButton( + height: 40, + onPressed: () { + context.read().add( + ToggleWaterHeaterEvent( + deviceId: deviceId, + code: 'countdown_1', + value: Duration(hours: hours, minutes: minutes) + .inSeconds, + ), + ); + }, + backgroundColor: ColorsManager.primaryColor, + child: const Text('Save'), + ), + ), + ], + ); + } +} diff --git a/lib/pages/device_managment/water_heater/widgets/count_down_inching_view.dart b/lib/pages/device_managment/water_heater/widgets/count_down_inching_view.dart new file mode 100644 index 00000000..53892c20 --- /dev/null +++ b/lib/pages/device_managment/water_heater/widgets/count_down_inching_view.dart @@ -0,0 +1,152 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:syncrow_web/pages/device_managment/water_heater/bloc/water_heater_bloc.dart'; +import 'package:syncrow_web/pages/device_managment/water_heater/models/water_heater_status_model.dart'; +import 'package:syncrow_web/utils/color_manager.dart'; +import 'package:syncrow_web/utils/extension/build_context_x.dart'; + +class CountdownInchingView extends StatelessWidget { + final WaterHeaterDeviceStatusLoaded state; + + const CountdownInchingView({ + super.key, + required this.state, + }); + + @override + Widget build(BuildContext context) { + final isCountDown = + state.scheduleMode?.name == ScheduleModes.countdown.name; + + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + isCountDown ? 'Countdown:' : 'Inching:', + style: context.textTheme.bodySmall!.copyWith( + fontSize: 13, + color: ColorsManager.grayColor, + ), + ), + const SizedBox(height: 8), + Visibility( + visible: !isCountDown, + child: const Text( + 'Once enabled this feature, each time the device is turned on, it will automatically turn off after a preset time.'), + ), + const SizedBox(height: 8), + _hourMinutesWheel(context, state), + ], + ); + } + + Row _hourMinutesWheel( + BuildContext context, WaterHeaterDeviceStatusLoaded state) { + final isCountDown = + state.scheduleMode?.name == ScheduleModes.countdown.name; + late bool isActive; + if (isCountDown && + state.countdownRemaining != null && + state.isCountdownActive == true) { + isActive = true; + } else if (!isCountDown && + state.countdownRemaining != null && + state.isInchingActive == true) { + isActive = true; + } else { + isActive = false; + } + + return Row( + mainAxisAlignment: MainAxisAlignment.start, + children: [ + _buildPickerColumn( + context, + 'h', + isCountDown + ? (state.countdownHours ?? 0) + : (state.inchingHours ?? 0), + 24, (value) { + context.read().add(UpdateScheduleEvent( + scheduleMode: state.scheduleMode ?? ScheduleModes.countdown, + hours: value, + minutes: isCountDown + ? (state.countdownMinutes ?? 0) + : (state.inchingMinutes ?? 0), + )); + }, isActive: isActive), + const SizedBox(width: 10), + _buildPickerColumn( + context, + 'm', + isCountDown + ? (state.countdownMinutes ?? 0) + : (state.inchingMinutes ?? 0), + 60, (value) { + context.read().add(UpdateScheduleEvent( + scheduleMode: state.scheduleMode ?? ScheduleModes.countdown, + hours: isCountDown + ? (state.countdownHours ?? 0) + : (state.inchingHours ?? 0), + minutes: value, + )); + }, isActive: isActive), + ], + ); + } + + Widget _buildPickerColumn( + BuildContext context, + String label, + int initialValue, + int itemCount, + ValueChanged onSelected, { + required bool isActive, + }) { + return Row( + mainAxisSize: MainAxisSize.min, + children: [ + Container( + height: 40, + width: 80, + padding: const EdgeInsets.symmetric(horizontal: 16), + decoration: BoxDecoration( + color: ColorsManager.boxColor, + borderRadius: BorderRadius.circular(8), + ), + child: ListWheelScrollView.useDelegate( + key: ValueKey('$label-$initialValue'), + controller: FixedExtentScrollController( + initialItem: initialValue, + ), + itemExtent: 40.0, + physics: const FixedExtentScrollPhysics(), + onSelectedItemChanged: onSelected, + childDelegate: ListWheelChildBuilderDelegate( + builder: (context, index) { + return Center( + child: Text( + index.toString().padLeft(2, '0'), + style: TextStyle( + fontSize: 24, + color: isActive ? ColorsManager.grayColor : Colors.black, + ), + ), + ); + }, + childCount: itemCount, + ), + ), + ), + const SizedBox(width: 8), + Text( + label, + style: const TextStyle( + color: ColorsManager.grayColor, + fontSize: 18, + ), + ), + ], + ); + } +} diff --git a/lib/pages/device_managment/water_heater/widgets/inching_mode_buttons.dart b/lib/pages/device_managment/water_heater/widgets/inching_mode_buttons.dart new file mode 100644 index 00000000..8eec5cca --- /dev/null +++ b/lib/pages/device_managment/water_heater/widgets/inching_mode_buttons.dart @@ -0,0 +1,74 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:syncrow_web/utils/color_manager.dart'; +import 'package:syncrow_web/pages/common/buttons/default_button.dart'; +import 'package:syncrow_web/pages/device_managment/water_heater/bloc/water_heater_bloc.dart'; +import 'package:syncrow_web/utils/extension/build_context_x.dart'; + +class InchingModeButtons extends StatelessWidget { + final bool isActive; + final String deviceId; + final int hours; + final int minutes; + + const InchingModeButtons({ + Key? key, + required this.isActive, + required this.deviceId, + required this.hours, + required this.minutes, + }) : super(key: key); + + @override + Widget build(BuildContext context) { + return Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Expanded( + child: DefaultButton( + height: 40, + onPressed: () => Navigator.pop(context), + backgroundColor: ColorsManager.boxColor, + child: Text('Cancel', style: context.textTheme.bodyMedium), + ), + ), + const SizedBox(width: 20), + Expanded( + child: isActive + ? DefaultButton( + height: 40, + onPressed: () { + context + .read() + .add(StopScheduleEvent(deviceId)); + context.read().add( + ToggleWaterHeaterEvent( + deviceId: deviceId, + code: 'switch_inching', + value: 0, + ), + ); + }, + backgroundColor: Colors.red, + child: const Text('Stop'), + ) + : DefaultButton( + height: 40, + onPressed: () { + context.read().add( + ToggleWaterHeaterEvent( + deviceId: deviceId, + code: 'switch_inching', + value: Duration(hours: hours, minutes: minutes) + .inSeconds, + ), + ); + }, + backgroundColor: ColorsManager.primaryColor, + child: const Text('Save'), + ), + ), + ], + ); + } +} diff --git a/lib/pages/device_managment/water_heater/widgets/schedual_view.dart b/lib/pages/device_managment/water_heater/widgets/schedual_view.dart index 7ef86bac..09745eb3 100644 --- a/lib/pages/device_managment/water_heater/widgets/schedual_view.dart +++ b/lib/pages/device_managment/water_heater/widgets/schedual_view.dart @@ -1,13 +1,15 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; -import 'package:flutter_svg/flutter_svg.dart'; -import 'package:syncrow_web/pages/common/buttons/default_button.dart'; import 'package:syncrow_web/pages/device_managment/water_heater/bloc/water_heater_bloc.dart'; -import 'package:syncrow_web/pages/device_managment/water_heater/models/schedule_entry.dart'; +import 'package:syncrow_web/pages/device_managment/water_heater/helper/add_schedule_dialog_helper.dart'; import 'package:syncrow_web/pages/device_managment/water_heater/models/water_heater_status_model.dart'; -import 'package:syncrow_web/utils/color_manager.dart'; -import 'package:syncrow_web/utils/constants/assets.dart'; -import 'package:syncrow_web/utils/extension/build_context_x.dart'; +import 'package:syncrow_web/pages/device_managment/water_heater/widgets/count_down_button.dart'; +import 'package:syncrow_web/pages/device_managment/water_heater/widgets/count_down_inching_view.dart'; +import 'package:syncrow_web/pages/device_managment/water_heater/widgets/inching_mode_buttons.dart'; +import 'package:syncrow_web/pages/device_managment/water_heater/widgets/schedule_header.dart'; +import 'package:syncrow_web/pages/device_managment/water_heater/widgets/schedule_managment_ui.dart'; +import 'package:syncrow_web/pages/device_managment/water_heater/widgets/schedule_mode_buttons.dart'; +import 'package:syncrow_web/pages/device_managment/water_heater/widgets/schedule_mode_selector.dart'; class BuildScheduleView extends StatefulWidget { const BuildScheduleView({super.key, required this.status}); @@ -15,7 +17,7 @@ class BuildScheduleView extends StatefulWidget { final WaterHeaterStatusModel status; @override - _BuildScheduleViewState createState() => _BuildScheduleViewState(); + State createState() => _BuildScheduleViewState(); } class _BuildScheduleViewState extends State { @@ -42,20 +44,50 @@ class _BuildScheduleViewState extends State { return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - _scheduleHeader(context), + const ScheduleHeader(), const SizedBox(height: 20), - _buildScheduleModeSelector(context, state), + ScheduleModeSelector(state: state), const SizedBox(height: 20), if (state.scheduleMode == ScheduleModes.schedule) - _buildScheduleManagementUI(state), + ScheduleManagementUI( + state: state, + onAddSchedule: () => + ScheduleDialogHelper.showAddScheduleDialog( + context, + schedule: null, + index: null, + isEdit: false + ), + ), if (state.scheduleMode == ScheduleModes.countdown || state.scheduleMode == ScheduleModes.inching) - ..._buildCountDownAngInchingView(context, state), + CountdownInchingView(state: state), const SizedBox(height: 20), - _buildSaveStopCancelButtons(context, state), + if (state.scheduleMode == ScheduleModes.countdown) + CountdownModeButtons( + isActive: state.isCountdownActive ?? false, + deviceId: widget.status.uuid, + hours: state.countdownHours ?? 0, + minutes: state.countdownMinutes ?? 0, + ), + if (state.scheduleMode == ScheduleModes.inching) + InchingModeButtons( + isActive: state.isInchingActive ?? false, + deviceId: widget.status.uuid, + hours: state.inchingHours ?? 0, + minutes: state.inchingMinutes ?? 0, + ), + if (state.scheduleMode != ScheduleModes.countdown && + state.scheduleMode != ScheduleModes.inching) + ScheduleModeButtons( + onSave: () {}, + ), ], ); } + if (state is WaterHeaterLoadingState) { + return const Center(child: CircularProgressIndicator()); + } return const SizedBox(); }, ), @@ -65,673 +97,4 @@ class _BuildScheduleViewState extends State { ), ); } - - Row _scheduleHeader(BuildContext context) { - return Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - const SizedBox(), - Text( - 'Scheduling', - style: TextStyle( - fontWeight: FontWeight.bold, - fontSize: 22, - color: ColorsManager.dialogBlueTitle, - ), - ), - Container( - width: 25, - decoration: BoxDecoration( - color: Colors.transparent, - shape: BoxShape.circle, - border: Border.all( - color: Colors.grey, - width: 1.0, - ), - ), - child: IconButton( - padding: EdgeInsets.all(1), - icon: const Icon( - Icons.close, - color: Colors.grey, - size: 18, - ), - onPressed: () { - Navigator.of(context).pop(); - }, - ), - ), - ], - ); - } - - Widget _buildScheduleModeSelector( - BuildContext context, WaterHeaterDeviceStatusLoaded state) { - return Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - 'Type:', - style: context.textTheme.bodySmall!.copyWith( - fontSize: 13, - color: ColorsManager.grayColor, - ), - ), - const SizedBox(height: 4), - Row( - mainAxisAlignment: MainAxisAlignment.spaceEvenly, - children: [ - _buildRadioTile( - context, 'Countdown', ScheduleModes.countdown, state), - _buildRadioTile(context, 'Schedule', ScheduleModes.schedule, state), - _buildRadioTile( - context, 'Circulate', ScheduleModes.circulate, state), - _buildRadioTile(context, 'Inching', ScheduleModes.inching, state), - ], - ), - ], - ); - } - - Widget _buildRadioTile(BuildContext context, String label, ScheduleModes mode, - WaterHeaterDeviceStatusLoaded state) { - return Flexible( - child: ListTile( - contentPadding: EdgeInsets.zero, - title: Text( - label, - style: context.textTheme.bodySmall!.copyWith( - fontSize: 13, - color: ColorsManager.blackColor, - ), - ), - leading: Radio( - value: mode, - groupValue: state.scheduleMode, - onChanged: (ScheduleModes? value) { - if (value != null) { - context.read().add(UpdateScheduleEvent( - scheduleMode: value, - hours: state.hours ?? 0, - minutes: state.minutes ?? 0, - )); - } - }, - ), - ), - ); - } - - Widget _buildScheduleManagementUI(WaterHeaterDeviceStatusLoaded state) { - return Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - SizedBox( - width: 170, - height: 40, - child: DefaultButton( - borderColor: ColorsManager.boxColor, - padding: 2, - backgroundColor: ColorsManager.graysColor, - borderRadius: 15, - onPressed: () => - _showAddScheduleDialog(context, schedule: null, index: null), - child: Row( - children: [ - const Icon(Icons.add, color: ColorsManager.primaryColor), - Text( - ' Add new schedule', - style: context.textTheme.bodySmall!.copyWith( - color: ColorsManager.blackColor, - ), - ), - ], - ), - ), - ), - const SizedBox(height: 20), - _buildScheduleTable(state), - ], - ); - } - - Widget _buildScheduleTable(WaterHeaterDeviceStatusLoaded state) { - return Column( - children: [ - Table( - border: TableBorder.all( - color: ColorsManager.graysColor, - borderRadius: const BorderRadius.only( - topLeft: Radius.circular(20), topRight: Radius.circular(20)), - ), - children: [ - TableRow( - decoration: const BoxDecoration( - color: ColorsManager.boxColor, - borderRadius: BorderRadius.only( - topLeft: Radius.circular(20), - topRight: Radius.circular(20), - ), - ), - children: [ - _buildTableHeader('Active'), - _buildTableHeader('Days'), - _buildTableHeader('Time'), - _buildTableHeader('Function'), - _buildTableHeader('Action'), - ], - ), - ], - ), - Container( - height: 200, - decoration: BoxDecoration( - border: Border.all(color: ColorsManager.graysColor), - borderRadius: const BorderRadius.only( - bottomLeft: Radius.circular(20), - bottomRight: Radius.circular(20)), - ), - child: state.schedules.isEmpty - ? _buildEmptyState(context) - : _buildTableBody(state), - ), - ], - ); - } - - Widget _buildEmptyState(BuildContext context) { - return Center( - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - SvgPicture.asset(Assets.emptyRecords, width: 40, height: 40), - const SizedBox(height: 8), - Padding( - padding: const EdgeInsets.all(8.0), - child: Text( - 'No schedules added yet', - style: context.textTheme.bodySmall!.copyWith( - fontSize: 13, - color: ColorsManager.grayColor, - ), - ), - ), - ], - ), - ); - } - - Widget _buildTableBody(WaterHeaterDeviceStatusLoaded state) { - return SingleChildScrollView( - child: Table( - border: TableBorder.all(color: ColorsManager.graysColor), - defaultVerticalAlignment: TableCellVerticalAlignment.middle, - children: [ - for (int i = 0; i < state.schedules.length; i++) - _buildScheduleRow(state.schedules[i], i, context), - ], - ), - ); - } - - TableRow _buildScheduleRow( - ScheduleEntry schedule, int index, BuildContext context) { - return TableRow( - children: [ - Center( - child: schedule.functionOn - ? const Icon(Icons.radio_button_checked, - color: ColorsManager.blueColor) - : const Icon(Icons.radio_button_unchecked)), - Center(child: Text(_getSelectedDays(schedule.selectedDays))), - Center(child: Text(schedule.time.format(context))), - Center(child: Text(schedule.functionOn ? 'On' : 'Off')), - Center( - child: Wrap( - runAlignment: WrapAlignment.center, - children: [ - TextButton( - style: TextButton.styleFrom(padding: EdgeInsets.zero), - onPressed: () { - _showAddScheduleDialog(context, - schedule: schedule, index: index); - }, - child: Text( - 'Edit', - style: context.textTheme.bodySmall! - .copyWith(color: ColorsManager.blueColor), - ), - ), - TextButton( - style: TextButton.styleFrom(padding: EdgeInsets.zero), - onPressed: () { - context - .read() - .add(DeleteScheduleEvent(index)); - }, - child: Text( - 'Delete', - style: context.textTheme.bodySmall! - .copyWith(color: ColorsManager.blueColor), - ), - ), - ], - ), - ), - ], - ); - } - - String _getSelectedDays(List selectedDays) { - final days = ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun']; - List selectedDaysStr = []; - for (int i = 0; i < selectedDays.length; i++) { - if (selectedDays[i]) { - selectedDaysStr.add(days[i]); - } - } - return selectedDaysStr.join(', '); - } - - Widget _buildTableHeader(String label) { - return TableCell( - child: Padding( - padding: const EdgeInsets.all(12), - child: Text( - label, - style: context.textTheme.bodySmall!.copyWith( - fontSize: 13, - color: ColorsManager.grayColor, - ), - ), - ), - ); - } - - void _showAddScheduleDialog(BuildContext context, - {ScheduleEntry? schedule, int? index}) { - final bloc = context.read(); - - if (schedule != null) { - bloc.add(InitializeAddScheduleEvent( - selectedTime: schedule.time, - selectedDays: schedule.selectedDays, - functionOn: schedule.functionOn, - isEditing: true, - index: index, - )); - } else { - bloc.add( - const InitializeAddScheduleEvent( - selectedDays: [false, false, false, false, false, false, false], - functionOn: false, - isEditing: false, - index: null, - selectedTime: null, - ), - ); - } - - showDialog( - context: context, - builder: (ctx) { - return BlocProvider.value( - value: bloc, - child: BlocBuilder( - builder: (context, state) { - if (state is WaterHeaterDeviceStatusLoaded) { - return AlertDialog( - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(20), - ), - content: Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - const SizedBox(), - Text( - 'Scheduling', - style: context.textTheme.titleLarge!.copyWith( - color: ColorsManager.dialogBlueTitle, - fontWeight: FontWeight.bold, - ), - ), - const SizedBox(), - ], - ), - const SizedBox(height: 24), - SizedBox( - width: 150, - height: 40, - child: DefaultButton( - padding: 8, - backgroundColor: ColorsManager.boxColor, - borderRadius: 15, - onPressed: () async { - TimeOfDay? time = await showTimePicker( - context: context, - initialTime: - state.selectedTime ?? TimeOfDay.now(), - ); - if (time != null) { - bloc.add(UpdateSelectedTimeEvent(time)); - } - }, - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Text( - state.selectedTime == null - ? 'Time' - : state.selectedTime!.format(context), - style: context.textTheme.bodySmall!.copyWith( - color: ColorsManager.grayColor, - ), - ), - const Icon( - Icons.access_time, - color: ColorsManager.grayColor, - size: 18, - ), - ], - ), - ), - ), - const SizedBox(height: 16), - _buildDayCheckboxes(context, state.selectedDays), - const SizedBox(height: 16), - _buildFunctionSwitch(context, state.functionOn), - ], - ), - actions: [ - SizedBox( - width: 200, - child: DefaultButton( - height: 40, - onPressed: () { - Navigator.pop(context); - }, - backgroundColor: ColorsManager.boxColor, - child: Text( - 'Cancel', - style: context.textTheme.bodyMedium, - ), - ), - ), - SizedBox( - width: 200, - child: DefaultButton( - height: 40, - onPressed: () { - if (state.selectedTime != null) { - if (state.isEditing && index != null) { - bloc.add(UpdateScheduleEntryEvent( - deviceId: state.status.uuid, - category: 'kg', - functionOn: state.functionOn, - )); - } else { - bloc.add(AddScheduleEvent( - category: 'kg', - time: state.selectedTime!, - selectedDays: state.selectedDays, - functionOn: state.functionOn, - )); - } - Navigator.pop(context); - } - }, - backgroundColor: ColorsManager.primaryColor, - child: const Text('Save'), - ), - ), - ], - ); - } - return const SizedBox(); - }, - ), - ); - }, - ); - } - - Widget _buildDayCheckboxes(BuildContext context, List selectedDays) { - return Row( - children: List.generate(7, (index) { - final dayLabels = ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun']; - return Row( - children: [ - Checkbox( - value: selectedDays[index], - onChanged: (bool? value) { - context - .read() - .add(UpdateSelectedDayEvent(index, value!)); - }, - ), - Text(dayLabels[index]), - ], - ); - }), - ); - } - - Widget _buildFunctionSwitch(BuildContext context, bool isOn) { - return Row( - children: [ - Text( - 'Function:', - style: context.textTheme.bodySmall! - .copyWith(color: ColorsManager.grayColor), - ), - const SizedBox(width: 10), - Radio( - value: true, - groupValue: isOn, - onChanged: (bool? value) { - context - .read() - .add(const UpdateFunctionOnEvent(true)); - }, - ), - const Text('On'), - const SizedBox(width: 10), - Radio( - value: false, - groupValue: isOn, - onChanged: (bool? value) { - context - .read() - .add(const UpdateFunctionOnEvent(false)); - }, - ), - const Text('Off'), - ], - ); - } - - Center _buildSaveStopCancelButtons( - BuildContext context, WaterHeaterDeviceStatusLoaded state) { - return Center( - child: SizedBox( - width: 400, - height: 50, - child: Row( - mainAxisAlignment: MainAxisAlignment.center, - mainAxisSize: MainAxisSize.max, - children: [ - Expanded( - child: DefaultButton( - height: 40, - onPressed: () { - Navigator.pop(context); - }, - backgroundColor: ColorsManager.boxColor, - child: Text( - 'Cancel', - style: context.textTheme.bodyMedium, - ), - ), - ), - const SizedBox(width: 20), - Expanded( - child: - (state.countdownRemaining != null && state.isActive == true) - ? DefaultButton( - height: 40, - onPressed: () { - late String code; - if (state.scheduleMode == ScheduleModes.countdown) { - code = 'countdown_1'; - } else if (state.scheduleMode == - ScheduleModes.inching) { - code = 'switch_inching'; - } - context - .read() - .add(StopScheduleEvent(widget.status.uuid)); - context.read().add( - ToggleWaterHeaterEvent( - deviceId: widget.status.uuid, - code: code, - value: 0, - ), - ); - }, - backgroundColor: Colors.red, - child: const Text('Stop'), - ) - : DefaultButton( - height: 40, - onPressed: () { - late String code; - if (state.scheduleMode == ScheduleModes.countdown) { - code = 'countdown_1'; - } else if (state.scheduleMode == - ScheduleModes.inching) { - code = 'switch_inching'; - } - context.read().add( - ToggleWaterHeaterEvent( - deviceId: widget.status.uuid, - code: code, - value: Duration( - hours: state.hours ?? 0, - minutes: state.minutes ?? 0) - .inSeconds, - ), - ); - }, - backgroundColor: ColorsManager.primaryColor, - child: const Text('Save'), - ), - ), - ], - ), - ), - ); - } - - List _buildCountDownAngInchingView( - BuildContext context, WaterHeaterDeviceStatusLoaded state) { - final isCountDown = - state.scheduleMode?.name == ScheduleModes.countdown.name; - return [ - Text( - isCountDown ? 'Countdown:' : 'Inching:', - style: context.textTheme.bodySmall!.copyWith( - fontSize: 13, - color: ColorsManager.grayColor, - ), - ), - const SizedBox(height: 8), - Visibility( - visible: !isCountDown, - child: const Text( - 'Once enabled this feature, each time the device is turned on, it will automatically turn of after a period time as pre-set.'), - ), - const SizedBox(height: 8), - _hourMinutesWheel(state, context) - ]; - } - - Row _hourMinutesWheel( - WaterHeaterDeviceStatusLoaded state, BuildContext context) { - final isActive = - (state.countdownRemaining != null && state.isActive == true); - return Row( - mainAxisAlignment: MainAxisAlignment.start, - children: [ - _buildPickerColumn(context, 'h', state.hours ?? 0, 24, (value) { - context.read().add(UpdateScheduleEvent( - scheduleMode: state.scheduleMode ?? ScheduleModes.countdown, - hours: value, - minutes: state.minutes ?? 0, - )); - }, isActive: isActive), - const SizedBox(width: 10), - _buildPickerColumn(context, 'm', state.minutes ?? 0, 60, (value) { - context.read().add(UpdateScheduleEvent( - scheduleMode: state.scheduleMode ?? ScheduleModes.countdown, - hours: state.hours ?? 0, - minutes: value, - )); - }, isActive: isActive), - ], - ); - } - - Widget _buildPickerColumn(BuildContext context, String label, - int initialValue, int itemCount, ValueChanged onSelected, - {required bool isActive}) { - return Row( - mainAxisSize: MainAxisSize.min, - children: [ - Container( - height: 40, - width: 80, - padding: const EdgeInsets.symmetric(horizontal: 16), - decoration: BoxDecoration( - color: ColorsManager.boxColor, - borderRadius: BorderRadius.circular(8), - ), - child: ListWheelScrollView.useDelegate( - key: ValueKey('$label-$initialValue'), - controller: FixedExtentScrollController( - initialItem: initialValue, - ), - itemExtent: 40.0, - physics: const FixedExtentScrollPhysics(), - onSelectedItemChanged: onSelected, - childDelegate: ListWheelChildBuilderDelegate( - builder: (context, index) { - return Center( - child: Text( - index.toString().padLeft(2, '0'), - style: TextStyle( - fontSize: 24, - color: isActive ? ColorsManager.grayColor : Colors.black, - ), - ), - ); - }, - childCount: itemCount, - ), - ), - ), - const SizedBox(width: 8), - Text( - label, - style: const TextStyle( - color: ColorsManager.grayColor, - fontSize: 18, - ), - ), - ], - ); - } } diff --git a/lib/pages/device_managment/water_heater/widgets/schedule_header.dart b/lib/pages/device_managment/water_heater/widgets/schedule_header.dart new file mode 100644 index 00000000..87afe430 --- /dev/null +++ b/lib/pages/device_managment/water_heater/widgets/schedule_header.dart @@ -0,0 +1,46 @@ +import 'package:flutter/material.dart'; +import 'package:syncrow_web/utils/color_manager.dart'; + +class ScheduleHeader extends StatelessWidget { + const ScheduleHeader({super.key}); + + @override + Widget build(BuildContext context) { + return Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + const SizedBox(), + Text( + 'Scheduling', + style: TextStyle( + fontWeight: FontWeight.bold, + fontSize: 22, + color: ColorsManager.dialogBlueTitle, + ), + ), + Container( + width: 25, + decoration: BoxDecoration( + color: Colors.transparent, + shape: BoxShape.circle, + border: Border.all( + color: Colors.grey, + width: 1.0, + ), + ), + child: IconButton( + padding: const EdgeInsets.all(1), + icon: const Icon( + Icons.close, + color: Colors.grey, + size: 18, + ), + onPressed: () { + Navigator.of(context).pop(); + }, + ), + ), + ], + ); + } +} diff --git a/lib/pages/device_managment/water_heater/widgets/schedule_managment_ui.dart b/lib/pages/device_managment/water_heater/widgets/schedule_managment_ui.dart new file mode 100644 index 00000000..1710c439 --- /dev/null +++ b/lib/pages/device_managment/water_heater/widgets/schedule_managment_ui.dart @@ -0,0 +1,50 @@ +import 'package:flutter/material.dart'; +import 'package:syncrow_web/pages/common/buttons/default_button.dart'; +import 'package:syncrow_web/pages/device_managment/water_heater/bloc/water_heater_bloc.dart'; +import 'package:syncrow_web/pages/device_managment/water_heater/widgets/schedule_table.dart'; +import 'package:syncrow_web/utils/color_manager.dart'; +import 'package:syncrow_web/utils/extension/build_context_x.dart'; + +class ScheduleManagementUI extends StatelessWidget { + final WaterHeaterDeviceStatusLoaded state; + final Function onAddSchedule; + + const ScheduleManagementUI({ + super.key, + required this.state, + required this.onAddSchedule, + }); + + @override + Widget build(BuildContext context) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + SizedBox( + width: 170, + height: 40, + child: DefaultButton( + borderColor: ColorsManager.boxColor, + padding: 2, + backgroundColor: ColorsManager.graysColor, + borderRadius: 15, + onPressed: () => onAddSchedule(), + child: Row( + children: [ + const Icon(Icons.add, color: ColorsManager.primaryColor), + Text( + ' Add new schedule', + style: context.textTheme.bodySmall!.copyWith( + color: ColorsManager.blackColor, + ), + ), + ], + ), + ), + ), + const SizedBox(height: 20), + ScheduleTableWidget(state: state), + ], + ); + } +} diff --git a/lib/pages/device_managment/water_heater/widgets/schedule_mode_buttons.dart b/lib/pages/device_managment/water_heater/widgets/schedule_mode_buttons.dart new file mode 100644 index 00000000..f1307d5f --- /dev/null +++ b/lib/pages/device_managment/water_heater/widgets/schedule_mode_buttons.dart @@ -0,0 +1,45 @@ +import 'package:flutter/material.dart'; +import 'package:syncrow_web/pages/common/buttons/default_button.dart'; +import 'package:syncrow_web/utils/color_manager.dart'; +import 'package:syncrow_web/utils/extension/build_context_x.dart'; + +class ScheduleModeButtons extends StatelessWidget { + final VoidCallback onSave; + + const ScheduleModeButtons({ + super.key, + required this.onSave, + }); + + @override + Widget build(BuildContext context) { + return Row( + mainAxisAlignment: MainAxisAlignment.center, + mainAxisSize: MainAxisSize.max, + children: [ + Expanded( + child: DefaultButton( + height: 40, + onPressed: () { + Navigator.pop(context); + }, + backgroundColor: ColorsManager.boxColor, + child: Text( + 'Cancel', + style: context.textTheme.bodyMedium, + ), + ), + ), + const SizedBox(width: 20), + Expanded( + child: DefaultButton( + height: 40, + onPressed: onSave, + backgroundColor: ColorsManager.primaryColor, + child: const Text('Save'), + ), + ), + ], + ); + } +} diff --git a/lib/pages/device_managment/water_heater/widgets/schedule_mode_selector.dart b/lib/pages/device_managment/water_heater/widgets/schedule_mode_selector.dart new file mode 100644 index 00000000..7afd96de --- /dev/null +++ b/lib/pages/device_managment/water_heater/widgets/schedule_mode_selector.dart @@ -0,0 +1,86 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:syncrow_web/pages/device_managment/water_heater/bloc/water_heater_bloc.dart'; +import 'package:syncrow_web/utils/color_manager.dart'; +import 'package:syncrow_web/utils/extension/build_context_x.dart'; +import 'package:syncrow_web/pages/device_managment/water_heater/models/water_heater_status_model.dart'; + +class ScheduleModeSelector extends StatelessWidget { + final WaterHeaterDeviceStatusLoaded state; + + const ScheduleModeSelector({super.key, required this.state}); + + @override + Widget build(BuildContext context) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'Type:', + style: context.textTheme.bodySmall!.copyWith( + fontSize: 13, + color: ColorsManager.grayColor, + ), + ), + const SizedBox(height: 4), + Row( + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + children: [ + _buildRadioTile( + context, 'Countdown', ScheduleModes.countdown, state), + _buildRadioTile(context, 'Schedule', ScheduleModes.schedule, state), + _buildRadioTile( + context, 'Circulate', ScheduleModes.circulate, state), + _buildRadioTile(context, 'Inching', ScheduleModes.inching, state), + ], + ), + ], + ); + } + + Widget _buildRadioTile(BuildContext context, String label, ScheduleModes mode, + WaterHeaterDeviceStatusLoaded state) { + return Flexible( + child: ListTile( + contentPadding: EdgeInsets.zero, + title: Text( + label, + style: context.textTheme.bodySmall!.copyWith( + fontSize: 13, + color: ColorsManager.blackColor, + ), + ), + leading: Radio( + value: mode, + groupValue: state.scheduleMode, + onChanged: (ScheduleModes? value) { + if (value != null) { + if (value == ScheduleModes.countdown) { + context.read().add(UpdateScheduleEvent( + scheduleMode: value, + hours: state.countdownHours ?? 0, + minutes: state.countdownMinutes ?? 0, + )); + } else if (value == ScheduleModes.inching) { + context.read().add(UpdateScheduleEvent( + scheduleMode: value, + hours: state.inchingHours ?? 0, + minutes: state.inchingMinutes ?? 0, + )); + } + + if (value == ScheduleModes.schedule) { + context.read().add( + GetSchedulesEvent( + category: 'kg', + uuid: state.status.uuid, + ), + ); + } + } + }, + ), + ), + ); + } +} diff --git a/lib/pages/device_managment/water_heater/widgets/schedule_row_widget.dart b/lib/pages/device_managment/water_heater/widgets/schedule_row_widget.dart new file mode 100644 index 00000000..760b86c7 --- /dev/null +++ b/lib/pages/device_managment/water_heater/widgets/schedule_row_widget.dart @@ -0,0 +1,76 @@ +import 'package:flutter/material.dart'; +import 'package:syncrow_web/pages/device_managment/water_heater/models/schedule_model.dart'; +import 'package:syncrow_web/utils/format_date_time.dart'; +import 'package:syncrow_web/utils/color_manager.dart'; + +class ScheduleRowWidget extends StatelessWidget { + final ScheduleModel schedule; + final int index; + final Function onEdit; + final Function onDelete; + + const ScheduleRowWidget({ + super.key, + required this.schedule, + required this.index, + required this.onEdit, + required this.onDelete, + }); + + @override + Widget build(BuildContext context) { + return Table( + border: TableBorder.all(color: ColorsManager.graysColor), + defaultVerticalAlignment: TableCellVerticalAlignment.middle, + children: [ + TableRow( + children: [ + Center( + child: schedule.function.value + ? const Icon(Icons.radio_button_checked, + color: ColorsManager.blueColor) + : const Icon(Icons.radio_button_unchecked), + ), + Center(child: Text(_getSelectedDays(schedule.selectedDays ?? []))), + Center(child: Text(formatIsoStringToTime(schedule.time))), + Center(child: Text(schedule.function.value ? 'On' : 'Off')), + Center( + child: Wrap( + runAlignment: WrapAlignment.center, + children: [ + TextButton( + style: TextButton.styleFrom(padding: EdgeInsets.zero), + onPressed: () => onEdit(), + child: const Text( + 'Edit', + style: TextStyle(color: ColorsManager.blueColor), + ), + ), + TextButton( + style: TextButton.styleFrom(padding: EdgeInsets.zero), + onPressed: () => onDelete(), + child: const Text( + 'Delete', + style: TextStyle(color: ColorsManager.blueColor), + ), + ), + ], + ), + ), + ], + ), + ], + ); + } + + String _getSelectedDays(List selectedDays) { + final days = ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun']; + List selectedDaysStr = []; + for (int i = 0; i < selectedDays.length; i++) { + if (selectedDays[i]) { + selectedDaysStr.add(days[i]); + } + } + return selectedDaysStr.join(', '); + } +} diff --git a/lib/pages/device_managment/water_heater/widgets/schedule_table.dart b/lib/pages/device_managment/water_heater/widgets/schedule_table.dart new file mode 100644 index 00000000..b997c7e0 --- /dev/null +++ b/lib/pages/device_managment/water_heater/widgets/schedule_table.dart @@ -0,0 +1,198 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:flutter_svg/flutter_svg.dart'; +import 'package:syncrow_web/pages/device_managment/water_heater/bloc/water_heater_bloc.dart'; +import 'package:syncrow_web/pages/device_managment/water_heater/models/schedule_model.dart'; +import 'package:syncrow_web/utils/color_manager.dart'; +import 'package:syncrow_web/utils/constants/assets.dart'; +import 'package:syncrow_web/utils/extension/build_context_x.dart'; +import 'package:syncrow_web/utils/format_date_time.dart'; + +import '../helper/add_schedule_dialog_helper.dart'; + +class ScheduleTableWidget extends StatelessWidget { + final WaterHeaterDeviceStatusLoaded state; + + const ScheduleTableWidget({ + super.key, + required this.state, + }); + + @override + Widget build(BuildContext context) { + return Column( + children: [ + Table( + border: TableBorder.all( + color: ColorsManager.graysColor, + borderRadius: const BorderRadius.only( + topLeft: Radius.circular(20), topRight: Radius.circular(20)), + ), + children: [ + TableRow( + decoration: const BoxDecoration( + color: ColorsManager.boxColor, + borderRadius: BorderRadius.only( + topLeft: Radius.circular(20), + topRight: Radius.circular(20), + ), + ), + children: [ + _buildTableHeader('Active'), + _buildTableHeader('Days'), + _buildTableHeader('Time'), + _buildTableHeader('Function'), + _buildTableHeader('Action'), + ], + ), + ], + ), + BlocBuilder( + builder: (context, state) { + if (state is ScheduleLoadingState) { + return const SizedBox( + height: 200, + child: Center(child: CircularProgressIndicator())); + } + if (state is WaterHeaterDeviceStatusLoaded && + state.schedules.isEmpty) { + return _buildEmptyState(context); + } else if (state is WaterHeaterDeviceStatusLoaded) { + return Container( + height: 200, + decoration: BoxDecoration( + border: Border.all(color: ColorsManager.graysColor), + borderRadius: const BorderRadius.only( + bottomLeft: Radius.circular(20), + bottomRight: Radius.circular(20)), + ), + child: _buildTableBody(state, context)); + } + return const SizedBox(); + }, + ), + ], + ); + } + + Widget _buildEmptyState(BuildContext context) { + return Container( + height: 200, + decoration: BoxDecoration( + border: Border.all(color: ColorsManager.graysColor), + borderRadius: const BorderRadius.only( + bottomLeft: Radius.circular(20), bottomRight: Radius.circular(20)), + ), + child: Center( + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + SvgPicture.asset(Assets.emptyRecords, width: 40, height: 40), + const SizedBox(height: 8), + Padding( + padding: const EdgeInsets.all(8.0), + child: Text( + 'No schedules added yet', + style: context.textTheme.bodySmall!.copyWith( + fontSize: 13, + color: ColorsManager.grayColor, + ), + ), + ), + ], + ), + ), + ); + } + + Widget _buildTableBody( + WaterHeaterDeviceStatusLoaded state, BuildContext context) { + return SingleChildScrollView( + child: Table( + border: TableBorder.all(color: ColorsManager.graysColor), + defaultVerticalAlignment: TableCellVerticalAlignment.middle, + children: [ + for (int i = 0; i < state.schedules.length; i++) + _buildScheduleRow(state.schedules[i], i, context), + ], + ), + ); + } + + Widget _buildTableHeader(String label) { + return TableCell( + child: Padding( + padding: const EdgeInsets.all(12), + child: Text( + label, + style: const TextStyle( + fontSize: 13, + color: ColorsManager.grayColor, + ), + ), + ), + ); + } + + TableRow _buildScheduleRow( + ScheduleModel schedule, int index, BuildContext context) { + return TableRow( + children: [ + Center( + child: schedule.function.value + ? const Icon(Icons.radio_button_checked, + color: ColorsManager.blueColor) + : const Icon(Icons.radio_button_unchecked)), + Center( + child: Text(_getSelectedDays( + ScheduleModel.parseSelectedDays(schedule.days)))), + Center(child: Text(formatIsoStringToTime(schedule.time))), + Center(child: Text(schedule.function.value ? 'On' : 'Off')), + Center( + child: Wrap( + runAlignment: WrapAlignment.center, + children: [ + TextButton( + style: TextButton.styleFrom(padding: EdgeInsets.zero), + onPressed: () { + ScheduleDialogHelper.showAddScheduleDialog(context, + schedule: schedule, index: index, isEdit: true); + }, + child: Text( + 'Edit', + style: context.textTheme.bodySmall! + .copyWith(color: ColorsManager.blueColor), + ), + ), + TextButton( + style: TextButton.styleFrom(padding: EdgeInsets.zero), + onPressed: () { + context.read().add(DeleteScheduleEvent( + index: index, + scheduleId: schedule.scheduleId, + )); + }, + child: Text( + 'Delete', + style: context.textTheme.bodySmall! + .copyWith(color: ColorsManager.blueColor), + ), + ), + ], + ), + ), + ], + ); + } + + String _getSelectedDays(List selectedDays) { + final days = ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun']; + List selectedDaysStr = []; + for (int i = 0; i < selectedDays.length; i++) { + if (selectedDays[i]) { + selectedDaysStr.add(days[i]); + } + } + return selectedDaysStr.join(', '); + } +} diff --git a/lib/services/devices_mang_api.dart b/lib/services/devices_mang_api.dart index 049538d3..9ac42eca 100644 --- a/lib/services/devices_mang_api.dart +++ b/lib/services/devices_mang_api.dart @@ -195,6 +195,29 @@ class DevicesManagementApi { } } + Future> getDeviceSchedules( + String uuid, String category) async { + try { + final response = await HTTPService().get( + path: ApiEndpoints.scheduleByDeviceId + .replaceAll('{deviceUuid}', uuid) + .replaceAll('{category}', category), + showServerMessage: true, + expectedResponseModel: (json) { + List schedules = []; + for (var schedule in json['schedules']) { + schedules.add(ScheduleModel.fromJson(schedule)); + } + return schedules; + }, + ); + return response; + } catch (e) { + debugPrint('Error fetching $e'); + return []; + } + } + Future updateScheduleRecord( {required bool enable, required String uuid, diff --git a/lib/utils/constants/api_const.dart b/lib/utils/constants/api_const.dart index 0c17e2bc..2b39fc03 100644 --- a/lib/utils/constants/api_const.dart +++ b/lib/utils/constants/api_const.dart @@ -40,6 +40,8 @@ abstract class ApiEndpoints { '/device/report-logs/{uuid}?code={code}&startTime={startTime}&endTime={endTime}'; static const String scheduleByDeviceId = '/schedule/{deviceUuid}'; + static const String getScheduleByDeviceId = + '/schedule/{deviceUuid}?category={category}'; static const String deleteScheduleByDeviceId = '/schedule/{deviceUuid}/{scheduleUuid}'; static const String updateScheduleByDeviceId = diff --git a/lib/utils/format_date_time.dart b/lib/utils/format_date_time.dart index e214b46c..470acb97 100644 --- a/lib/utils/format_date_time.dart +++ b/lib/utils/format_date_time.dart @@ -23,4 +23,9 @@ String formatTimeOfDayToISO(TimeOfDay time, {DateTime? currentDate}) { ); return dateTime.toUtc().toIso8601String(); -} \ No newline at end of file +} + +String formatIsoStringToTime(String isoString) { + final dateTime = DateTime.parse(isoString); + return DateFormat('hh:mm a').format(dateTime); +} From 2955533209ae2d120cf66ef3d377ece389ed7b2b Mon Sep 17 00:00:00 2001 From: ashrafzarkanisala Date: Sun, 22 Sep 2024 21:56:17 +0300 Subject: [PATCH 07/10] push factory reset logic and call for all devices --- .../device_managment/ac/bloc/ac_bloc.dart | 19 +++++++ .../device_managment/ac/bloc/ac_event.dart | 14 +++++ .../ac/view/ac_device_batch_control.dart | 11 +++- .../models/factory_reset_model.dart | 55 +++++++++++++++++++ .../ceiling_sensor/bloc/bloc.dart | 23 +++++++- .../ceiling_sensor/bloc/event.dart | 14 +++++ .../view/ceiling_sensor_batch_control.dart | 13 ++++- .../curtain/bloc/curtain_bloc.dart | 19 +++++++ .../curtain/bloc/curtain_event.dart | 12 ++++ .../view/curtain_batch_status_view.dart | 12 +++- .../door_lock/bloc/door_lock_bloc.dart | 19 +++++++ .../door_lock/bloc/door_lock_event.dart | 12 ++++ .../view/door_lock_batch_control_view.dart | 15 ++++- .../gateway/bloc/gate_way_bloc.dart | 20 +++++++ .../gateway/bloc/gate_way_event.dart | 12 ++++ .../gateway/view/gateway_batch_control.dart | 13 ++++- .../bloc/wall_light_switch_bloc.dart | 19 +++++++ .../bloc/wall_light_switch_event.dart | 11 ++++ .../view/wall_light_batch_control.dart | 9 ++- .../shared/batch_control/factory_reset.dart | 53 ++++++++++-------- .../bloc/living_room_bloc.dart | 20 +++++++ .../bloc/living_room_event.dart | 9 +++ .../view/living_room_batch_controls.dart | 10 +++- .../bloc/two_gang_switch_bloc.dart | 19 +++++++ .../bloc/two_gang_switch_event.dart | 11 ++++ .../view/wall_light_batch_control.dart | 10 +++- .../wall_sensor/bloc/bloc.dart | 19 +++++++ .../wall_sensor/bloc/event.dart | 11 ++++ .../view/wall_sensor_batch_control.dart | 12 +++- lib/services/devices_mang_api.dart | 18 ++++++ lib/utils/constants/api_const.dart | 2 + 31 files changed, 481 insertions(+), 35 deletions(-) create mode 100644 lib/pages/device_managment/all_devices/models/factory_reset_model.dart diff --git a/lib/pages/device_managment/ac/bloc/ac_bloc.dart b/lib/pages/device_managment/ac/bloc/ac_bloc.dart index 54f36889..7c6ee628 100644 --- a/lib/pages/device_managment/ac/bloc/ac_bloc.dart +++ b/lib/pages/device_managment/ac/bloc/ac_bloc.dart @@ -18,6 +18,7 @@ class AcBloc extends Bloc { on(_onFetchAcBatchStatus); on(_onAcControl); on(_onAcBatchControl); + on(_onFactoryReset); } FutureOr _onFetchAcStatus( @@ -184,4 +185,22 @@ class AcBloc extends Bloc { emit: emit, ); } + + FutureOr _onFactoryReset( + AcFactoryResetEvent event, Emitter emit) async { + emit(AcsLoadingState()); + try { + final response = await DevicesManagementApi().factoryReset( + event.factoryResetModel, + event.deviceId, + ); + if (!response) { + emit(const AcsFailedState(error: 'Failed')); + } else { + add(AcFetchDeviceStatusEvent(event.deviceId)); + } + } catch (e) { + emit(AcsFailedState(error: e.toString())); + } + } } diff --git a/lib/pages/device_managment/ac/bloc/ac_event.dart b/lib/pages/device_managment/ac/bloc/ac_event.dart index acb81d95..8d49df96 100644 --- a/lib/pages/device_managment/ac/bloc/ac_event.dart +++ b/lib/pages/device_managment/ac/bloc/ac_event.dart @@ -1,4 +1,5 @@ import 'package:equatable/equatable.dart'; +import 'package:syncrow_web/pages/device_managment/all_devices/models/factory_reset_model.dart'; sealed class AcsEvent extends Equatable { const AcsEvent(); @@ -54,3 +55,16 @@ class AcBatchControlEvent extends AcsEvent { @override List get props => [devicesIds, code, value]; } + +class AcFactoryResetEvent extends AcsEvent { + final String deviceId; + final FactoryResetModel factoryResetModel; + + const AcFactoryResetEvent({ + required this.deviceId, + required this.factoryResetModel, + }); + + @override + List get props => [deviceId, factoryResetModel]; +} diff --git a/lib/pages/device_managment/ac/view/ac_device_batch_control.dart b/lib/pages/device_managment/ac/view/ac_device_batch_control.dart index edda3c5d..987084fa 100644 --- a/lib/pages/device_managment/ac/view/ac_device_batch_control.dart +++ b/lib/pages/device_managment/ac/view/ac_device_batch_control.dart @@ -6,6 +6,7 @@ import 'package:syncrow_web/pages/device_managment/ac/bloc/ac_state.dart'; import 'package:syncrow_web/pages/device_managment/ac/view/batch_control_list/batch_ac_mode.dart'; import 'package:syncrow_web/pages/device_managment/ac/view/batch_control_list/batch_current_temp.dart'; import 'package:syncrow_web/pages/device_managment/ac/view/batch_control_list/batch_fan_speed.dart'; +import 'package:syncrow_web/pages/device_managment/all_devices/models/factory_reset_model.dart'; import 'package:syncrow_web/pages/device_managment/shared/batch_control/factory_reset.dart'; import 'package:syncrow_web/pages/device_managment/shared/batch_control/firmware_update.dart'; import 'package:syncrow_web/pages/device_managment/shared/toggle_widget.dart'; @@ -89,7 +90,15 @@ class AcDeviceBatchControlView extends StatelessWidget }, ), FirmwareUpdateWidget(deviceId: devicesIds.first, version: 5), - FactoryResetWidget(deviceId: devicesIds.first), + FactoryResetWidget( + callFactoryReset: () { + context.read().add(AcFactoryResetEvent( + deviceId: state.status.uuid, + factoryResetModel: + FactoryResetModel(devicesUuid: devicesIds), + )); + }, + ), ], ); } else if (state is AcsLoadingState) { diff --git a/lib/pages/device_managment/all_devices/models/factory_reset_model.dart b/lib/pages/device_managment/all_devices/models/factory_reset_model.dart new file mode 100644 index 00000000..aec14d16 --- /dev/null +++ b/lib/pages/device_managment/all_devices/models/factory_reset_model.dart @@ -0,0 +1,55 @@ +import 'package:flutter/foundation.dart'; + +class FactoryResetModel { + final List devicesUuid; + + FactoryResetModel({ + required this.devicesUuid, + }); + + factory FactoryResetModel.fromJson(Map json) { + return FactoryResetModel( + devicesUuid: List.from(json['devicesUuid']), + ); + } + + Map toJson() { + return { + 'devicesUuid': devicesUuid, + }; + } + + FactoryResetModel copyWith({ + List? devicesUuid, + }) { + return FactoryResetModel( + devicesUuid: devicesUuid ?? this.devicesUuid, + ); + } + + Map toMap() { + return { + 'devicesUuid': devicesUuid, + }; + } + + factory FactoryResetModel.fromMap(Map map) { + return FactoryResetModel( + devicesUuid: List.from(map['devicesUuid']), + ); + } + + @override + String toString() => 'FactoryReset(devicesUuid: $devicesUuid)'; + + @override + bool operator ==(Object other) { + if (identical(this, other)) return true; + + return other is FactoryResetModel && + listEquals(other.devicesUuid, devicesUuid); + } + + @override + int get hashCode => devicesUuid.hashCode; +} diff --git a/lib/pages/device_managment/ceiling_sensor/bloc/bloc.dart b/lib/pages/device_managment/ceiling_sensor/bloc/bloc.dart index ba8e4114..a2382ba6 100644 --- a/lib/pages/device_managment/ceiling_sensor/bloc/bloc.dart +++ b/lib/pages/device_managment/ceiling_sensor/bloc/bloc.dart @@ -20,14 +20,15 @@ class CeilingSensorBloc extends Bloc { on(_getDeviceReports); on(_showDescription); on(_backToGridView); + on(_onFactoryReset); } void _fetchCeilingSensorStatus( CeilingInitialEvent event, Emitter emit) async { emit(CeilingLoadingInitialState()); try { - var response = await DevicesManagementApi() - .getDeviceStatus(event.deviceId); + var response = + await DevicesManagementApi().getDeviceStatus(event.deviceId); deviceStatus = CeilingSensorModel.fromJson(response.status); emit(CeilingUpdateState(ceilingSensorModel: deviceStatus)); // _listenToChanges(); @@ -188,4 +189,22 @@ class CeilingSensorBloc extends Bloc { return; } } + + FutureOr _onFactoryReset( + CeilingFactoryResetEvent event, Emitter emit) async { + emit(CeilingLoadingNewSate(ceilingSensorModel: deviceStatus)); + try { + final response = await DevicesManagementApi().factoryReset( + event.factoryResetModel, + event.devicesId, + ); + if (!response) { + emit(const CeilingFailedState(error: 'Failed')); + } else { + emit(CeilingUpdateState(ceilingSensorModel: deviceStatus)); + } + } catch (e) { + emit(CeilingFailedState(error: e.toString())); + } + } } diff --git a/lib/pages/device_managment/ceiling_sensor/bloc/event.dart b/lib/pages/device_managment/ceiling_sensor/bloc/event.dart index c1efa47c..582c9836 100644 --- a/lib/pages/device_managment/ceiling_sensor/bloc/event.dart +++ b/lib/pages/device_managment/ceiling_sensor/bloc/event.dart @@ -1,4 +1,5 @@ import 'package:equatable/equatable.dart'; +import 'package:syncrow_web/pages/device_managment/all_devices/models/factory_reset_model.dart'; abstract class CeilingSensorEvent extends Equatable { const CeilingSensorEvent(); @@ -69,3 +70,16 @@ class ShowCeilingDescriptionEvent extends CeilingSensorEvent { } class BackToCeilingGridViewEvent extends CeilingSensorEvent {} + +class CeilingFactoryResetEvent extends CeilingSensorEvent { + final String devicesId; + final FactoryResetModel factoryResetModel; + + const CeilingFactoryResetEvent({ + required this.devicesId, + required this.factoryResetModel, + }); + + @override + List get props => [devicesId, factoryResetModel]; +} diff --git a/lib/pages/device_managment/ceiling_sensor/view/ceiling_sensor_batch_control.dart b/lib/pages/device_managment/ceiling_sensor/view/ceiling_sensor_batch_control.dart index f95852a5..a6e60c8f 100644 --- a/lib/pages/device_managment/ceiling_sensor/view/ceiling_sensor_batch_control.dart +++ b/lib/pages/device_managment/ceiling_sensor/view/ceiling_sensor_batch_control.dart @@ -1,5 +1,6 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:syncrow_web/pages/device_managment/all_devices/models/factory_reset_model.dart'; import 'package:syncrow_web/pages/device_managment/ceiling_sensor/bloc/bloc.dart'; import 'package:syncrow_web/pages/device_managment/ceiling_sensor/bloc/event.dart'; import 'package:syncrow_web/pages/device_managment/ceiling_sensor/bloc/state.dart'; @@ -112,7 +113,17 @@ class CeilingSensorBatchControlView extends StatelessWidget ), ), FirmwareUpdateWidget(deviceId: devicesIds.first, version: 4), - FactoryResetWidget(deviceId: devicesIds.first), + FactoryResetWidget( + callFactoryReset: () { + context.read().add( + CeilingFactoryResetEvent( + devicesId: devicesIds.first, + factoryResetModel: + FactoryResetModel(devicesUuid: devicesIds), + ), + ); + }, + ), ], ); } diff --git a/lib/pages/device_managment/curtain/bloc/curtain_bloc.dart b/lib/pages/device_managment/curtain/bloc/curtain_bloc.dart index eb031552..4599f360 100644 --- a/lib/pages/device_managment/curtain/bloc/curtain_bloc.dart +++ b/lib/pages/device_managment/curtain/bloc/curtain_bloc.dart @@ -15,6 +15,7 @@ class CurtainBloc extends Bloc { on(_onFetchBatchStatus); on(_onCurtainControl); on(_onCurtainBatchControl); + on(_onFactoryReset); } FutureOr _onFetchDeviceStatus( @@ -139,4 +140,22 @@ class CurtainBloc extends Bloc { isBatch: true, ); } + + FutureOr _onFactoryReset( + CurtainFactoryReset event, Emitter emit) async { + emit(CurtainStatusLoading()); + try { + final response = await DevicesManagementApi().factoryReset( + event.factoryReset, + event.deviceId, + ); + if (!response) { + emit(const CurtainControlError('Failed')); + } else { + add(CurtainFetchDeviceStatus(event.deviceId)); + } + } catch (e) { + emit(CurtainControlError(e.toString())); + } + } } diff --git a/lib/pages/device_managment/curtain/bloc/curtain_event.dart b/lib/pages/device_managment/curtain/bloc/curtain_event.dart index 8ef85145..7236016c 100644 --- a/lib/pages/device_managment/curtain/bloc/curtain_event.dart +++ b/lib/pages/device_managment/curtain/bloc/curtain_event.dart @@ -1,4 +1,5 @@ import 'package:equatable/equatable.dart'; +import 'package:syncrow_web/pages/device_managment/all_devices/models/factory_reset_model.dart'; sealed class CurtainEvent extends Equatable { const CurtainEvent(); @@ -48,3 +49,14 @@ class CurtainBatchControl extends CurtainEvent { @override List get props => [devicesIds, code, value]; } + +class CurtainFactoryReset extends CurtainEvent { + final String deviceId; + final FactoryResetModel factoryReset; + + const CurtainFactoryReset( + {required this.deviceId, required this.factoryReset}); + + @override + List get props => [deviceId, factoryReset]; +} diff --git a/lib/pages/device_managment/curtain/view/curtain_batch_status_view.dart b/lib/pages/device_managment/curtain/view/curtain_batch_status_view.dart index ec1a0076..b558c837 100644 --- a/lib/pages/device_managment/curtain/view/curtain_batch_status_view.dart +++ b/lib/pages/device_managment/curtain/view/curtain_batch_status_view.dart @@ -1,6 +1,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:syncrow_web/pages/common/curtain_toggle.dart'; +import 'package:syncrow_web/pages/device_managment/all_devices/models/factory_reset_model.dart'; import 'package:syncrow_web/pages/device_managment/curtain/bloc/curtain_bloc.dart'; import 'package:syncrow_web/pages/device_managment/curtain/bloc/curtain_event.dart'; import 'package:syncrow_web/pages/device_managment/curtain/bloc/curtain_state.dart'; @@ -68,7 +69,16 @@ class CurtainBatchStatusView extends StatelessWidget }, ), FirmwareUpdateWidget(deviceId: devicesIds.first, version: 5), - FactoryResetWidget(deviceId: devicesIds.first), + FactoryResetWidget( + callFactoryReset: () { + context.read().add( + CurtainFactoryReset( + deviceId: devicesIds.first, + factoryReset: FactoryResetModel(devicesUuid: devicesIds), + ), + ); + }, + ), ], ); } diff --git a/lib/pages/device_managment/door_lock/bloc/door_lock_bloc.dart b/lib/pages/device_managment/door_lock/bloc/door_lock_bloc.dart index 8ad2a05c..c50203f3 100644 --- a/lib/pages/device_managment/door_lock/bloc/door_lock_bloc.dart +++ b/lib/pages/device_managment/door_lock/bloc/door_lock_bloc.dart @@ -17,6 +17,7 @@ class DoorLockBloc extends Bloc { on(_onFetchDeviceStatus); //on(_onDoorLockControl); on(_updateLock); + on(_onFactoryReset); } FutureOr _onFetchDeviceStatus( @@ -113,4 +114,22 @@ class DoorLockBloc extends Bloc { return null; } } + + FutureOr _onFactoryReset( + DoorLockFactoryReset event, Emitter emit) async { + emit(DoorLockStatusLoading()); + try { + final response = await DevicesManagementApi().factoryReset( + event.factoryReset, + event.deviceId, + ); + if (!response) { + emit(const DoorLockControlError('Failed')); + } else { + add(DoorLockFetchStatus(event.deviceId)); + } + } catch (e) { + emit(DoorLockControlError(e.toString())); + } + } } diff --git a/lib/pages/device_managment/door_lock/bloc/door_lock_event.dart b/lib/pages/device_managment/door_lock/bloc/door_lock_event.dart index 4033676f..54fa1ddf 100644 --- a/lib/pages/device_managment/door_lock/bloc/door_lock_event.dart +++ b/lib/pages/device_managment/door_lock/bloc/door_lock_event.dart @@ -1,4 +1,5 @@ import 'package:equatable/equatable.dart'; +import 'package:syncrow_web/pages/device_managment/all_devices/models/factory_reset_model.dart'; sealed class DoorLockEvent extends Equatable { const DoorLockEvent(); @@ -38,4 +39,15 @@ class UpdateLockEvent extends DoorLockEvent { List get props => [value]; } +class DoorLockFactoryReset extends DoorLockEvent { + final String deviceId; + final FactoryResetModel factoryReset; + const DoorLockFactoryReset({ + required this.deviceId, + required this.factoryReset, + }); + + @override + List get props => [deviceId, factoryReset]; +} diff --git a/lib/pages/device_managment/door_lock/view/door_lock_batch_control_view.dart b/lib/pages/device_managment/door_lock/view/door_lock_batch_control_view.dart index e4875f12..4efd76fc 100644 --- a/lib/pages/device_managment/door_lock/view/door_lock_batch_control_view.dart +++ b/lib/pages/device_managment/door_lock/view/door_lock_batch_control_view.dart @@ -1,4 +1,8 @@ import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:syncrow_web/pages/device_managment/all_devices/models/factory_reset_model.dart'; +import 'package:syncrow_web/pages/device_managment/door_lock/bloc/door_lock_bloc.dart'; +import 'package:syncrow_web/pages/device_managment/door_lock/bloc/door_lock_event.dart'; import 'package:syncrow_web/pages/device_managment/shared/batch_control/factory_reset.dart'; import 'package:syncrow_web/pages/device_managment/shared/batch_control/firmware_update.dart'; import 'package:syncrow_web/utils/helpers/responsice_layout_helper/responsive_layout_helper.dart'; @@ -30,7 +34,16 @@ class DoorLockBatchControlView extends StatelessWidget deviceId: devicesIds.first, version: 12, ), - FactoryResetWidget(deviceId: devicesIds.first), + FactoryResetWidget( + callFactoryReset: () { + BlocProvider.of(context).add( + DoorLockFactoryReset( + deviceId: devicesIds.first, + factoryReset: FactoryResetModel(devicesUuid: devicesIds), + ), + ); + }, + ), ], ), ); diff --git a/lib/pages/device_managment/gateway/bloc/gate_way_bloc.dart b/lib/pages/device_managment/gateway/bloc/gate_way_bloc.dart index ca572bf0..f4c16ba0 100644 --- a/lib/pages/device_managment/gateway/bloc/gate_way_bloc.dart +++ b/lib/pages/device_managment/gateway/bloc/gate_way_bloc.dart @@ -2,6 +2,7 @@ import 'dart:async'; import 'package:bloc/bloc.dart'; import 'package:equatable/equatable.dart'; +import 'package:syncrow_web/pages/device_managment/all_devices/models/factory_reset_model.dart'; import 'package:syncrow_web/pages/visitor_password/model/device_model.dart'; import 'package:syncrow_web/services/devices_mang_api.dart'; @@ -12,6 +13,7 @@ class GateWayBloc extends Bloc { GateWayBloc() : super(GateWayInitial()) { on((event, emit) {}); on(_getGatWayById); + on(_onFactoryReset); } FutureOr _getGatWayById( @@ -27,4 +29,22 @@ class GateWayBloc extends Bloc { return; } } + + FutureOr _onFactoryReset( + GateWayFactoryReset event, Emitter emit) async { + emit(GatewayLoadingState()); + try { + final response = await DevicesManagementApi().factoryReset( + event.factoryReset, + event.deviceId, + ); + if (!response) { + emit(const ErrorState(message: 'Failed')); + } else { + add(GatWayById(event.deviceId)); + } + } catch (e) { + emit(ErrorState(message: e.toString())); + } + } } diff --git a/lib/pages/device_managment/gateway/bloc/gate_way_event.dart b/lib/pages/device_managment/gateway/bloc/gate_way_event.dart index 22c81a12..6ee5faf5 100644 --- a/lib/pages/device_managment/gateway/bloc/gate_way_event.dart +++ b/lib/pages/device_managment/gateway/bloc/gate_way_event.dart @@ -18,3 +18,15 @@ class GatWayById extends GateWayEvent { final String getWayId; const GatWayById(this.getWayId); } + +class GateWayFactoryReset extends GateWayEvent { + final String deviceId; + final FactoryResetModel factoryReset; + const GateWayFactoryReset({ + required this.deviceId, + required this.factoryReset, + }); + + @override + List get props => [deviceId, factoryReset]; +} diff --git a/lib/pages/device_managment/gateway/view/gateway_batch_control.dart b/lib/pages/device_managment/gateway/view/gateway_batch_control.dart index f303cdba..8679a78f 100644 --- a/lib/pages/device_managment/gateway/view/gateway_batch_control.dart +++ b/lib/pages/device_managment/gateway/view/gateway_batch_control.dart @@ -1,5 +1,6 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:syncrow_web/pages/device_managment/all_devices/models/factory_reset_model.dart'; import 'package:syncrow_web/pages/device_managment/gateway/bloc/gate_way_bloc.dart'; import 'package:syncrow_web/pages/device_managment/shared/batch_control/factory_reset.dart'; import 'package:syncrow_web/pages/device_managment/shared/batch_control/firmware_update.dart'; @@ -36,7 +37,17 @@ class GatewayBatchControlView extends StatelessWidget ), children: [ FirmwareUpdateWidget(deviceId: gatewayIds.first, version: 2), - FactoryResetWidget(deviceId: gatewayIds.first), + FactoryResetWidget( + callFactoryReset: () { + context.read().add( + GateWayFactoryReset( + deviceId: gatewayIds.first, + factoryReset: + FactoryResetModel(devicesUuid: gatewayIds), + ), + ); + }, + ), ], ); } else { diff --git a/lib/pages/device_managment/one_gang_switch/bloc/wall_light_switch_bloc.dart b/lib/pages/device_managment/one_gang_switch/bloc/wall_light_switch_bloc.dart index ef25e7ac..595e7e06 100644 --- a/lib/pages/device_managment/one_gang_switch/bloc/wall_light_switch_bloc.dart +++ b/lib/pages/device_managment/one_gang_switch/bloc/wall_light_switch_bloc.dart @@ -15,6 +15,7 @@ class WallLightSwitchBloc on(_onControl); on(_onFetchBatchStatus); on(_onBatchControl); + on(_onFactoryReset); } late WallLightStatusModel deviceStatus; @@ -153,4 +154,22 @@ class WallLightSwitchBloc isBatch: true, ); } + + FutureOr _onFactoryReset( + WallLightFactoryReset event, Emitter emit) async { + emit(WallLightSwitchLoading()); + try { + final response = await DevicesManagementApi().factoryReset( + event.factoryReset, + event.deviceId, + ); + if (!response) { + emit(WallLightSwitchError('Failed')); + } else { + emit(WallLightSwitchStatusLoaded(deviceStatus)); + } + } catch (e) { + emit(WallLightSwitchError(e.toString())); + } + } } diff --git a/lib/pages/device_managment/one_gang_switch/bloc/wall_light_switch_event.dart b/lib/pages/device_managment/one_gang_switch/bloc/wall_light_switch_event.dart index 88c86c97..5c601484 100644 --- a/lib/pages/device_managment/one_gang_switch/bloc/wall_light_switch_event.dart +++ b/lib/pages/device_managment/one_gang_switch/bloc/wall_light_switch_event.dart @@ -1,4 +1,5 @@ import 'package:equatable/equatable.dart'; +import 'package:syncrow_web/pages/device_managment/all_devices/models/factory_reset_model.dart'; class WallLightSwitchEvent extends Equatable { @override @@ -46,3 +47,13 @@ class WallLightSwitchBatchControl extends WallLightSwitchEvent { @override List get props => [devicesIds, code, value]; } + +class WallLightFactoryReset extends WallLightSwitchEvent { + final String deviceId; + final FactoryResetModel factoryReset; + + WallLightFactoryReset({required this.deviceId, required this.factoryReset}); + + @override + List get props => [deviceId, factoryReset]; +} diff --git a/lib/pages/device_managment/one_gang_switch/view/wall_light_batch_control.dart b/lib/pages/device_managment/one_gang_switch/view/wall_light_batch_control.dart index 0c58c6f5..e1dabb61 100644 --- a/lib/pages/device_managment/one_gang_switch/view/wall_light_batch_control.dart +++ b/lib/pages/device_managment/one_gang_switch/view/wall_light_batch_control.dart @@ -1,5 +1,6 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:syncrow_web/pages/device_managment/all_devices/models/factory_reset_model.dart'; import 'package:syncrow_web/pages/device_managment/one_gang_switch/bloc/wall_light_switch_bloc.dart'; import 'package:syncrow_web/pages/device_managment/one_gang_switch/bloc/wall_light_switch_event.dart'; import 'package:syncrow_web/pages/device_managment/one_gang_switch/bloc/wall_light_switch_state.dart'; @@ -77,7 +78,13 @@ class WallLightBatchControlView extends StatelessWidget deviceId: deviceIds.first, version: 12, ), - FactoryResetWidget(deviceId: deviceIds.first), + FactoryResetWidget( + callFactoryReset: () { + context.read().add(WallLightFactoryReset( + deviceId: status.uuid, + factoryReset: FactoryResetModel(devicesUuid: deviceIds))); + }, + ), ], ), ); diff --git a/lib/pages/device_managment/shared/batch_control/factory_reset.dart b/lib/pages/device_managment/shared/batch_control/factory_reset.dart index 98f8f043..78dfc307 100644 --- a/lib/pages/device_managment/shared/batch_control/factory_reset.dart +++ b/lib/pages/device_managment/shared/batch_control/factory_reset.dart @@ -6,36 +6,41 @@ import 'package:syncrow_web/utils/constants/assets.dart'; import 'package:syncrow_web/utils/extension/build_context_x.dart'; class FactoryResetWidget extends StatelessWidget { - const FactoryResetWidget({super.key, required String deviceId}); + const FactoryResetWidget({super.key, required this.callFactoryReset}); + + final Null Function() callFactoryReset; @override Widget build(BuildContext context) { return DeviceControlsContainer( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - ClipOval( - child: Container( - color: ColorsManager.whiteColors, - height: 60, - width: 60, - child: Padding( - padding: const EdgeInsets.all(12.0), - child: SvgPicture.asset( - Assets.factoryReset, - fit: BoxFit.cover, + child: GestureDetector( + onTap: callFactoryReset, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + ClipOval( + child: Container( + color: ColorsManager.whiteColors, + height: 60, + width: 60, + child: Padding( + padding: const EdgeInsets.all(12.0), + child: SvgPicture.asset( + Assets.factoryReset, + fit: BoxFit.cover, + ), + ), + )), + Text( + 'Factory Reset', + style: context.textTheme.titleMedium!.copyWith( + fontWeight: FontWeight.w400, + color: ColorsManager.blackColor, ), ), - )), - Text( - 'Factory Reset', - style: context.textTheme.titleMedium!.copyWith( - fontWeight: FontWeight.w400, - color: ColorsManager.blackColor, - ), - ), - ], + ], + ), ), ); } diff --git a/lib/pages/device_managment/three_gang_switch/bloc/living_room_bloc.dart b/lib/pages/device_managment/three_gang_switch/bloc/living_room_bloc.dart index 4ab6f8e8..ca264c13 100644 --- a/lib/pages/device_managment/three_gang_switch/bloc/living_room_bloc.dart +++ b/lib/pages/device_managment/three_gang_switch/bloc/living_room_bloc.dart @@ -4,6 +4,7 @@ import 'dart:async'; import 'package:bloc/bloc.dart'; import 'package:equatable/equatable.dart'; import 'package:syncrow_web/pages/device_managment/all_devices/models/device_status.dart'; +import 'package:syncrow_web/pages/device_managment/all_devices/models/factory_reset_model.dart'; import 'package:syncrow_web/pages/device_managment/three_gang_switch/models/living_room_model.dart'; import 'package:syncrow_web/services/devices_mang_api.dart'; @@ -20,6 +21,7 @@ class LivingRoomBloc extends Bloc { on(_livingRoomControl); on(_livingRoomBatchControl); on(_livingRoomFetchBatchControl); + on(_livingRoomFactoryReset); } FutureOr _onFetchDeviceStatus(LivingRoomFetchDeviceStatusEvent event, @@ -165,4 +167,22 @@ class LivingRoomBloc extends Bloc { isBatch: true, ); } + + FutureOr _livingRoomFactoryReset( + LivingRoomFactoryResetEvent event, Emitter emit) async { + emit(LivingRoomDeviceStatusLoading()); + try { + final response = await DevicesManagementApi().factoryReset( + event.factoryReset, + event.uuid, + ); + if (!response) { + emit(const LivingRoomDeviceManagementError('Failed')); + } else { + emit(LivingRoomDeviceStatusLoaded(deviceStatus)); + } + } catch (e) { + emit(LivingRoomDeviceManagementError(e.toString())); + } + } } diff --git a/lib/pages/device_managment/three_gang_switch/bloc/living_room_event.dart b/lib/pages/device_managment/three_gang_switch/bloc/living_room_event.dart index a3b0d78b..c0ada0f6 100644 --- a/lib/pages/device_managment/three_gang_switch/bloc/living_room_event.dart +++ b/lib/pages/device_managment/three_gang_switch/bloc/living_room_event.dart @@ -49,3 +49,12 @@ class LivingRoomBatchControl extends LivingRoomEvent { @override List get props => [devicesIds, code, value]; } + +class LivingRoomFactoryResetEvent extends LivingRoomEvent { + final String uuid; + final FactoryResetModel factoryReset; + const LivingRoomFactoryResetEvent(this.uuid, this.factoryReset); + + @override + List get props => [uuid, factoryReset]; +} diff --git a/lib/pages/device_managment/three_gang_switch/view/living_room_batch_controls.dart b/lib/pages/device_managment/three_gang_switch/view/living_room_batch_controls.dart index f8c40179..0d82c515 100644 --- a/lib/pages/device_managment/three_gang_switch/view/living_room_batch_controls.dart +++ b/lib/pages/device_managment/three_gang_switch/view/living_room_batch_controls.dart @@ -1,5 +1,6 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:syncrow_web/pages/device_managment/all_devices/models/factory_reset_model.dart'; import 'package:syncrow_web/pages/device_managment/shared/batch_control/factory_reset.dart'; import 'package:syncrow_web/pages/device_managment/shared/batch_control/firmware_update.dart'; import 'package:syncrow_web/pages/device_managment/three_gang_switch/bloc/living_room_bloc.dart'; @@ -105,7 +106,14 @@ class LivingRoomBatchControlsView extends StatelessWidget deviceId: deviceIds.first, version: 12, ), - FactoryResetWidget(deviceId: deviceIds.first), + FactoryResetWidget(callFactoryReset: () { + context.read().add( + LivingRoomFactoryResetEvent( + status.uuid, + FactoryResetModel(devicesUuid: deviceIds), + ), + ); + }), ], ), ); diff --git a/lib/pages/device_managment/two_gang_switch/bloc/two_gang_switch_bloc.dart b/lib/pages/device_managment/two_gang_switch/bloc/two_gang_switch_bloc.dart index 7a15a68c..0d35d8e8 100644 --- a/lib/pages/device_managment/two_gang_switch/bloc/two_gang_switch_bloc.dart +++ b/lib/pages/device_managment/two_gang_switch/bloc/two_gang_switch_bloc.dart @@ -13,6 +13,7 @@ class TwoGangSwitchBloc extends Bloc { on(_onControl); on(_onFetchBatchStatus); on(_onBatchControl); + on(_onFactoryReset); } late TwoGangStatusModel deviceStatus; @@ -155,4 +156,22 @@ class TwoGangSwitchBloc extends Bloc { isBatch: true, ); } + + FutureOr _onFactoryReset( + TwoGangFactoryReset event, Emitter emit) async { + emit(TwoGangSwitchLoading()); + try { + final response = await DevicesManagementApi().factoryReset( + event.factoryReset, + event.deviceId, + ); + if (!response) { + emit(TwoGangSwitchError('Failed')); + } else { + emit(TwoGangSwitchStatusLoaded(deviceStatus)); + } + } catch (e) { + emit(TwoGangSwitchError(e.toString())); + } + } } diff --git a/lib/pages/device_managment/two_gang_switch/bloc/two_gang_switch_event.dart b/lib/pages/device_managment/two_gang_switch/bloc/two_gang_switch_event.dart index d5b9a01d..16973b3a 100644 --- a/lib/pages/device_managment/two_gang_switch/bloc/two_gang_switch_event.dart +++ b/lib/pages/device_managment/two_gang_switch/bloc/two_gang_switch_event.dart @@ -1,4 +1,5 @@ import 'package:equatable/equatable.dart'; +import 'package:syncrow_web/pages/device_managment/all_devices/models/factory_reset_model.dart'; class TwoGangSwitchEvent extends Equatable { @override @@ -46,3 +47,13 @@ class TwoGangSwitchBatchControl extends TwoGangSwitchEvent { @override List get props => [deviceId, code, value]; } + +class TwoGangFactoryReset extends TwoGangSwitchEvent { + final String deviceId; + final FactoryResetModel factoryReset; + + TwoGangFactoryReset({required this.deviceId, required this.factoryReset}); + + @override + List get props => [deviceId, factoryReset]; +} diff --git a/lib/pages/device_managment/two_gang_switch/view/wall_light_batch_control.dart b/lib/pages/device_managment/two_gang_switch/view/wall_light_batch_control.dart index 1e417dfa..52900155 100644 --- a/lib/pages/device_managment/two_gang_switch/view/wall_light_batch_control.dart +++ b/lib/pages/device_managment/two_gang_switch/view/wall_light_batch_control.dart @@ -1,5 +1,6 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:syncrow_web/pages/device_managment/all_devices/models/factory_reset_model.dart'; import 'package:syncrow_web/pages/device_managment/shared/batch_control/factory_reset.dart'; import 'package:syncrow_web/pages/device_managment/shared/batch_control/firmware_update.dart'; import 'package:syncrow_web/pages/device_managment/shared/toggle_widget.dart'; @@ -87,7 +88,14 @@ class TwoGangBatchControlView extends StatelessWidget deviceId: deviceIds.first, version: 12, ), - FactoryResetWidget(deviceId: deviceIds.first), + FactoryResetWidget(callFactoryReset: () { + context.read().add( + TwoGangFactoryReset( + deviceId: status.uuid, + factoryReset: FactoryResetModel(devicesUuid: deviceIds), + ), + ); + }), ], ), ); diff --git a/lib/pages/device_managment/wall_sensor/bloc/bloc.dart b/lib/pages/device_managment/wall_sensor/bloc/bloc.dart index cc96955a..f3040b48 100644 --- a/lib/pages/device_managment/wall_sensor/bloc/bloc.dart +++ b/lib/pages/device_managment/wall_sensor/bloc/bloc.dart @@ -19,6 +19,7 @@ class WallSensorBloc extends Bloc { on(_getDeviceReports); on(_showDescription); on(_backToGridView); + on(_onFactoryReset); } void _fetchWallSensorStatus( @@ -168,4 +169,22 @@ class WallSensorBloc extends Bloc { BackToGridViewEvent event, Emitter emit) { emit(WallSensorUpdateState(wallSensorModel: deviceStatus)); } + + FutureOr _onFactoryReset( + WallSensorFactoryResetEvent event, Emitter emit) async { + emit(WallSensorLoadingNewSate(wallSensorModel: deviceStatus)); + try { + final response = await DevicesManagementApi().factoryReset( + event.factoryReset, + event.deviceId, + ); + if (!response) { + emit(const WallSensorFailedState(error: 'Failed')); + } else { + emit(WallSensorUpdateState(wallSensorModel: deviceStatus)); + } + } catch (e) { + emit(WallSensorFailedState(error: e.toString())); + } + } } diff --git a/lib/pages/device_managment/wall_sensor/bloc/event.dart b/lib/pages/device_managment/wall_sensor/bloc/event.dart index f09c7123..17d85d43 100644 --- a/lib/pages/device_managment/wall_sensor/bloc/event.dart +++ b/lib/pages/device_managment/wall_sensor/bloc/event.dart @@ -1,4 +1,5 @@ import 'package:equatable/equatable.dart'; +import 'package:syncrow_web/pages/device_managment/all_devices/models/factory_reset_model.dart'; abstract class WallSensorEvent extends Equatable { const WallSensorEvent(); @@ -59,3 +60,13 @@ class WallSensorBatchControlEvent extends WallSensorEvent { @override List get props => [deviceIds, code, value]; } + +class WallSensorFactoryResetEvent extends WallSensorEvent { + final String deviceId; + final FactoryResetModel factoryReset; + + const WallSensorFactoryResetEvent({ + required this.deviceId, + required this.factoryReset, + }); +} diff --git a/lib/pages/device_managment/wall_sensor/view/wall_sensor_batch_control.dart b/lib/pages/device_managment/wall_sensor/view/wall_sensor_batch_control.dart index dab1e152..5e855208 100644 --- a/lib/pages/device_managment/wall_sensor/view/wall_sensor_batch_control.dart +++ b/lib/pages/device_managment/wall_sensor/view/wall_sensor_batch_control.dart @@ -1,5 +1,6 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:syncrow_web/pages/device_managment/all_devices/models/factory_reset_model.dart'; import 'package:syncrow_web/pages/device_managment/shared/batch_control/factory_reset.dart'; import 'package:syncrow_web/pages/device_managment/shared/batch_control/firmware_update.dart'; import 'package:syncrow_web/pages/device_managment/shared/sensors_widgets/presence_update_data.dart'; @@ -118,7 +119,16 @@ class WallSensorBatchControlView extends StatelessWidget ), ), FirmwareUpdateWidget(deviceId: devicesIds.first, version: 2), - FactoryResetWidget(deviceId: devicesIds.first), + FactoryResetWidget( + callFactoryReset: () { + context.read().add( + WallSensorFactoryResetEvent( + deviceId: devicesIds.first, + factoryReset: FactoryResetModel(devicesUuid: devicesIds), + ), + ); + }, + ), ], ); } diff --git a/lib/services/devices_mang_api.dart b/lib/services/devices_mang_api.dart index 9ac42eca..5551bfe6 100644 --- a/lib/services/devices_mang_api.dart +++ b/lib/services/devices_mang_api.dart @@ -2,6 +2,7 @@ import 'package:flutter/material.dart'; import 'package:syncrow_web/pages/device_managment/all_devices/models/device_reports.dart'; import 'package:syncrow_web/pages/device_managment/all_devices/models/device_status.dart'; import 'package:syncrow_web/pages/device_managment/all_devices/models/devices_model.dart'; +import 'package:syncrow_web/pages/device_managment/all_devices/models/factory_reset_model.dart'; import 'package:syncrow_web/pages/device_managment/water_heater/models/schedule_model.dart'; import 'package:syncrow_web/pages/visitor_password/model/device_model.dart'; import 'package:syncrow_web/services/api/http_service.dart'; @@ -259,4 +260,21 @@ class DevicesManagementApi { return false; } } + + Future factoryReset(FactoryResetModel factoryReset, String uuid) async { + try { + final response = await HTTPService().post( + path: ApiEndpoints.factoryReset.replaceAll('{deviceUuid}', uuid), + body: factoryReset.toMap(), + showServerMessage: true, + expectedResponseModel: (json) { + return json['success'] ?? false; + }, + ); + return response; + } catch (e) { + debugPrint('Error fetching $e'); + return false; + } + } } diff --git a/lib/utils/constants/api_const.dart b/lib/utils/constants/api_const.dart index 2b39fc03..23bc5c6c 100644 --- a/lib/utils/constants/api_const.dart +++ b/lib/utils/constants/api_const.dart @@ -46,4 +46,6 @@ abstract class ApiEndpoints { '/schedule/{deviceUuid}/{scheduleUuid}'; static const String updateScheduleByDeviceId = '/schedule/enable/{deviceUuid}'; + + static const String factoryReset = '/device/factory/reset/{deviceUuid}'; } From b5eeeedcd2e384a8d04b5c72f40a65fb0066842b Mon Sep 17 00:00:00 2001 From: ashrafzarkanisala Date: Mon, 23 Sep 2024 07:29:04 +0300 Subject: [PATCH 08/10] push ac temp step amount and main door icons --- assets/icons/open_close_door.svg | 24 +++++++++++ assets/icons/open_close_records.svg | 19 +++++++++ .../batch_current_temp.dart | 4 +- .../ac/view/control_list/current_temp.dart | 4 +- .../view/main_door_control_view.dart | 42 ++++++++++++------- lib/utils/constants/assets.dart | 6 +++ 6 files changed, 81 insertions(+), 18 deletions(-) create mode 100644 assets/icons/open_close_door.svg create mode 100644 assets/icons/open_close_records.svg diff --git a/assets/icons/open_close_door.svg b/assets/icons/open_close_door.svg new file mode 100644 index 00000000..d5aacdef --- /dev/null +++ b/assets/icons/open_close_door.svg @@ -0,0 +1,24 @@ + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/assets/icons/open_close_records.svg b/assets/icons/open_close_records.svg new file mode 100644 index 00000000..9c5c585c --- /dev/null +++ b/assets/icons/open_close_records.svg @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + + + + + + diff --git a/lib/pages/device_managment/ac/view/batch_control_list/batch_current_temp.dart b/lib/pages/device_managment/ac/view/batch_control_list/batch_current_temp.dart index e6d9378f..ba5ea639 100644 --- a/lib/pages/device_managment/ac/view/batch_control_list/batch_current_temp.dart +++ b/lib/pages/device_managment/ac/view/batch_control_list/batch_current_temp.dart @@ -118,7 +118,7 @@ class _CurrentTempState extends State { onIncrement: () { if (_adjustedValue < 30) { setState(() { - _adjustedValue++; + _adjustedValue = _adjustedValue + 0.5; }); _onValueChanged(_adjustedValue); } @@ -126,7 +126,7 @@ class _CurrentTempState extends State { onDecrement: () { if (_adjustedValue > 20) { setState(() { - _adjustedValue--; + _adjustedValue = _adjustedValue - 0.5; }); _onValueChanged(_adjustedValue); } diff --git a/lib/pages/device_managment/ac/view/control_list/current_temp.dart b/lib/pages/device_managment/ac/view/control_list/current_temp.dart index 5ff6fea6..0327e357 100644 --- a/lib/pages/device_managment/ac/view/control_list/current_temp.dart +++ b/lib/pages/device_managment/ac/view/control_list/current_temp.dart @@ -118,7 +118,7 @@ class _CurrentTempState extends State { onIncrement: () { if (_adjustedValue < 30) { setState(() { - _adjustedValue++; + _adjustedValue = _adjustedValue + 0.5; }); _onValueChanged(_adjustedValue); } @@ -126,7 +126,7 @@ class _CurrentTempState extends State { onDecrement: () { if (_adjustedValue > 20) { setState(() { - _adjustedValue--; + _adjustedValue = _adjustedValue - 0.5; }); _onValueChanged(_adjustedValue); } diff --git a/lib/pages/device_managment/main_door_sensor/view/main_door_control_view.dart b/lib/pages/device_managment/main_door_sensor/view/main_door_control_view.dart index 2168be5a..44b2fec3 100644 --- a/lib/pages/device_managment/main_door_sensor/view/main_door_control_view.dart +++ b/lib/pages/device_managment/main_door_sensor/view/main_door_control_view.dart @@ -75,16 +75,18 @@ class MainDoorSensorControlView extends StatelessWidget ), children: [ IconNameStatusContainer( + isFullIcon: true, name: status.doorContactState ? 'Open' : 'Close', - icon: Assets.mainDoor, + icon: Assets.openCloseDoor, onTap: () {}, status: status.doorContactState, textColor: ColorsManager.red, paddingAmount: 8, ), IconNameStatusContainer( + isFullIcon: true, name: 'Open/Close\nRecord', - icon: Assets.mainDoorReports, + icon: Assets.openCloseRecords, onTap: () { final from = DateTime.now() .subtract(const Duration(days: 30)) @@ -103,6 +105,7 @@ class MainDoorSensorControlView extends StatelessWidget textColor: ColorsManager.blackColor, ), IconNameStatusContainer( + isFullIcon: false, name: 'Notifications\nSettings', icon: Assets.mainDoorNotifi, onTap: () { @@ -113,6 +116,7 @@ class MainDoorSensorControlView extends StatelessWidget }, status: false, textColor: ColorsManager.blackColor, + paddingAmount: 14, ), ], ); @@ -128,6 +132,7 @@ class IconNameStatusContainer extends StatelessWidget { required this.status, required this.textColor, this.paddingAmount = 12, + required this.isFullIcon, }); final String name; @@ -136,6 +141,7 @@ class IconNameStatusContainer extends StatelessWidget { final bool status; final Color textColor; final double? paddingAmount; + final bool isFullIcon; @override Widget build(BuildContext context) { @@ -145,22 +151,30 @@ class IconNameStatusContainer extends StatelessWidget { child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - Container( - width: 60, - height: 60, - decoration: const BoxDecoration( - shape: BoxShape.circle, - color: ColorsManager.whiteColors, - ), - margin: const EdgeInsets.symmetric(horizontal: 4), - padding: EdgeInsets.all(paddingAmount ?? 12), - child: ClipOval( + if (isFullIcon) + ClipOval( child: SvgPicture.asset( icon, - fit: BoxFit.fill, + fit: BoxFit.contain, + ), + ) + else + Container( + width: 60, + height: 60, + decoration: const BoxDecoration( + shape: BoxShape.circle, + color: ColorsManager.whiteColors, + ), + //margin: const EdgeInsets.symmetric(horizontal: 4), + padding: EdgeInsets.all(paddingAmount ?? 12), + child: ClipOval( + child: SvgPicture.asset( + icon, + fit: BoxFit.contain, + ), ), ), - ), const Spacer(), Padding( padding: const EdgeInsets.symmetric(horizontal: 6), diff --git a/lib/utils/constants/assets.dart b/lib/utils/constants/assets.dart index c334d2d1..719cc21c 100644 --- a/lib/utils/constants/assets.dart +++ b/lib/utils/constants/assets.dart @@ -157,4 +157,10 @@ class Assets { //assets/icons/empty_records.svg static const String emptyRecords = 'assets/icons/empty_records.svg'; + + //assets/icons/open_close_door.svg + static const String openCloseDoor = 'assets/icons/open_close_door.svg'; + + //assets/icons/open_close_records.svg + static const String openCloseRecords = 'assets/icons/open_close_records.svg'; } From 0c530e9ea604fe78de21fdd0b9f1ae6fbf916355 Mon Sep 17 00:00:00 2001 From: ashrafzarkanisala Date: Mon, 23 Sep 2024 08:32:49 +0300 Subject: [PATCH 09/10] push water heater batch control view --- assets/icons/water_heater.svg | 22 +++++ .../helper/route_controls_based_code.dart | 11 ++- .../water_heater/bloc/water_heater_bloc.dart | 73 ++++++++++++++-- .../water_heater/bloc/water_heater_event.dart | 35 +++++--- .../view/water_heater_batch_control.dart | 87 +++++++++++++++++++ .../view/water_heater_device_control.dart | 4 +- lib/utils/constants/assets.dart | 3 + 7 files changed, 217 insertions(+), 18 deletions(-) create mode 100644 assets/icons/water_heater.svg create mode 100644 lib/pages/device_managment/water_heater/view/water_heater_batch_control.dart diff --git a/assets/icons/water_heater.svg b/assets/icons/water_heater.svg new file mode 100644 index 00000000..aa58e3fc --- /dev/null +++ b/assets/icons/water_heater.svg @@ -0,0 +1,22 @@ + + + + + + + + + + + + + + + + + + + + + + diff --git a/lib/pages/device_managment/all_devices/helper/route_controls_based_code.dart b/lib/pages/device_managment/all_devices/helper/route_controls_based_code.dart index 5065ec31..80a00094 100644 --- a/lib/pages/device_managment/all_devices/helper/route_controls_based_code.dart +++ b/lib/pages/device_managment/all_devices/helper/route_controls_based_code.dart @@ -20,6 +20,7 @@ import 'package:syncrow_web/pages/device_managment/two_gang_switch/view/wall_lig import 'package:syncrow_web/pages/device_managment/two_gang_switch/view/wall_light_device_control.dart'; import 'package:syncrow_web/pages/device_managment/wall_sensor/view/wall_sensor_batch_control.dart'; import 'package:syncrow_web/pages/device_managment/wall_sensor/view/wall_sensor_conrtols.dart'; +import 'package:syncrow_web/pages/device_managment/water_heater/view/water_heater_batch_control.dart'; import 'package:syncrow_web/pages/device_managment/water_heater/view/water_heater_device_control.dart'; mixin RouteControlsBasedCode { @@ -56,7 +57,7 @@ mixin RouteControlsBasedCode { case 'AC': return AcDeviceControlsView(device: device); case 'WH': - return WaterHeaterDeviceControl( + return WaterHeaterDeviceControlView( device: device, ); case 'DS': @@ -140,6 +141,14 @@ mixin RouteControlsBasedCode { .where((e) => (e.productType == 'AC')) .map((e) => e.uuid!) .toList()); + case 'WH': + return WaterHEaterBatchControlView( + deviceIds: devices + .where((e) => (e.productType == 'WH')) + .map((e) => e.uuid!) + .toList(), + ); + default: return const SizedBox(); } diff --git a/lib/pages/device_managment/water_heater/bloc/water_heater_bloc.dart b/lib/pages/device_managment/water_heater/bloc/water_heater_bloc.dart index 1eb88912..2e84c011 100644 --- a/lib/pages/device_managment/water_heater/bloc/water_heater_bloc.dart +++ b/lib/pages/device_managment/water_heater/bloc/water_heater_bloc.dart @@ -17,6 +17,8 @@ class WaterHeaterBloc extends Bloc { WaterHeaterBloc() : super(WaterHeaterInitial()) { on(_fetchWaterHeaterStatus); on(_controlWaterHeater); + on(_batchFetchWaterHeater); + on(_batchControlWaterHeater); on(_updateScheduleEvent); on(_stopScheduleEvent); on(_onDecrementCountdown); @@ -145,6 +147,7 @@ class WaterHeaterBloc extends Bloc { value: event.value, oldValue: oldValue, emit: emit, + isBatch: false, ); if (success) { @@ -395,19 +398,29 @@ class WaterHeaterBloc extends Bloc { // } Future _runDebounce({ - required String deviceId, + required dynamic deviceId, required String code, required dynamic value, required dynamic oldValue, required Emitter emit, + required bool isBatch, }) async { try { + late bool status; await Future.delayed(const Duration(milliseconds: 500)); - final status = await DevicesManagementApi().deviceControl( - deviceId, - Status(code: code, value: value), - ); + if (isBatch) { + status = await DevicesManagementApi().deviceBatchControl( + deviceId, + code, + value, + ); + } else { + status = await DevicesManagementApi().deviceControl( + deviceId, + Status(code: code, value: value), + ); + } if (!status) { _revertValue(code, oldValue, emit.call); @@ -580,4 +593,54 @@ class WaterHeaterBloc extends Bloc { } } } + + FutureOr _batchFetchWaterHeater(FetchWaterHeaterBatchStatusEvent event, + Emitter emit) async { + emit(WaterHeaterLoadingState()); + + try { + final status = + await DevicesManagementApi().getBatchStatus(event.devicesUuid); + deviceStatus = WaterHeaterStatusModel.fromJson( + event.devicesUuid.first, status.status); + + emit(WaterHeaterDeviceStatusLoaded(deviceStatus)); + } catch (e) { + emit(WaterHeaterDeviceStatusLoaded(deviceStatus)); + } + } + + FutureOr _batchControlWaterHeater(ControlWaterHeaterBatchEvent event, + Emitter emit) async { + if (state is WaterHeaterDeviceStatusLoaded) { + final currentState = state as WaterHeaterDeviceStatusLoaded; + + final oldValue = _getValueByCode(event.code); + + _updateLocalValue(event.code, event.value); + + emit(currentState.copyWith( + status: deviceStatus, + )); + + final success = await _runDebounce( + deviceId: event.devicesUuid, + code: event.code, + value: event.value, + oldValue: oldValue, + emit: emit, + isBatch: true, + ); + + if (success) { + if (event.code == "switch_1") { + emit(currentState.copyWith( + status: deviceStatus, + )); + } + } else { + _updateLocalValue(event.code, oldValue); + } + } + } } diff --git a/lib/pages/device_managment/water_heater/bloc/water_heater_event.dart b/lib/pages/device_managment/water_heater/bloc/water_heater_event.dart index bc0909ab..e2e718ae 100644 --- a/lib/pages/device_managment/water_heater/bloc/water_heater_event.dart +++ b/lib/pages/device_managment/water_heater/bloc/water_heater_event.dart @@ -52,15 +52,6 @@ final class WaterHeaterFetchStatusEvent extends WaterHeaterEvent { List get props => [deviceId]; } -final class WaterHeaterFetchBatchStatusEvent extends WaterHeaterEvent { - final String deviceId; - - const WaterHeaterFetchBatchStatusEvent(this.deviceId); - - @override - List get props => [deviceId]; -} - final class DecrementCountdownEvent extends WaterHeaterEvent {} final class AddScheduleEvent extends WaterHeaterEvent { @@ -106,7 +97,8 @@ final class UpdateScheduleEntryEvent extends WaterHeaterEvent { }); @override - List get props => [category, functionOn, deviceId, scheduleId, index]; + List get props => + [category, functionOn, deviceId, scheduleId, index]; } class GetSchedulesEvent extends WaterHeaterEvent { @@ -165,3 +157,26 @@ class UpdateFunctionOnEvent extends WaterHeaterEvent { @override List get props => [isOn]; } + +class FetchWaterHeaterBatchStatusEvent extends WaterHeaterEvent { + final List devicesUuid; + const FetchWaterHeaterBatchStatusEvent({required this.devicesUuid}); + + @override + List get props => [devicesUuid]; +} + +class ControlWaterHeaterBatchEvent extends WaterHeaterEvent { + final List devicesUuid; + final String code; + final dynamic value; + + const ControlWaterHeaterBatchEvent({ + required this.devicesUuid, + required this.code, + required this.value, + }); + + @override + List get props => [devicesUuid, code, value]; +} diff --git a/lib/pages/device_managment/water_heater/view/water_heater_batch_control.dart b/lib/pages/device_managment/water_heater/view/water_heater_batch_control.dart new file mode 100644 index 00000000..cc62adfd --- /dev/null +++ b/lib/pages/device_managment/water_heater/view/water_heater_batch_control.dart @@ -0,0 +1,87 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; + +import 'package:syncrow_web/pages/device_managment/shared/batch_control/factory_reset.dart'; +import 'package:syncrow_web/pages/device_managment/shared/batch_control/firmware_update.dart'; +import 'package:syncrow_web/pages/device_managment/shared/toggle_widget.dart'; +import 'package:syncrow_web/pages/device_managment/water_heater/bloc/water_heater_bloc.dart'; +import 'package:syncrow_web/pages/device_managment/water_heater/models/water_heater_status_model.dart'; +import 'package:syncrow_web/utils/constants/assets.dart'; +import 'package:syncrow_web/utils/helpers/responsice_layout_helper/responsive_layout_helper.dart'; + +class WaterHEaterBatchControlView extends StatelessWidget + with HelperResponsiveLayout { + const WaterHEaterBatchControlView({super.key, required this.deviceIds}); + + final List deviceIds; + + @override + Widget build(BuildContext context) { + return BlocProvider( + create: (context) => WaterHeaterBloc() + ..add(FetchWaterHeaterBatchStatusEvent(devicesUuid: deviceIds)), + child: BlocBuilder( + builder: (context, state) { + if (state is WaterHeaterLoadingState) { + return const Center(child: CircularProgressIndicator()); + } else if (state is WaterHeaterDeviceStatusLoaded) { + return _buildStatusControls(context, state.status); + } else if (state is WaterHeaterBatchFailedState) { + return const Center(child: Text('Error fetching status')); + } else { + return const Center(child: CircularProgressIndicator()); + } + }, + ), + ); + } + + Widget _buildStatusControls( + BuildContext context, WaterHeaterStatusModel status) { + final isExtraLarge = isExtraLargeScreenSize(context); + final isLarge = isLargeScreenSize(context); + final isMedium = isMediumScreenSize(context); + return SizedBox( + child: GridView( + padding: const EdgeInsets.symmetric(horizontal: 50), + shrinkWrap: true, + physics: const NeverScrollableScrollPhysics(), + gridDelegate: SliverGridDelegateWithFixedCrossAxisCount( + crossAxisCount: isLarge || isExtraLarge + ? 3 + : isMedium + ? 2 + : 1, + mainAxisExtent: 140, + crossAxisSpacing: 12, + mainAxisSpacing: 12, + ), + children: [ + ToggleWidget( + value: status.heaterSwitch, + code: 'switch_1', + deviceId: deviceIds.first, + label: 'Water Heater', + icon: Assets.waterHeater, + onChange: (value) { + context.read().add( + ControlWaterHeaterBatchEvent( + devicesUuid: deviceIds, + code: 'switch_1', + value: value, + ), + ); + }, + ), + FirmwareUpdateWidget( + deviceId: deviceIds.first, + version: 12, + ), + FactoryResetWidget( + callFactoryReset: () {}, + ), + ], + ), + ); + } +} diff --git a/lib/pages/device_managment/water_heater/view/water_heater_device_control.dart b/lib/pages/device_managment/water_heater/view/water_heater_device_control.dart index c3f9d360..57f7444d 100644 --- a/lib/pages/device_managment/water_heater/view/water_heater_device_control.dart +++ b/lib/pages/device_managment/water_heater/view/water_heater_device_control.dart @@ -12,9 +12,9 @@ import 'package:syncrow_web/utils/constants/assets.dart'; import 'package:syncrow_web/utils/extension/build_context_x.dart'; import 'package:syncrow_web/utils/helpers/responsice_layout_helper/responsive_layout_helper.dart'; -class WaterHeaterDeviceControl extends StatelessWidget +class WaterHeaterDeviceControlView extends StatelessWidget with HelperResponsiveLayout { - const WaterHeaterDeviceControl({super.key, required this.device}); + const WaterHeaterDeviceControlView({super.key, required this.device}); final AllDevicesModel device; diff --git a/lib/utils/constants/assets.dart b/lib/utils/constants/assets.dart index 719cc21c..4ad6ba2f 100644 --- a/lib/utils/constants/assets.dart +++ b/lib/utils/constants/assets.dart @@ -163,4 +163,7 @@ class Assets { //assets/icons/open_close_records.svg static const String openCloseRecords = 'assets/icons/open_close_records.svg'; + + //assets/icons/water_heater.svg + static const String waterHeater = 'assets/icons/water_heater.svg'; } From 6e3ad984e1f1577648a54840ec118267f85625ad Mon Sep 17 00:00:00 2001 From: ashrafzarkanisala Date: Mon, 23 Sep 2024 09:22:32 +0300 Subject: [PATCH 10/10] change timer picker theme color --- .../helper/add_schedule_dialog_helper.dart | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/lib/pages/device_managment/water_heater/helper/add_schedule_dialog_helper.dart b/lib/pages/device_managment/water_heater/helper/add_schedule_dialog_helper.dart index 68e364d7..c6cc51df 100644 --- a/lib/pages/device_managment/water_heater/helper/add_schedule_dialog_helper.dart +++ b/lib/pages/device_managment/water_heater/helper/add_schedule_dialog_helper.dart @@ -79,6 +79,16 @@ class ScheduleDialogHelper { context: context, initialTime: state.selectedTime ?? TimeOfDay.now(), + builder: (context, child) { + return Theme( + data: Theme.of(context).copyWith( + colorScheme: const ColorScheme.light( + primary: ColorsManager.primaryColor, + ), + ), + child: child!, + ); + }, ); if (time != null) { bloc.add(UpdateSelectedTimeEvent(time));