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/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/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/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/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/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/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/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/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/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/pages/device_managment/water_heater/bloc/water_heater_bloc.dart b/lib/pages/device_managment/water_heater/bloc/water_heater_bloc.dart index 0deb00b7..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 @@ -1,9 +1,14 @@ +// water_heater_bloc.dart + 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/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'; part 'water_heater_event.dart'; part 'water_heater_state.dart'; @@ -12,115 +17,449 @@ class WaterHeaterBloc extends Bloc { WaterHeaterBloc() : super(WaterHeaterInitial()) { on(_fetchWaterHeaterStatus); on(_controlWaterHeater); + on(_batchFetchWaterHeater); + on(_batchControlWaterHeater); on(_updateScheduleEvent); on(_stopScheduleEvent); + on(_onDecrementCountdown); + on(_initializeAddSchedule); + on(_updateSelectedTime); + on(_updateSelectedDay); + on(_updateFunctionOn); + + on(_getSchedule); + on(_onAddSchedule); + on(_onDeleteSchedule); + on(_onUpdateSchedule); } late WaterHeaterStatusModel deviceStatus; - Timer? _timer; + Timer? _countdownTimer; + // Timer? _inchingTimer; + + 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, ) async { - final currentState = state as WaterHeaterScheduleViewState; + final currentState = state; + if (currentState is WaterHeaterDeviceStatusLoaded) { + 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); - final countdownRemaining = currentState.isActive - ? currentState.countdownRemaining - : Duration(hours: event.hours, minutes: event.minutes); + emit(currentState.copyWith( + scheduleMode: ScheduleModes.countdown, + countdownHours: countdownRemaining.inHours, + countdownMinutes: countdownRemaining.inMinutes % 60, + isCountdownActive: currentState.isCountdownActive, + countdownRemaining: countdownRemaining, + )); - emit(WaterHeaterScheduleViewState( - scheduleMode: event.scheduleMode, - hours: countdownRemaining!.inHours, - minutes: countdownRemaining.inMinutes % 60, - isActive: currentState.isActive, - countdownRemaining: 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); - if (currentState.isActive) { - _startCountdown(countdownRemaining, emit); + emit(currentState.copyWith( + scheduleMode: ScheduleModes.inching, + inchingHours: inchingDuration.inHours, + inchingMinutes: inchingDuration.inMinutes % 60, + isInchingActive: currentState.isInchingActive, + )); + } } } 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, + isBatch: false, + ); + + if (success) { + if (event.code == "countdown_1") { + final countdownDuration = Duration(seconds: event.value); + + emit(currentState.copyWith( + 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, + )); + // } + } + } } } + FutureOr _stopScheduleEvent( + StopScheduleEvent event, + Emitter emit, + ) async { + if (state is WaterHeaterDeviceStatusLoaded) { + final currentState = state as WaterHeaterDeviceStatusLoaded; + final isCountDown = currentState.scheduleMode == ScheduleModes.countdown; + + _countdownTimer?.cancel(); + + 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: isCountDown ? 'countdown_1' : 'switch_inching', value: 0), + ); + if (!status) { + emit(const WaterHeaterFailedState(error: 'Failed to stop schedule.')); + } + } catch (e) { + emit(WaterHeaterFailedState(error: e.toString())); + } + } + } + + FutureOr _fetchWaterHeaterStatus( + WaterHeaterFetchStatusEvent event, + Emitter emit, + ) async { + emit(WaterHeaterLoadingState()); + + try { + final status = + await DevicesManagementApi().getDeviceStatus(event.deviceId); + deviceStatus = + WaterHeaterStatusModel.fromJson(event.deviceId, status.status); + + if (deviceStatus.scheduleMode == ScheduleModes.countdown) { + final countdownRemaining = Duration( + hours: deviceStatus.countdownHours, + minutes: deviceStatus.countdownMinutes, + ); + + 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, + ); + + 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, + countdownHours: 0, + countdownMinutes: 0, + inchingHours: 0, + inchingMinutes: 0, + isCountdownActive: false, + isInchingActive: false, + )); + } + } catch (e) { + emit(WaterHeaterFailedState(error: e.toString())); + } + } + + 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, + ) { + if (state is WaterHeaterDeviceStatusLoaded) { + final currentState = state as WaterHeaterDeviceStatusLoaded; + + if (currentState.countdownRemaining != null && + currentState.countdownRemaining! > Duration.zero) { + final newRemaining = + currentState.countdownRemaining! - const Duration(minutes: 1); + + if (newRemaining <= Duration.zero) { + _countdownTimer?.cancel(); + emit(currentState.copyWith( + countdownHours: 0, + countdownMinutes: 0, + isCountdownActive: false, + countdownRemaining: Duration.zero, + )); + return; + } + + int totalSeconds = newRemaining.inSeconds; + + int newHours = totalSeconds ~/ 3600; + int newMinutes = (totalSeconds % 3600) ~/ 60; + + emit(currentState.copyWith( + countdownHours: newHours, + countdownMinutes: newMinutes, + countdownRemaining: newRemaining, + )); + } + } + } + + // FutureOr _onDecrementInching( + // DecrementInchingEvent event, + // Emitter emit, + // ) { + // if (state is WaterHeaterDeviceStatusLoaded) { + // final currentState = state as WaterHeaterDeviceStatusLoaded; + + // 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, + required dynamic deviceId, required String code, required dynamic value, required dynamic oldValue, required Emitter emit, + required bool isBatch, }) async { - final completer = Completer(); + try { + late bool status; + await Future.delayed(const Duration(milliseconds: 500)); - if (_timer != null) { - _timer!.cancel(); - } - - _timer = Timer(const Duration(milliseconds: 500), () async { - try { - final status = await DevicesManagementApi().deviceControl( + if (isBatch) { + status = await DevicesManagementApi().deviceBatchControl( + deviceId, + code, + value, + ); + } else { + status = await DevicesManagementApi().deviceControl( deviceId, Status(code: code, value: value), ); - - if (!status) { - _revertValueAndEmit(deviceId, code, oldValue, emit); - completer.complete(false); - } else { - completer.complete(true); - } - } catch (e) { - _revertValueAndEmit(deviceId, code, oldValue, emit); - completer.complete(false); } - }); - return completer.future; + if (!status) { + _revertValue(code, oldValue, emit.call); + return false; + } else { + return true; + } + } catch (e) { + _revertValue(code, oldValue, emit.call); + return false; + } } - void _revertValueAndEmit( - String deviceId, String code, dynamic oldValue, Emitter emit) { - _updateLocalValue(code, oldValue, emit); - emit(WaterHeaterDeviceStatusLoaded(deviceStatus)); + 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, Emitter emit) { + 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; } @@ -130,97 +469,178 @@ class WaterHeaterBloc extends Bloc { switch (code) { case 'switch_1': return deviceStatus.heaterSwitch; - + case 'countdown_1': + return deviceStatus.countdownHours * 60 + deviceStatus.countdownMinutes; default: return null; } } - FutureOr _fetchWaterHeaterStatus( - WaterHeaterFetchStatusEvent event, Emitter emit) async { - emit(WaterHeaterLoadingState()); + @override + Future close() { + _countdownTimer?.cancel(); + return super.close(); + } + + FutureOr _getSchedule( + GetSchedulesEvent event, Emitter emit) async { + emit(ScheduleLoadingState()); try { - final status = await DevicesManagementApi().getDeviceStatus(event.deviceId); - deviceStatus = WaterHeaterStatusModel.fromJson(event.deviceId, status.status); + // List schedules = await DevicesManagementApi() + // .getDeviceSchedules(deviceStatus.uuid, event.category); - if (deviceStatus.countdownHours > 0 || deviceStatus.countdownMinutes > 0) { - final remainingDuration = Duration( - hours: deviceStatus.countdownHours, - minutes: deviceStatus.countdownMinutes, - ); + List schedules = const []; - emit(WaterHeaterScheduleViewState( - scheduleMode: deviceStatus.scheduleMode, - hours: deviceStatus.countdownHours, - minutes: deviceStatus.countdownMinutes, - isActive: true, - countdownRemaining: remainingDuration, - )); - - // _startCountdown(remainingDuration, emit); - } else { - emit(WaterHeaterScheduleViewState( - scheduleMode: deviceStatus.scheduleMode, - hours: 0, - minutes: 0, - isActive: false, - )); - } + emit(WaterHeaterDeviceStatusLoaded( + deviceStatus, + schedules: schedules, + scheduleMode: ScheduleModes.schedule, + )); } catch (e) { - emit(WaterHeaterFailedState(error: e.toString())); + //(const WaterHeaterFailedState(error: 'Failed to fetch schedules.')); + emit(WaterHeaterDeviceStatusLoaded( + deviceStatus, + schedules: const [], + )); } } - void _startCountdown(Duration duration, Emitter emit) { - _timer?.cancel(); - - _timer = Timer.periodic(const Duration(seconds: 1), (timer) { - final state = this.state as WaterHeaterScheduleViewState; - final remaining = state.countdownRemaining! - const Duration(seconds: 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, - )); - } - }); - } - - FutureOr _stopScheduleEvent( - StopScheduleEvent event, + FutureOr _onAddSchedule( + AddScheduleEvent 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, - )); + ) async { + if (state is WaterHeaterDeviceStatusLoaded) { + final currentState = state as WaterHeaterDeviceStatusLoaded; + + ScheduleModel newSchedule = ScheduleModel( + category: event.category, + time: formatTimeOfDayToISO(event.time), + function: Status(code: 'switch_1', value: event.functionOn), + days: ScheduleModel.convertSelectedDaysToStrings(event.selectedDays), + ); + + // emit(ScheduleLoadingState()); + + bool success = await DevicesManagementApi() + .addScheduleRecord(newSchedule, currentState.status.uuid); + + if (success) { + final updatedSchedules = + List.from(currentState.schedules)..add(newSchedule); + + emit(currentState.copyWith(schedules: updatedSchedules)); + } else { + emit(currentState); + //emit(const WaterHeaterFailedState(error: 'Failed to add schedule.')); + } + } } - @override - Future close() { - _timer?.cancel(); - return super.close(); + FutureOr _onUpdateSchedule( + UpdateScheduleEntryEvent event, + Emitter emit, + ) async { + if (state is WaterHeaterDeviceStatusLoaded) { + final currentState = state as WaterHeaterDeviceStatusLoaded; + + ScheduleModel updatedSchedule = currentState.schedules[event.index] + .copyWith( + function: Status(code: 'switch_1', value: event.functionOn)); + + // emit(ScheduleLoadingState()); + + bool success = await DevicesManagementApi().updateScheduleRecord( + enable: event.functionOn, + uuid: currentState.status.uuid, + scheduleId: event.scheduleId, + ); + + if (success) { + final updatedSchedules = + List.from(currentState.schedules) + ..[event.index] = updatedSchedule; + + emit(currentState.copyWith(schedules: updatedSchedules)); + } else { + emit(currentState); + // emit(const WaterHeaterFailedState(error: 'Failed to update schedule.')); + } + } + } + + 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.')); + } + } + } + + 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 a0164b3a..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 @@ -12,11 +12,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 +27,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,15 +52,131 @@ class WaterHeaterFetchStatusEvent extends WaterHeaterEvent { List get props => [deviceId]; } -class WaterHeaterFetchBatchStatusEvent extends WaterHeaterEvent { - final String deviceId; +final class DecrementCountdownEvent extends WaterHeaterEvent {} - const WaterHeaterFetchBatchStatusEvent(this.deviceId); +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 => [deviceId]; + List get props => [selectedDays, time, functionOn, category]; } -// class ShowScheduleViewEvent extends WaterHeaterEvent { -// const ShowScheduleViewEvent(); -// } +final class DeleteScheduleEvent extends WaterHeaterEvent { + final int index; + final String scheduleId; + + const DeleteScheduleEvent({required this.index, required this.scheduleId}); + + @override + 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, 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 { + 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]; +} + +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/bloc/water_heater_state.dart b/lib/pages/device_managment/water_heater/bloc/water_heater_state.dart index 2dc45367..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 @@ -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(); @@ -13,13 +13,97 @@ final class WaterHeaterInitial extends WaterHeaterState {} final class WaterHeaterLoadingState extends WaterHeaterState {} -final class WaterHeaterDeviceStatusLoaded extends WaterHeaterState { - final WaterHeaterStatusModel status; +final class ScheduleLoadingState extends WaterHeaterState {} - const WaterHeaterDeviceStatusLoaded(this.status); +class WaterHeaterDeviceStatusLoaded extends WaterHeaterState { + final WaterHeaterStatusModel status; + final ScheduleModes? scheduleMode; + + // Countdown-specific + final int? countdownHours; + final int? countdownMinutes; + final Duration? countdownRemaining; + 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; + final bool isEditing; + + const WaterHeaterDeviceStatusLoaded( + this.status, { + this.scheduleMode, + 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, + this.functionOn = false, + this.isEditing = false, + }); @override - List get props => [status]; + List get props => [ + status, + scheduleMode, + countdownHours, + countdownMinutes, + countdownRemaining, + isCountdownActive, + inchingHours, + inchingMinutes, + isInchingActive, + schedules, + selectedDays, + selectedTime, + functionOn, + isEditing + ]; + + WaterHeaterDeviceStatusLoaded copyWith({ + WaterHeaterStatusModel? status, + ScheduleModes? scheduleMode, + int? countdownHours, + int? countdownMinutes, + Duration? countdownRemaining, + bool? isCountdownActive, + int? inchingHours, + int? inchingMinutes, + bool? isInchingActive, + List? schedules, + List? selectedDays, + TimeOfDay? selectedTime, + bool? functionOn, + bool? isEditing, + }) { + return WaterHeaterDeviceStatusLoaded( + status ?? this.status, + scheduleMode: scheduleMode ?? this.scheduleMode, + 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, + functionOn: functionOn ?? this.functionOn, + isEditing: isEditing ?? this.isEditing, + ); + } } final class WaterHeaterFailedState extends WaterHeaterState { @@ -39,23 +123,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/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..c6cc51df --- /dev/null +++ b/lib/pages/device_managment/water_heater/helper/add_schedule_dialog_helper.dart @@ -0,0 +1,256 @@ +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(), + 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)); + } + }, + 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 new file mode 100644 index 00000000..8b79f563 --- /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/schedule_model.dart b/lib/pages/device_managment/water_heater/models/schedule_model.dart new file mode 100644 index 00000000..7e9410be --- /dev/null +++ b/lib/pages/device_managment/water_heater/models/schedule_model.dart @@ -0,0 +1,125 @@ +import 'dart:convert'; +import 'package:flutter/material.dart'; +import 'package:syncrow_web/pages/device_managment/all_devices/models/device_status.dart'; +import 'package:flutter/foundation.dart'; + +class ScheduleModel { + final String scheduleId; + final String category; + final String time; + final Status function; + 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 = '', + }); + + Map toMap() { + return { + 'category': category, + 'time': time, + 'function': function.toMap(), + 'days': days, + }; + } + + 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']), + ); + } + + String toJson() => json.encode(toMap()); + + 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 'ScheduleModel(category: $category, time: $time, function: $function, days: $days, timeOfDay: $timeOfDay, selectedDays: $selectedDays)'; + } + + @override + bool operator ==(Object other) { + if (identical(this, other)) return true; + + return other is ScheduleModel && + other.category == category && + other.time == time && + other.function == function && + listEquals(other.days, days) && + timeOfDay == other.timeOfDay && + listEquals(other.selectedDays, selectedDays); + } + + @override + int get hashCode { + return category.hashCode ^ + time.hashCode ^ + function.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 fd16df50..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,17 +1,22 @@ +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 } -class WaterHeaterStatusModel { +class WaterHeaterStatusModel extends Equatable { final String uuid; 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, @@ -19,6 +24,9 @@ class WaterHeaterStatusModel { 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) { @@ -34,7 +42,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 +56,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, @@ -60,6 +67,7 @@ class WaterHeaterStatusModel { relayStatus: relayStatus, cycleTiming: cycleTiming, scheduleMode: scheduleMode, + schedules: const [], ); } @@ -71,6 +79,7 @@ class WaterHeaterStatusModel { String? relayStatus, String? cycleTiming, ScheduleModes? scheduleMode, + List? schedules, }) { return WaterHeaterStatusModel( uuid: uuid ?? this.uuid, @@ -80,6 +89,7 @@ class WaterHeaterStatusModel { relayStatus: relayStatus ?? this.relayStatus, cycleTiming: cycleTiming ?? this.cycleTiming, scheduleMode: scheduleMode ?? this.scheduleMode, + schedules: schedules ?? this.schedules, ); } @@ -97,4 +107,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_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 6063a544..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; @@ -29,14 +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 || 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())); } }, )); @@ -75,7 +73,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/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 3d221789..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,412 +1,100 @@ -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'; import 'package:syncrow_web/pages/device_managment/water_heater/bloc/water_heater_bloc.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/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 StatelessWidget { +class BuildScheduleView extends StatefulWidget { const BuildScheduleView({super.key, required this.status}); final WaterHeaterStatusModel status; + @override + State createState() => _BuildScheduleViewState(); +} + +class _BuildScheduleViewState extends State { @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 WaterHeaterScheduleViewState) { - 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, + 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: [ + const ScheduleHeader(), + const SizedBox(height: 20), + ScheduleModeSelector(state: state), + const SizedBox(height: 20), + if (state.scheduleMode == ScheduleModes.schedule) + ScheduleManagementUI( + state: state, + onAddSchedule: () => + ScheduleDialogHelper.showAddScheduleDialog( + context, + schedule: null, + index: null, + isEdit: false ), ), - 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(); - }, - ), + if (state.scheduleMode == ScheduleModes.countdown || + state.scheduleMode == ScheduleModes.inching) + CountdownInchingView(state: state), + const SizedBox(height: 20), + if (state.scheduleMode == ScheduleModes.countdown) + CountdownModeButtons( + isActive: state.isCountdownActive ?? false, + deviceId: widget.status.uuid, + hours: state.countdownHours ?? 0, + minutes: state.countdownMinutes ?? 0, ), - ], - ), - 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, - minutes: state.minutes, - )); - } - }, - ), - ), - ), - 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, - minutes: state.minutes, - )); - } - }, - ), - ), - ), - 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, - minutes: state.minutes, - )); - } - }, - ), - ), - ), - 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, - minutes: state.minutes, - )); - } - }, - ), - ), - ), - ], - ), - ), - 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, + 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: () {}, ), - ), - const SizedBox(height: 4), - _hourMinutesWheel(state, context) ], - 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) - ? 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()); - context.read().add( - ToggleWaterHeaterEvent( - deviceId: 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: status.uuid, - code: code, - // value is time in seconds - value: Duration( - hours: state.hours, - minutes: - state.minutes) - .inSeconds, - ), - ); - }, - backgroundColor: - ColorsManager.primaryColor, - child: const Text('Save'), - ), - ), - ], - ), - ), - ), - ), - ], - ); - } - return const SizedBox(); - }, + ); + } + if (state is WaterHeaterLoadingState) { + return const Center(child: CircularProgressIndicator()); + } + return const SizedBox(); + }, + ), ), ), ), ), ); } - - Row _hourMinutesWheel( - WaterHeaterScheduleViewState state, BuildContext context) { - return Row( - mainAxisAlignment: MainAxisAlignment.start, - 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( - controller: - FixedExtentScrollController(initialItem: state.hours), - itemExtent: 40.0, - physics: FixedExtentScrollPhysics(), - onSelectedItemChanged: (int value) { - context.read().add( - UpdateScheduleEvent( - scheduleMode: state.scheduleMode, - hours: value, - minutes: state.minutes, - ), - ); - }, - 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(width: 10), - // Minutes 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( - controller: - FixedExtentScrollController(initialItem: state.minutes), - itemExtent: 40.0, - physics: FixedExtentScrollPhysics(), - onSelectedItemChanged: (int value) { - context.read().add( - UpdateScheduleEvent( - scheduleMode: state.scheduleMode, - hours: state.hours, - 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, - ), - ), - ], - ), - ], - ); - } } 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 4be915a2..5551bfe6 100644 --- a/lib/services/devices_mang_api.dart +++ b/lib/services/devices_mang_api.dart @@ -2,6 +2,8 @@ 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'; import 'package:syncrow_web/utils/constants/api_const.dart'; @@ -79,7 +81,7 @@ class DevicesManagementApi { body: body, showServerMessage: true, expectedResponseModel: (json) { - return (json['successResults'] as List).isNotEmpty ; + return (json['successResults'] as List).isNotEmpty; }, ); @@ -175,4 +177,104 @@ class DevicesManagementApi { ); } } + + Future addScheduleRecord( + ScheduleModel 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> 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, + 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, String scheduleId) async { + try { + final response = await HTTPService().delete( + path: ApiEndpoints.scheduleByDeviceId + .replaceAll('{deviceUuid}', uuid) + .replaceAll('{scheduleUuid}', scheduleId), + showServerMessage: true, + expectedResponseModel: (json) { + return json['success'] ?? false; + }, + ); + return response; + } catch (e) { + debugPrint('Error fetching $e'); + 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 3138c502..23bc5c6c 100644 --- a/lib/utils/constants/api_const.dart +++ b/lib/utils/constants/api_const.dart @@ -38,4 +38,14 @@ 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 getScheduleByDeviceId = + '/schedule/{deviceUuid}?category={category}'; + static const String deleteScheduleByDeviceId = + '/schedule/{deviceUuid}/{scheduleUuid}'; + static const String updateScheduleByDeviceId = + '/schedule/enable/{deviceUuid}'; + + static const String factoryReset = '/device/factory/reset/{deviceUuid}'; } diff --git a/lib/utils/constants/assets.dart b/lib/utils/constants/assets.dart index 3c22e031..4ad6ba2f 100644 --- a/lib/utils/constants/assets.dart +++ b/lib/utils/constants/assets.dart @@ -154,4 +154,16 @@ 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'; + + //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'; + + //assets/icons/water_heater.svg + static const String waterHeater = 'assets/icons/water_heater.svg'; } diff --git a/lib/utils/format_date_time.dart b/lib/utils/format_date_time.dart index 5c089a2c..470acb97 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,22 @@ 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(); +} + +String formatIsoStringToTime(String isoString) { + final dateTime = DateTime.parse(isoString); + return DateFormat('hh:mm a').format(dateTime); +} 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),