diff --git a/lib/pages/device_managment/water_heater/bloc/water_heater_bloc.dart b/lib/pages/device_managment/water_heater/bloc/water_heater_bloc.dart index a96213db..1eb88912 100644 --- a/lib/pages/device_managment/water_heater/bloc/water_heater_bloc.dart +++ b/lib/pages/device_managment/water_heater/bloc/water_heater_bloc.dart @@ -5,7 +5,6 @@ import 'package:bloc/bloc.dart'; import 'package:equatable/equatable.dart'; import 'package:flutter/material.dart'; import 'package:syncrow_web/pages/device_managment/all_devices/models/device_status.dart'; -import 'package:syncrow_web/pages/device_managment/water_heater/models/schedule_entry.dart'; import 'package:syncrow_web/pages/device_managment/water_heater/models/schedule_model.dart'; import 'package:syncrow_web/pages/device_managment/water_heater/models/water_heater_status_model.dart'; import 'package:syncrow_web/services/devices_mang_api.dart'; @@ -21,17 +20,20 @@ class WaterHeaterBloc extends Bloc { on(_updateScheduleEvent); on(_stopScheduleEvent); on(_onDecrementCountdown); - on(_onAddSchedule); - on(_onDeleteSchedule); - on(_onUpdateSchedule); on(_initializeAddSchedule); on(_updateSelectedTime); on(_updateSelectedDay); on(_updateFunctionOn); + + on(_getSchedule); + on(_onAddSchedule); + on(_onDeleteSchedule); + on(_onUpdateSchedule); } late WaterHeaterStatusModel deviceStatus; Timer? _countdownTimer; + // Timer? _inchingTimer; FutureOr _initializeAddSchedule( InitializeAddScheduleEvent event, @@ -87,22 +89,37 @@ class WaterHeaterBloc extends Bloc { ) async { final currentState = state; if (currentState is WaterHeaterDeviceStatusLoaded) { - final countdownRemaining = - // currentState.isActive == true - // ? currentState.countdownRemaining - // : - Duration(hours: event.hours, minutes: event.minutes); + if (event.scheduleMode == ScheduleModes.schedule) { + emit(currentState.copyWith( + scheduleMode: ScheduleModes.schedule, + )); + } + if (event.scheduleMode == ScheduleModes.countdown) { + final countdownRemaining = + Duration(hours: event.hours, minutes: event.minutes); - emit(currentState.copyWith( - scheduleMode: event.scheduleMode, - hours: countdownRemaining.inHours, - minutes: countdownRemaining.inMinutes % 60, - isActive: currentState.isActive, - countdownRemaining: countdownRemaining, - )); + emit(currentState.copyWith( + scheduleMode: ScheduleModes.countdown, + countdownHours: countdownRemaining.inHours, + countdownMinutes: countdownRemaining.inMinutes % 60, + isCountdownActive: currentState.isCountdownActive, + countdownRemaining: countdownRemaining, + )); - if (!currentState.isActive! && countdownRemaining > Duration.zero) { - _startCountdown(emit, countdownRemaining); + if (!currentState.isCountdownActive! && + countdownRemaining > Duration.zero) { + _startCountdownTimer(emit, countdownRemaining); + } + } else if (event.scheduleMode == ScheduleModes.inching) { + final inchingDuration = + Duration(hours: event.hours, minutes: event.minutes); + + emit(currentState.copyWith( + scheduleMode: ScheduleModes.inching, + inchingHours: inchingDuration.inHours, + inchingMinutes: inchingDuration.inMinutes % 60, + isInchingActive: currentState.isInchingActive, + )); } } } @@ -130,28 +147,39 @@ class WaterHeaterBloc extends Bloc { emit: emit, ); - if (success && - (event.code == "countdown_1" || event.code == "switch_inching")) { - final countdownDuration = Duration(seconds: event.value); + if (success) { + if (event.code == "countdown_1") { + final countdownDuration = Duration(seconds: event.value); - emit(currentState.copyWith( - status: deviceStatus, - scheduleMode: deviceStatus.scheduleMode, - hours: countdownDuration.inHours, - minutes: (countdownDuration.inMinutes % 60), - isActive: true, - countdownRemaining: countdownDuration, - )); - if (countdownDuration.inSeconds > 0) { - _startCountdown(emit, countdownDuration); - } else { - _countdownTimer?.cancel(); emit(currentState.copyWith( - hours: 0, - minutes: 0, - isActive: false, - countdownRemaining: Duration.zero, + countdownHours: countdownDuration.inHours, + countdownMinutes: countdownDuration.inMinutes % 60, + countdownRemaining: countdownDuration, + isCountdownActive: true, )); + + if (countdownDuration.inSeconds > 0) { + _startCountdownTimer(emit, countdownDuration); + } else { + _countdownTimer?.cancel(); + emit(currentState.copyWith( + countdownHours: 0, + countdownMinutes: 0, + countdownRemaining: Duration.zero, + isCountdownActive: false, + )); + } + } else if (event.code == "switch_inching") { + final inchingDuration = Duration(seconds: event.value); + //if (inchingDuration.inSeconds > 0) { + // _startInchingTimer(emit, inchingDuration); + // } else { + emit(currentState.copyWith( + inchingHours: inchingDuration.inHours, + inchingMinutes: inchingDuration.inMinutes % 60, + isInchingActive: true, + )); + // } } } } @@ -163,28 +191,30 @@ class WaterHeaterBloc extends Bloc { ) async { if (state is WaterHeaterDeviceStatusLoaded) { final currentState = state as WaterHeaterDeviceStatusLoaded; + final isCountDown = currentState.scheduleMode == ScheduleModes.countdown; _countdownTimer?.cancel(); - deviceStatus = deviceStatus.copyWith( - countdownHours: 0, - countdownMinutes: 0, - scheduleMode: ScheduleModes.countdown, - ); - - emit(currentState.copyWith( - status: deviceStatus, - scheduleMode: ScheduleModes.countdown, - hours: 0, - minutes: 0, - isActive: false, - countdownRemaining: Duration.zero, - )); + if (isCountDown) { + emit(currentState.copyWith( + countdownHours: 0, + countdownMinutes: 0, + countdownRemaining: Duration.zero, + isCountdownActive: false, + )); + } else if (currentState.scheduleMode == ScheduleModes.inching) { + emit(currentState.copyWith( + inchingHours: 0, + inchingMinutes: 0, + isInchingActive: false, + )); + } try { final status = await DevicesManagementApi().deviceControl( event.deviceId, - Status(code: 'countdown_1', value: 0), + Status( + code: isCountDown ? 'countdown_1' : 'switch_inching', value: 0), ); if (!status) { emit(const WaterHeaterFailedState(error: 'Failed to stop schedule.')); @@ -207,30 +237,66 @@ class WaterHeaterBloc extends Bloc { deviceStatus = WaterHeaterStatusModel.fromJson(event.deviceId, status.status); - if (deviceStatus.countdownHours > 0 || - deviceStatus.countdownMinutes > 0) { - final remainingDuration = Duration( + if (deviceStatus.scheduleMode == ScheduleModes.countdown) { + final countdownRemaining = Duration( hours: deviceStatus.countdownHours, minutes: deviceStatus.countdownMinutes, ); - emit(WaterHeaterDeviceStatusLoaded( - deviceStatus, - scheduleMode: deviceStatus.scheduleMode, - hours: deviceStatus.countdownHours, - minutes: deviceStatus.countdownMinutes, - isActive: true, - countdownRemaining: remainingDuration, - )); + if (countdownRemaining > Duration.zero) { + emit(WaterHeaterDeviceStatusLoaded( + deviceStatus, + scheduleMode: ScheduleModes.countdown, + countdownHours: deviceStatus.countdownHours, + countdownMinutes: deviceStatus.countdownMinutes, + isCountdownActive: true, + countdownRemaining: countdownRemaining, + )); + _startCountdownTimer(emit, countdownRemaining); + } else { + emit(WaterHeaterDeviceStatusLoaded( + deviceStatus, + scheduleMode: ScheduleModes.countdown, + countdownHours: 0, + countdownMinutes: 0, + isCountdownActive: false, + countdownRemaining: Duration.zero, + )); + } + } else if (deviceStatus.scheduleMode == ScheduleModes.inching) { + final inchingDuration = Duration( + hours: deviceStatus.inchingHours, + minutes: deviceStatus.inchingMinutes, + ); - _startCountdown(emit, remainingDuration); + if (inchingDuration > Duration.zero) { + emit(WaterHeaterDeviceStatusLoaded( + deviceStatus, + scheduleMode: ScheduleModes.inching, + inchingHours: deviceStatus.inchingHours, + inchingMinutes: deviceStatus.inchingMinutes, + isInchingActive: true, + )); +//_startInchingTimer(emit, inchingDuration); + } else { + emit(WaterHeaterDeviceStatusLoaded( + deviceStatus, + scheduleMode: ScheduleModes.inching, + inchingHours: 0, + inchingMinutes: 0, + isInchingActive: false, + )); + } } else { emit(WaterHeaterDeviceStatusLoaded( deviceStatus, scheduleMode: deviceStatus.scheduleMode, - hours: 0, - minutes: 0, - isActive: false, + countdownHours: 0, + countdownMinutes: 0, + inchingHours: 0, + inchingMinutes: 0, + isCountdownActive: false, + isInchingActive: false, )); } } catch (e) { @@ -238,6 +304,28 @@ class WaterHeaterBloc extends Bloc { } } + void _startCountdownTimer( + Emitter emit, + Duration countdownRemaining, + ) { + _countdownTimer?.cancel(); + + _countdownTimer = Timer.periodic(const Duration(minutes: 1), (timer) { + add(DecrementCountdownEvent()); + }); + } + + // void _startInchingTimer( + // Emitter emit, + // Duration inchingDuration, + // ) { + // _inchingTimer?.cancel(); + + // _inchingTimer = Timer.periodic(const Duration(minutes: 1), (timer) { + // add(DecrementInchingEvent()); + // }); + // } + _onDecrementCountdown( DecrementCountdownEvent event, Emitter emit, @@ -253,9 +341,9 @@ class WaterHeaterBloc extends Bloc { if (newRemaining <= Duration.zero) { _countdownTimer?.cancel(); emit(currentState.copyWith( - hours: 0, - minutes: 0, - isActive: false, + countdownHours: 0, + countdownMinutes: 0, + isCountdownActive: false, countdownRemaining: Duration.zero, )); return; @@ -267,22 +355,44 @@ class WaterHeaterBloc extends Bloc { int newMinutes = (totalSeconds % 3600) ~/ 60; emit(currentState.copyWith( - hours: newHours, - minutes: newMinutes, + countdownHours: newHours, + countdownMinutes: newMinutes, countdownRemaining: newRemaining, )); } } } - void _startCountdown( - Emitter emit, Duration countdownRemaining) { - _countdownTimer?.cancel(); + // FutureOr _onDecrementInching( + // DecrementInchingEvent event, + // Emitter emit, + // ) { + // if (state is WaterHeaterDeviceStatusLoaded) { + // final currentState = state as WaterHeaterDeviceStatusLoaded; - _countdownTimer = Timer.periodic(const Duration(minutes: 1), (timer) { - add(DecrementCountdownEvent()); - }); - } + // if (currentState.inchingHours > 0 || currentState.inchingMinutes > 0) { + // final newRemaining = Duration( + // hours: currentState.inchingHours, + // minutes: currentState.inchingMinutes, + // ) - + // const Duration(minutes: 1); + + // if (newRemaining <= Duration.zero) { + // _inchingTimer?.cancel(); + // emit(currentState.copyWith( + // inchingHours: 0, + // inchingMinutes: 0, + // isInchingActive: false, + // )); + // } else { + // emit(currentState.copyWith( + // inchingHours: newRemaining.inHours, + // inchingMinutes: newRemaining.inMinutes % 60, + // )); + // } + // } + // } + // } Future _runDebounce({ required String deviceId, @@ -353,6 +463,36 @@ class WaterHeaterBloc extends Bloc { } } + @override + Future close() { + _countdownTimer?.cancel(); + return super.close(); + } + + FutureOr _getSchedule( + GetSchedulesEvent event, Emitter emit) async { + emit(ScheduleLoadingState()); + + try { + // List schedules = await DevicesManagementApi() + // .getDeviceSchedules(deviceStatus.uuid, event.category); + + List schedules = const []; + + emit(WaterHeaterDeviceStatusLoaded( + deviceStatus, + schedules: schedules, + scheduleMode: ScheduleModes.schedule, + )); + } catch (e) { + //(const WaterHeaterFailedState(error: 'Failed to fetch schedules.')); + emit(WaterHeaterDeviceStatusLoaded( + deviceStatus, + schedules: const [], + )); + } + } + FutureOr _onAddSchedule( AddScheduleEvent event, Emitter emit, @@ -360,59 +500,30 @@ class WaterHeaterBloc extends Bloc { if (state is WaterHeaterDeviceStatusLoaded) { final currentState = state as WaterHeaterDeviceStatusLoaded; - ScheduleModel sendSchedule = ScheduleModel( + ScheduleModel newSchedule = ScheduleModel( category: event.category, time: formatTimeOfDayToISO(event.time), function: Status(code: 'switch_1', value: event.functionOn), - days: _getSelectedDaysString(event.selectedDays), + days: ScheduleModel.convertSelectedDaysToStrings(event.selectedDays), ); + // emit(ScheduleLoadingState()); + bool success = await DevicesManagementApi() - .addScheduleRecord(sendSchedule, currentState.status.uuid); + .addScheduleRecord(newSchedule, currentState.status.uuid); if (success) { - final newSchedule = ScheduleEntry( - selectedDays: event.selectedDays, - time: event.time, - functionOn: event.functionOn, - category: event.category, - ); - final updatedSchedules = - List.from(currentState.schedules)..add(newSchedule); + List.from(currentState.schedules)..add(newSchedule); emit(currentState.copyWith(schedules: updatedSchedules)); } else { - emit(const WaterHeaterFailedState( - error: 'Failed to add schedule. Please try again.')); + emit(currentState); + //emit(const WaterHeaterFailedState(error: 'Failed to add schedule.')); } } } - List _getSelectedDaysString(List selectedDays) { - final days = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat']; - List selectedDaysStr = []; - for (int i = 0; i < selectedDays.length; i++) { - if (selectedDays[i]) { - selectedDaysStr.add(days[i]); - } - } - return selectedDaysStr; - } - - FutureOr _onDeleteSchedule( - DeleteScheduleEvent event, - Emitter emit, - ) { - if (state is WaterHeaterDeviceStatusLoaded) { - final currentState = state as WaterHeaterDeviceStatusLoaded; - final updatedSchedules = List.from(currentState.schedules) - ..removeAt(event.index); - - emit(currentState.copyWith(schedules: updatedSchedules)); - } - } - FutureOr _onUpdateSchedule( UpdateScheduleEntryEvent event, Emitter emit, @@ -420,44 +531,53 @@ class WaterHeaterBloc extends Bloc { if (state is WaterHeaterDeviceStatusLoaded) { final currentState = state as WaterHeaterDeviceStatusLoaded; - // Get the current schedule ID or UUID (assuming it's stored in the schedules) - String scheduleId = - ""; // Retrieve the actual schedule ID based on your model + ScheduleModel updatedSchedule = currentState.schedules[event.index] + .copyWith( + function: Status(code: 'switch_1', value: event.functionOn)); + + // emit(ScheduleLoadingState()); - // Call the API to update the schedule bool success = await DevicesManagementApi().updateScheduleRecord( enable: event.functionOn, - uuid: event.deviceId, - scheduleId: scheduleId, + uuid: currentState.status.uuid, + scheduleId: event.scheduleId, ); if (success) { final updatedSchedules = - List.from(currentState.schedules); + List.from(currentState.schedules) + ..[event.index] = updatedSchedule; - final updatedScheduleIndex = updatedSchedules.indexWhere((schedule) { - return schedule.category == event.category; - }); - - if (updatedScheduleIndex != -1) { - updatedSchedules[updatedScheduleIndex] = ScheduleEntry( - category: event.category, - selectedDays: updatedSchedules[updatedScheduleIndex].selectedDays, - time: updatedSchedules[updatedScheduleIndex].time, - functionOn: event.functionOn, - ); - emit(currentState.copyWith(schedules: updatedSchedules)); - } + emit(currentState.copyWith(schedules: updatedSchedules)); } else { - emit(const WaterHeaterFailedState( - error: 'Failed to update schedule. Please try again.')); + emit(currentState); + // emit(const WaterHeaterFailedState(error: 'Failed to update schedule.')); } } } - @override - Future close() { - _countdownTimer?.cancel(); - return super.close(); + FutureOr _onDeleteSchedule( + DeleteScheduleEvent event, + Emitter emit, + ) async { + if (state is WaterHeaterDeviceStatusLoaded) { + final currentState = state as WaterHeaterDeviceStatusLoaded; + + // emit(ScheduleLoadingState()); + + bool success = await DevicesManagementApi() + .deleteScheduleRecord(currentState.status.uuid, event.scheduleId); + + if (success) { + final updatedSchedules = + List.from(currentState.schedules) + ..removeAt(event.index); + + emit(currentState.copyWith(schedules: updatedSchedules)); + } else { + emit(currentState); + // emit(const WaterHeaterFailedState(error: 'Failed to delete schedule.')); + } + } } } diff --git a/lib/pages/device_managment/water_heater/bloc/water_heater_event.dart b/lib/pages/device_managment/water_heater/bloc/water_heater_event.dart index 0c9cd794..bc0909ab 100644 --- a/lib/pages/device_managment/water_heater/bloc/water_heater_event.dart +++ b/lib/pages/device_managment/water_heater/bloc/water_heater_event.dart @@ -82,26 +82,44 @@ final class AddScheduleEvent extends WaterHeaterEvent { final class DeleteScheduleEvent extends WaterHeaterEvent { final int index; + final String scheduleId; - const DeleteScheduleEvent(this.index); + const DeleteScheduleEvent({required this.index, required this.scheduleId}); @override - List get props => [index]; + List get props => [index, scheduleId]; } final class UpdateScheduleEntryEvent extends WaterHeaterEvent { final bool functionOn; final String category; final String deviceId; + final int index; + final String scheduleId; const UpdateScheduleEntryEvent({ required this.functionOn, required this.category, required this.deviceId, + required this.scheduleId, + required this.index, }); @override - List get props => [category, functionOn, deviceId]; + List get props => [category, functionOn, deviceId, scheduleId, index]; +} + +class GetSchedulesEvent extends WaterHeaterEvent { + final String uuid; + final String category; + + const GetSchedulesEvent({ + required this.uuid, + required this.category, + }); + + @override + List get props => [uuid, category]; } class InitializeAddScheduleEvent extends WaterHeaterEvent { diff --git a/lib/pages/device_managment/water_heater/bloc/water_heater_state.dart b/lib/pages/device_managment/water_heater/bloc/water_heater_state.dart index 1054104f..5ff5ba3d 100644 --- a/lib/pages/device_managment/water_heater/bloc/water_heater_state.dart +++ b/lib/pages/device_managment/water_heater/bloc/water_heater_state.dart @@ -13,14 +13,24 @@ final class WaterHeaterInitial extends WaterHeaterState {} final class WaterHeaterLoadingState extends WaterHeaterState {} +final class ScheduleLoadingState extends WaterHeaterState {} + class WaterHeaterDeviceStatusLoaded extends WaterHeaterState { final WaterHeaterStatusModel status; final ScheduleModes? scheduleMode; - final int? hours; - final int? minutes; - final bool? isActive; + + // Countdown-specific + final int? countdownHours; + final int? countdownMinutes; final Duration? countdownRemaining; - final List schedules; + final bool? isCountdownActive; + + // Inching-specific + final int? inchingHours; + final int? inchingMinutes; + final bool? isInchingActive; + + final List schedules; final List selectedDays; final TimeOfDay? selectedTime; final bool functionOn; @@ -29,10 +39,13 @@ class WaterHeaterDeviceStatusLoaded extends WaterHeaterState { const WaterHeaterDeviceStatusLoaded( this.status, { this.scheduleMode, - this.hours, - this.minutes, - this.isActive, + this.countdownHours, + this.countdownMinutes, this.countdownRemaining, + this.isCountdownActive, + this.inchingHours, + this.inchingMinutes, + this.isInchingActive, this.schedules = const [], this.selectedDays = const [false, false, false, false, false, false, false], this.selectedTime, @@ -44,10 +57,13 @@ class WaterHeaterDeviceStatusLoaded extends WaterHeaterState { List get props => [ status, scheduleMode, - hours, - minutes, - isActive, + countdownHours, + countdownMinutes, countdownRemaining, + isCountdownActive, + inchingHours, + inchingMinutes, + isInchingActive, schedules, selectedDays, selectedTime, @@ -58,11 +74,14 @@ class WaterHeaterDeviceStatusLoaded extends WaterHeaterState { WaterHeaterDeviceStatusLoaded copyWith({ WaterHeaterStatusModel? status, ScheduleModes? scheduleMode, - int? hours, - int? minutes, - bool? isActive, + int? countdownHours, + int? countdownMinutes, Duration? countdownRemaining, - List? schedules, + bool? isCountdownActive, + int? inchingHours, + int? inchingMinutes, + bool? isInchingActive, + List? schedules, List? selectedDays, TimeOfDay? selectedTime, bool? functionOn, @@ -71,10 +90,13 @@ class WaterHeaterDeviceStatusLoaded extends WaterHeaterState { return WaterHeaterDeviceStatusLoaded( status ?? this.status, scheduleMode: scheduleMode ?? this.scheduleMode, - hours: hours ?? this.hours, - minutes: minutes ?? this.minutes, - isActive: isActive ?? this.isActive, + countdownHours: countdownHours ?? this.countdownHours, + countdownMinutes: countdownMinutes ?? this.countdownMinutes, countdownRemaining: countdownRemaining ?? this.countdownRemaining, + isCountdownActive: isCountdownActive ?? this.isCountdownActive, + inchingHours: inchingHours ?? this.inchingHours, + inchingMinutes: inchingMinutes ?? this.inchingMinutes, + isInchingActive: isInchingActive ?? this.isInchingActive, schedules: schedules ?? this.schedules, selectedDays: selectedDays ?? this.selectedDays, selectedTime: selectedTime ?? this.selectedTime, diff --git a/lib/pages/device_managment/water_heater/helper/add_schedule_dialog_helper.dart b/lib/pages/device_managment/water_heater/helper/add_schedule_dialog_helper.dart new file mode 100644 index 00000000..68e364d7 --- /dev/null +++ b/lib/pages/device_managment/water_heater/helper/add_schedule_dialog_helper.dart @@ -0,0 +1,246 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:syncrow_web/pages/device_managment/water_heater/bloc/water_heater_bloc.dart'; +import 'package:syncrow_web/pages/device_managment/water_heater/models/schedule_model.dart'; +import 'package:syncrow_web/utils/color_manager.dart'; +import 'package:syncrow_web/utils/extension/build_context_x.dart'; +import 'package:syncrow_web/pages/common/buttons/default_button.dart'; + +class ScheduleDialogHelper { + static void showAddScheduleDialog(BuildContext context, + {ScheduleModel? schedule, int? index, bool? isEdit}) { + final bloc = context.read(); + + if (schedule != null) { + final time = _convertStringToTimeOfDay(schedule.time); + final selectedDays = _convertDaysStringToBooleans(schedule.days); + + bloc.add(InitializeAddScheduleEvent( + selectedTime: time, + selectedDays: selectedDays, + functionOn: schedule.function.value, + isEditing: true, + index: index, + )); + } else { + bloc.add( + const InitializeAddScheduleEvent( + selectedDays: [false, false, false, false, false, false, false], + functionOn: false, + isEditing: false, + index: null, + selectedTime: null, + ), + ); + } + + showDialog( + context: context, + builder: (ctx) { + return BlocProvider.value( + value: bloc, + child: BlocBuilder( + builder: (context, state) { + if (state is WaterHeaterDeviceStatusLoaded) { + return AlertDialog( + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(20), + ), + content: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + const SizedBox(), + Text( + 'Scheduling', + style: context.textTheme.titleLarge!.copyWith( + color: ColorsManager.dialogBlueTitle, + fontWeight: FontWeight.bold, + ), + ), + const SizedBox(), + ], + ), + const SizedBox(height: 24), + SizedBox( + width: 150, + height: 40, + child: DefaultButton( + padding: 8, + backgroundColor: ColorsManager.boxColor, + borderRadius: 15, + onPressed: isEdit == true + ? null + : () async { + TimeOfDay? time = await showTimePicker( + context: context, + initialTime: + state.selectedTime ?? TimeOfDay.now(), + ); + if (time != null) { + bloc.add(UpdateSelectedTimeEvent(time)); + } + }, + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + state.selectedTime == null + ? 'Time' + : state.selectedTime!.format(context), + style: context.textTheme.bodySmall!.copyWith( + color: ColorsManager.grayColor, + ), + ), + const Icon( + Icons.access_time, + color: ColorsManager.grayColor, + size: 18, + ), + ], + ), + ), + ), + const SizedBox(height: 16), + _buildDayCheckboxes(context, state.selectedDays, + isEdit: isEdit), + const SizedBox(height: 16), + _buildFunctionSwitch(context, state.functionOn), + ], + ), + actions: [ + SizedBox( + width: 200, + child: DefaultButton( + height: 40, + onPressed: () { + Navigator.pop(context); + }, + backgroundColor: ColorsManager.boxColor, + child: Text( + 'Cancel', + style: context.textTheme.bodyMedium, + ), + ), + ), + SizedBox( + width: 200, + child: DefaultButton( + height: 40, + onPressed: () { + if (state.selectedTime != null) { + if (state.isEditing && index != null) { + bloc.add(UpdateScheduleEntryEvent( + index: index, + deviceId: state.status.uuid, + category: 'kg', + functionOn: state.functionOn, + scheduleId: state.schedules[index].scheduleId, + )); + } else { + bloc.add(AddScheduleEvent( + category: 'kg', + time: state.selectedTime!, + selectedDays: state.selectedDays, + functionOn: state.functionOn, + )); + } + Navigator.pop(context); + } + }, + backgroundColor: ColorsManager.primaryColor, + child: const Text('Save'), + ), + ), + ], + ); + } + return const SizedBox(); + }, + ), + ); + }, + ); + } + + static TimeOfDay _convertStringToTimeOfDay(String timeString) { + final DateTime dateTime = DateTime.parse(timeString); + return TimeOfDay(hour: dateTime.hour, minute: dateTime.minute); + } + + static List _convertDaysStringToBooleans(List selectedDays) { + final daysOfWeek = ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun']; + List daysBoolean = List.filled(7, false); + + for (int i = 0; i < daysOfWeek.length; i++) { + if (selectedDays.contains(daysOfWeek[i])) { + daysBoolean[i] = true; + } + } + + return daysBoolean; + } + + static Widget _buildDayCheckboxes( + BuildContext context, List selectedDays, + {bool? isEdit}) { + final dayLabels = ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun']; + + return Row( + children: List.generate(7, (index) { + return Row( + children: [ + Checkbox( + value: selectedDays[index], + onChanged: isEdit == true + ? null + : (bool? value) { + context + .read() + .add(UpdateSelectedDayEvent(index, value!)); + }, + ), + Text(dayLabels[index]), + ], + ); + }), + ); + } + + static Widget _buildFunctionSwitch(BuildContext context, bool isOn) { + return Row( + children: [ + Text( + 'Function:', + style: context.textTheme.bodySmall! + .copyWith(color: ColorsManager.grayColor), + ), + const SizedBox(width: 10), + Radio( + value: true, + groupValue: isOn, + onChanged: (bool? value) { + context + .read() + .add(const UpdateFunctionOnEvent(true)); + }, + ), + const Text('On'), + const SizedBox(width: 10), + Radio( + value: false, + groupValue: isOn, + onChanged: (bool? value) { + context + .read() + .add(const UpdateFunctionOnEvent(false)); + }, + ), + const Text('Off'), + ], + ); + } +} diff --git a/lib/pages/device_managment/water_heater/models/schedule_entry.dart b/lib/pages/device_managment/water_heater/models/schedule_entry.dart index 2d02b617..8b79f563 100644 --- a/lib/pages/device_managment/water_heater/models/schedule_entry.dart +++ b/lib/pages/device_managment/water_heater/models/schedule_entry.dart @@ -1,19 +1,19 @@ -import 'package:flutter/material.dart'; +// import 'package:flutter/material.dart'; -class ScheduleEntry { - final List selectedDays; - final TimeOfDay time; - final bool functionOn; - final String category; +// class ScheduleEntry { +// final List selectedDays; +// final TimeOfDay time; +// final bool functionOn; +// final String category; - ScheduleEntry({ - required this.selectedDays, - required this.time, - required this.functionOn, - required this.category, - }); +// ScheduleEntry({ +// required this.selectedDays, +// required this.time, +// required this.functionOn, +// required this.category, +// }); - @override - String toString() => - 'ScheduleEntry(selectedDays: $selectedDays, time: $time, functionOn: $functionOn)'; -} \ No newline at end of file +// @override +// String toString() => +// 'ScheduleEntry(selectedDays: $selectedDays, time: $time, functionOn: $functionOn)'; +// } \ No newline at end of file diff --git a/lib/pages/device_managment/water_heater/models/schedule_model.dart b/lib/pages/device_managment/water_heater/models/schedule_model.dart index 57386136..7e9410be 100644 --- a/lib/pages/device_managment/water_heater/models/schedule_model.dart +++ b/lib/pages/device_managment/water_heater/models/schedule_model.dart @@ -1,49 +1,27 @@ import 'dart:convert'; - -import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; import 'package:syncrow_web/pages/device_managment/all_devices/models/device_status.dart'; - -/* -{ - "category": "kg", - "time": "2024-09-22T10:31:54Z", - "function": { - "code": "switch_1", - "value": true - }, - "days": [ - "Sun" - ] -} -*/ +import 'package:flutter/foundation.dart'; class ScheduleModel { + final String scheduleId; final String category; final String time; final Status function; - final List days; + final List days; + final TimeOfDay? timeOfDay; + final List? selectedDays; ScheduleModel({ required this.category, required this.time, required this.function, required this.days, + this.timeOfDay, + this.selectedDays, + this.scheduleId = '', }); - ScheduleModel copyWith({ - String? category, - String? time, - Status? function, - List? days, - }) { - return ScheduleModel( - category: category ?? this.category, - time: time ?? this.time, - function: function ?? this.function, - days: days ?? this.days, - ); - } - Map toMap() { return { 'category': category, @@ -55,10 +33,15 @@ class ScheduleModel { factory ScheduleModel.fromMap(Map map) { return ScheduleModel( + scheduleId: map['scheduleId'] ?? '', category: map['category'] ?? '', time: map['time'] ?? '', function: Status.fromMap(map['function']), days: List.from(map['days']), + timeOfDay: + parseTimeOfDay(map['time']), + selectedDays: + parseSelectedDays(map['days']), ); } @@ -67,9 +50,54 @@ class ScheduleModel { factory ScheduleModel.fromJson(String source) => ScheduleModel.fromMap(json.decode(source)); + ScheduleModel copyWith({ + String? category, + String? time, + Status? function, + List? days, + TimeOfDay? timeOfDay, + List? selectedDays, + String? scheduleId, + }) { + return ScheduleModel( + category: category ?? this.category, + time: time ?? this.time, + function: function ?? this.function, + days: days ?? this.days, + timeOfDay: timeOfDay ?? this.timeOfDay, + selectedDays: selectedDays ?? this.selectedDays, + scheduleId: scheduleId ?? this.scheduleId, + ); + } + + static TimeOfDay? parseTimeOfDay(String isoTime) { + try { + final dateTime = DateTime.parse(isoTime); + return TimeOfDay(hour: dateTime.hour, minute: dateTime.minute); + } catch (e) { + return null; + } + } + + static List parseSelectedDays(List days) { + const allDays = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat']; + return allDays.map((day) => days.contains(day)).toList(); + } + + static List convertSelectedDaysToStrings(List selectedDays) { + const allDays = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat']; + List result = []; + for (int i = 0; i < selectedDays.length; i++) { + if (selectedDays[i]) { + result.add(allDays[i]); + } + } + return result; + } + @override String toString() { - return 'SendSchedule(category: $category, time: $time, function: $function, days: $days)'; + return 'ScheduleModel(category: $category, time: $time, function: $function, days: $days, timeOfDay: $timeOfDay, selectedDays: $selectedDays)'; } @override @@ -80,7 +108,9 @@ class ScheduleModel { other.category == category && other.time == time && other.function == function && - listEquals(other.days, days); + listEquals(other.days, days) && + timeOfDay == other.timeOfDay && + listEquals(other.selectedDays, selectedDays); } @override @@ -88,6 +118,8 @@ class ScheduleModel { return category.hashCode ^ time.hashCode ^ function.hashCode ^ - days.hashCode; + days.hashCode ^ + timeOfDay.hashCode ^ + selectedDays.hashCode; } } diff --git a/lib/pages/device_managment/water_heater/models/water_heater_status_model.dart b/lib/pages/device_managment/water_heater/models/water_heater_status_model.dart index 2eebefa9..c535bda2 100644 --- a/lib/pages/device_managment/water_heater/models/water_heater_status_model.dart +++ b/lib/pages/device_managment/water_heater/models/water_heater_status_model.dart @@ -1,5 +1,6 @@ import 'package:equatable/equatable.dart'; import 'package:syncrow_web/pages/device_managment/all_devices/models/device_status.dart'; +import 'package:syncrow_web/pages/device_managment/water_heater/models/schedule_model.dart'; enum ScheduleModes { countdown, schedule, circulate, inching } @@ -8,11 +9,14 @@ class WaterHeaterStatusModel extends Equatable { final bool heaterSwitch; final int countdownHours; final int countdownMinutes; + final int inchingHours; + final int inchingMinutes; final ScheduleModes scheduleMode; final String relayStatus; final String cycleTiming; + final List schedules; - WaterHeaterStatusModel({ + const WaterHeaterStatusModel({ required this.uuid, required this.heaterSwitch, required this.countdownHours, @@ -20,6 +24,9 @@ class WaterHeaterStatusModel extends Equatable { required this.relayStatus, required this.cycleTiming, required this.scheduleMode, + required this.schedules, + this.inchingHours = 0, + this.inchingMinutes = 0, }); factory WaterHeaterStatusModel.fromJson(String id, List jsonList) { @@ -60,6 +67,7 @@ class WaterHeaterStatusModel extends Equatable { relayStatus: relayStatus, cycleTiming: cycleTiming, scheduleMode: scheduleMode, + schedules: const [], ); } @@ -71,6 +79,7 @@ class WaterHeaterStatusModel extends Equatable { String? relayStatus, String? cycleTiming, ScheduleModes? scheduleMode, + List? schedules, }) { return WaterHeaterStatusModel( uuid: uuid ?? this.uuid, @@ -80,6 +89,7 @@ class WaterHeaterStatusModel extends Equatable { relayStatus: relayStatus ?? this.relayStatus, cycleTiming: cycleTiming ?? this.cycleTiming, scheduleMode: scheduleMode ?? this.scheduleMode, + schedules: schedules ?? this.schedules, ); } diff --git a/lib/pages/device_managment/water_heater/view/water_heater_device_control.dart b/lib/pages/device_managment/water_heater/view/water_heater_device_control.dart index e30303ac..c3f9d360 100644 --- a/lib/pages/device_managment/water_heater/view/water_heater_device_control.dart +++ b/lib/pages/device_managment/water_heater/view/water_heater_device_control.dart @@ -29,16 +29,12 @@ class WaterHeaterDeviceControl extends StatelessWidget return const Center(child: CircularProgressIndicator()); } else if (state is WaterHeaterDeviceStatusLoaded) { return _buildStatusControls(context, state.status); - } - // else if (state is WaterHeaterScheduleViewState) { - // final status = context.read().deviceStatus; - // return _buildStatusControls(context, status); - // } - else if (state is WaterHeaterFailedState || + } else if (state is WaterHeaterFailedState || state is WaterHeaterBatchFailedState) { return const Center(child: Text('Error fetching status')); } else { - return const Center(child: CircularProgressIndicator()); + return const SizedBox( + height: 200, child: Center(child: SizedBox())); } }, )); diff --git a/lib/pages/device_managment/water_heater/widgets/count_down_button.dart b/lib/pages/device_managment/water_heater/widgets/count_down_button.dart new file mode 100644 index 00000000..e60c7def --- /dev/null +++ b/lib/pages/device_managment/water_heater/widgets/count_down_button.dart @@ -0,0 +1,74 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:syncrow_web/utils/color_manager.dart'; +import 'package:syncrow_web/pages/common/buttons/default_button.dart'; +import 'package:syncrow_web/pages/device_managment/water_heater/bloc/water_heater_bloc.dart'; +import 'package:syncrow_web/utils/extension/build_context_x.dart'; + +class CountdownModeButtons extends StatelessWidget { + final bool isActive; + final String deviceId; + final int hours; + final int minutes; + + const CountdownModeButtons({ + super.key, + required this.isActive, + required this.deviceId, + required this.hours, + required this.minutes, + }); + + @override + Widget build(BuildContext context) { + return Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Expanded( + child: DefaultButton( + height: 40, + onPressed: () => Navigator.pop(context), + backgroundColor: ColorsManager.boxColor, + child: Text('Cancel', style: context.textTheme.bodyMedium), + ), + ), + const SizedBox(width: 20), + Expanded( + child: isActive + ? DefaultButton( + height: 40, + onPressed: () { + context + .read() + .add(StopScheduleEvent(deviceId)); + context.read().add( + ToggleWaterHeaterEvent( + deviceId: deviceId, + code: 'countdown_1', + value: 0, + ), + ); + }, + backgroundColor: Colors.red, + child: const Text('Stop'), + ) + : DefaultButton( + height: 40, + onPressed: () { + context.read().add( + ToggleWaterHeaterEvent( + deviceId: deviceId, + code: 'countdown_1', + value: Duration(hours: hours, minutes: minutes) + .inSeconds, + ), + ); + }, + backgroundColor: ColorsManager.primaryColor, + child: const Text('Save'), + ), + ), + ], + ); + } +} diff --git a/lib/pages/device_managment/water_heater/widgets/count_down_inching_view.dart b/lib/pages/device_managment/water_heater/widgets/count_down_inching_view.dart new file mode 100644 index 00000000..53892c20 --- /dev/null +++ b/lib/pages/device_managment/water_heater/widgets/count_down_inching_view.dart @@ -0,0 +1,152 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:syncrow_web/pages/device_managment/water_heater/bloc/water_heater_bloc.dart'; +import 'package:syncrow_web/pages/device_managment/water_heater/models/water_heater_status_model.dart'; +import 'package:syncrow_web/utils/color_manager.dart'; +import 'package:syncrow_web/utils/extension/build_context_x.dart'; + +class CountdownInchingView extends StatelessWidget { + final WaterHeaterDeviceStatusLoaded state; + + const CountdownInchingView({ + super.key, + required this.state, + }); + + @override + Widget build(BuildContext context) { + final isCountDown = + state.scheduleMode?.name == ScheduleModes.countdown.name; + + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + isCountDown ? 'Countdown:' : 'Inching:', + style: context.textTheme.bodySmall!.copyWith( + fontSize: 13, + color: ColorsManager.grayColor, + ), + ), + const SizedBox(height: 8), + Visibility( + visible: !isCountDown, + child: const Text( + 'Once enabled this feature, each time the device is turned on, it will automatically turn off after a preset time.'), + ), + const SizedBox(height: 8), + _hourMinutesWheel(context, state), + ], + ); + } + + Row _hourMinutesWheel( + BuildContext context, WaterHeaterDeviceStatusLoaded state) { + final isCountDown = + state.scheduleMode?.name == ScheduleModes.countdown.name; + late bool isActive; + if (isCountDown && + state.countdownRemaining != null && + state.isCountdownActive == true) { + isActive = true; + } else if (!isCountDown && + state.countdownRemaining != null && + state.isInchingActive == true) { + isActive = true; + } else { + isActive = false; + } + + return Row( + mainAxisAlignment: MainAxisAlignment.start, + children: [ + _buildPickerColumn( + context, + 'h', + isCountDown + ? (state.countdownHours ?? 0) + : (state.inchingHours ?? 0), + 24, (value) { + context.read().add(UpdateScheduleEvent( + scheduleMode: state.scheduleMode ?? ScheduleModes.countdown, + hours: value, + minutes: isCountDown + ? (state.countdownMinutes ?? 0) + : (state.inchingMinutes ?? 0), + )); + }, isActive: isActive), + const SizedBox(width: 10), + _buildPickerColumn( + context, + 'm', + isCountDown + ? (state.countdownMinutes ?? 0) + : (state.inchingMinutes ?? 0), + 60, (value) { + context.read().add(UpdateScheduleEvent( + scheduleMode: state.scheduleMode ?? ScheduleModes.countdown, + hours: isCountDown + ? (state.countdownHours ?? 0) + : (state.inchingHours ?? 0), + minutes: value, + )); + }, isActive: isActive), + ], + ); + } + + Widget _buildPickerColumn( + BuildContext context, + String label, + int initialValue, + int itemCount, + ValueChanged onSelected, { + required bool isActive, + }) { + return Row( + mainAxisSize: MainAxisSize.min, + children: [ + Container( + height: 40, + width: 80, + padding: const EdgeInsets.symmetric(horizontal: 16), + decoration: BoxDecoration( + color: ColorsManager.boxColor, + borderRadius: BorderRadius.circular(8), + ), + child: ListWheelScrollView.useDelegate( + key: ValueKey('$label-$initialValue'), + controller: FixedExtentScrollController( + initialItem: initialValue, + ), + itemExtent: 40.0, + physics: const FixedExtentScrollPhysics(), + onSelectedItemChanged: onSelected, + childDelegate: ListWheelChildBuilderDelegate( + builder: (context, index) { + return Center( + child: Text( + index.toString().padLeft(2, '0'), + style: TextStyle( + fontSize: 24, + color: isActive ? ColorsManager.grayColor : Colors.black, + ), + ), + ); + }, + childCount: itemCount, + ), + ), + ), + const SizedBox(width: 8), + Text( + label, + style: const TextStyle( + color: ColorsManager.grayColor, + fontSize: 18, + ), + ), + ], + ); + } +} diff --git a/lib/pages/device_managment/water_heater/widgets/inching_mode_buttons.dart b/lib/pages/device_managment/water_heater/widgets/inching_mode_buttons.dart new file mode 100644 index 00000000..8eec5cca --- /dev/null +++ b/lib/pages/device_managment/water_heater/widgets/inching_mode_buttons.dart @@ -0,0 +1,74 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:syncrow_web/utils/color_manager.dart'; +import 'package:syncrow_web/pages/common/buttons/default_button.dart'; +import 'package:syncrow_web/pages/device_managment/water_heater/bloc/water_heater_bloc.dart'; +import 'package:syncrow_web/utils/extension/build_context_x.dart'; + +class InchingModeButtons extends StatelessWidget { + final bool isActive; + final String deviceId; + final int hours; + final int minutes; + + const InchingModeButtons({ + Key? key, + required this.isActive, + required this.deviceId, + required this.hours, + required this.minutes, + }) : super(key: key); + + @override + Widget build(BuildContext context) { + return Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Expanded( + child: DefaultButton( + height: 40, + onPressed: () => Navigator.pop(context), + backgroundColor: ColorsManager.boxColor, + child: Text('Cancel', style: context.textTheme.bodyMedium), + ), + ), + const SizedBox(width: 20), + Expanded( + child: isActive + ? DefaultButton( + height: 40, + onPressed: () { + context + .read() + .add(StopScheduleEvent(deviceId)); + context.read().add( + ToggleWaterHeaterEvent( + deviceId: deviceId, + code: 'switch_inching', + value: 0, + ), + ); + }, + backgroundColor: Colors.red, + child: const Text('Stop'), + ) + : DefaultButton( + height: 40, + onPressed: () { + context.read().add( + ToggleWaterHeaterEvent( + deviceId: deviceId, + code: 'switch_inching', + value: Duration(hours: hours, minutes: minutes) + .inSeconds, + ), + ); + }, + backgroundColor: ColorsManager.primaryColor, + child: const Text('Save'), + ), + ), + ], + ); + } +} diff --git a/lib/pages/device_managment/water_heater/widgets/schedual_view.dart b/lib/pages/device_managment/water_heater/widgets/schedual_view.dart index 7ef86bac..09745eb3 100644 --- a/lib/pages/device_managment/water_heater/widgets/schedual_view.dart +++ b/lib/pages/device_managment/water_heater/widgets/schedual_view.dart @@ -1,13 +1,15 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; -import 'package:flutter_svg/flutter_svg.dart'; -import 'package:syncrow_web/pages/common/buttons/default_button.dart'; import 'package:syncrow_web/pages/device_managment/water_heater/bloc/water_heater_bloc.dart'; -import 'package:syncrow_web/pages/device_managment/water_heater/models/schedule_entry.dart'; +import 'package:syncrow_web/pages/device_managment/water_heater/helper/add_schedule_dialog_helper.dart'; import 'package:syncrow_web/pages/device_managment/water_heater/models/water_heater_status_model.dart'; -import 'package:syncrow_web/utils/color_manager.dart'; -import 'package:syncrow_web/utils/constants/assets.dart'; -import 'package:syncrow_web/utils/extension/build_context_x.dart'; +import 'package:syncrow_web/pages/device_managment/water_heater/widgets/count_down_button.dart'; +import 'package:syncrow_web/pages/device_managment/water_heater/widgets/count_down_inching_view.dart'; +import 'package:syncrow_web/pages/device_managment/water_heater/widgets/inching_mode_buttons.dart'; +import 'package:syncrow_web/pages/device_managment/water_heater/widgets/schedule_header.dart'; +import 'package:syncrow_web/pages/device_managment/water_heater/widgets/schedule_managment_ui.dart'; +import 'package:syncrow_web/pages/device_managment/water_heater/widgets/schedule_mode_buttons.dart'; +import 'package:syncrow_web/pages/device_managment/water_heater/widgets/schedule_mode_selector.dart'; class BuildScheduleView extends StatefulWidget { const BuildScheduleView({super.key, required this.status}); @@ -15,7 +17,7 @@ class BuildScheduleView extends StatefulWidget { final WaterHeaterStatusModel status; @override - _BuildScheduleViewState createState() => _BuildScheduleViewState(); + State createState() => _BuildScheduleViewState(); } class _BuildScheduleViewState extends State { @@ -42,20 +44,50 @@ class _BuildScheduleViewState extends State { return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - _scheduleHeader(context), + const ScheduleHeader(), const SizedBox(height: 20), - _buildScheduleModeSelector(context, state), + ScheduleModeSelector(state: state), const SizedBox(height: 20), if (state.scheduleMode == ScheduleModes.schedule) - _buildScheduleManagementUI(state), + ScheduleManagementUI( + state: state, + onAddSchedule: () => + ScheduleDialogHelper.showAddScheduleDialog( + context, + schedule: null, + index: null, + isEdit: false + ), + ), if (state.scheduleMode == ScheduleModes.countdown || state.scheduleMode == ScheduleModes.inching) - ..._buildCountDownAngInchingView(context, state), + CountdownInchingView(state: state), const SizedBox(height: 20), - _buildSaveStopCancelButtons(context, state), + if (state.scheduleMode == ScheduleModes.countdown) + CountdownModeButtons( + isActive: state.isCountdownActive ?? false, + deviceId: widget.status.uuid, + hours: state.countdownHours ?? 0, + minutes: state.countdownMinutes ?? 0, + ), + if (state.scheduleMode == ScheduleModes.inching) + InchingModeButtons( + isActive: state.isInchingActive ?? false, + deviceId: widget.status.uuid, + hours: state.inchingHours ?? 0, + minutes: state.inchingMinutes ?? 0, + ), + if (state.scheduleMode != ScheduleModes.countdown && + state.scheduleMode != ScheduleModes.inching) + ScheduleModeButtons( + onSave: () {}, + ), ], ); } + if (state is WaterHeaterLoadingState) { + return const Center(child: CircularProgressIndicator()); + } return const SizedBox(); }, ), @@ -65,673 +97,4 @@ class _BuildScheduleViewState extends State { ), ); } - - Row _scheduleHeader(BuildContext context) { - return Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - const SizedBox(), - Text( - 'Scheduling', - style: TextStyle( - fontWeight: FontWeight.bold, - fontSize: 22, - color: ColorsManager.dialogBlueTitle, - ), - ), - Container( - width: 25, - decoration: BoxDecoration( - color: Colors.transparent, - shape: BoxShape.circle, - border: Border.all( - color: Colors.grey, - width: 1.0, - ), - ), - child: IconButton( - padding: EdgeInsets.all(1), - icon: const Icon( - Icons.close, - color: Colors.grey, - size: 18, - ), - onPressed: () { - Navigator.of(context).pop(); - }, - ), - ), - ], - ); - } - - Widget _buildScheduleModeSelector( - BuildContext context, WaterHeaterDeviceStatusLoaded state) { - return Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - 'Type:', - style: context.textTheme.bodySmall!.copyWith( - fontSize: 13, - color: ColorsManager.grayColor, - ), - ), - const SizedBox(height: 4), - Row( - mainAxisAlignment: MainAxisAlignment.spaceEvenly, - children: [ - _buildRadioTile( - context, 'Countdown', ScheduleModes.countdown, state), - _buildRadioTile(context, 'Schedule', ScheduleModes.schedule, state), - _buildRadioTile( - context, 'Circulate', ScheduleModes.circulate, state), - _buildRadioTile(context, 'Inching', ScheduleModes.inching, state), - ], - ), - ], - ); - } - - Widget _buildRadioTile(BuildContext context, String label, ScheduleModes mode, - WaterHeaterDeviceStatusLoaded state) { - return Flexible( - child: ListTile( - contentPadding: EdgeInsets.zero, - title: Text( - label, - style: context.textTheme.bodySmall!.copyWith( - fontSize: 13, - color: ColorsManager.blackColor, - ), - ), - leading: Radio( - value: mode, - groupValue: state.scheduleMode, - onChanged: (ScheduleModes? value) { - if (value != null) { - context.read().add(UpdateScheduleEvent( - scheduleMode: value, - hours: state.hours ?? 0, - minutes: state.minutes ?? 0, - )); - } - }, - ), - ), - ); - } - - Widget _buildScheduleManagementUI(WaterHeaterDeviceStatusLoaded state) { - return Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - SizedBox( - width: 170, - height: 40, - child: DefaultButton( - borderColor: ColorsManager.boxColor, - padding: 2, - backgroundColor: ColorsManager.graysColor, - borderRadius: 15, - onPressed: () => - _showAddScheduleDialog(context, schedule: null, index: null), - child: Row( - children: [ - const Icon(Icons.add, color: ColorsManager.primaryColor), - Text( - ' Add new schedule', - style: context.textTheme.bodySmall!.copyWith( - color: ColorsManager.blackColor, - ), - ), - ], - ), - ), - ), - const SizedBox(height: 20), - _buildScheduleTable(state), - ], - ); - } - - Widget _buildScheduleTable(WaterHeaterDeviceStatusLoaded state) { - return Column( - children: [ - Table( - border: TableBorder.all( - color: ColorsManager.graysColor, - borderRadius: const BorderRadius.only( - topLeft: Radius.circular(20), topRight: Radius.circular(20)), - ), - children: [ - TableRow( - decoration: const BoxDecoration( - color: ColorsManager.boxColor, - borderRadius: BorderRadius.only( - topLeft: Radius.circular(20), - topRight: Radius.circular(20), - ), - ), - children: [ - _buildTableHeader('Active'), - _buildTableHeader('Days'), - _buildTableHeader('Time'), - _buildTableHeader('Function'), - _buildTableHeader('Action'), - ], - ), - ], - ), - Container( - height: 200, - decoration: BoxDecoration( - border: Border.all(color: ColorsManager.graysColor), - borderRadius: const BorderRadius.only( - bottomLeft: Radius.circular(20), - bottomRight: Radius.circular(20)), - ), - child: state.schedules.isEmpty - ? _buildEmptyState(context) - : _buildTableBody(state), - ), - ], - ); - } - - Widget _buildEmptyState(BuildContext context) { - return Center( - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - SvgPicture.asset(Assets.emptyRecords, width: 40, height: 40), - const SizedBox(height: 8), - Padding( - padding: const EdgeInsets.all(8.0), - child: Text( - 'No schedules added yet', - style: context.textTheme.bodySmall!.copyWith( - fontSize: 13, - color: ColorsManager.grayColor, - ), - ), - ), - ], - ), - ); - } - - Widget _buildTableBody(WaterHeaterDeviceStatusLoaded state) { - return SingleChildScrollView( - child: Table( - border: TableBorder.all(color: ColorsManager.graysColor), - defaultVerticalAlignment: TableCellVerticalAlignment.middle, - children: [ - for (int i = 0; i < state.schedules.length; i++) - _buildScheduleRow(state.schedules[i], i, context), - ], - ), - ); - } - - TableRow _buildScheduleRow( - ScheduleEntry schedule, int index, BuildContext context) { - return TableRow( - children: [ - Center( - child: schedule.functionOn - ? const Icon(Icons.radio_button_checked, - color: ColorsManager.blueColor) - : const Icon(Icons.radio_button_unchecked)), - Center(child: Text(_getSelectedDays(schedule.selectedDays))), - Center(child: Text(schedule.time.format(context))), - Center(child: Text(schedule.functionOn ? 'On' : 'Off')), - Center( - child: Wrap( - runAlignment: WrapAlignment.center, - children: [ - TextButton( - style: TextButton.styleFrom(padding: EdgeInsets.zero), - onPressed: () { - _showAddScheduleDialog(context, - schedule: schedule, index: index); - }, - child: Text( - 'Edit', - style: context.textTheme.bodySmall! - .copyWith(color: ColorsManager.blueColor), - ), - ), - TextButton( - style: TextButton.styleFrom(padding: EdgeInsets.zero), - onPressed: () { - context - .read() - .add(DeleteScheduleEvent(index)); - }, - child: Text( - 'Delete', - style: context.textTheme.bodySmall! - .copyWith(color: ColorsManager.blueColor), - ), - ), - ], - ), - ), - ], - ); - } - - String _getSelectedDays(List selectedDays) { - final days = ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun']; - List selectedDaysStr = []; - for (int i = 0; i < selectedDays.length; i++) { - if (selectedDays[i]) { - selectedDaysStr.add(days[i]); - } - } - return selectedDaysStr.join(', '); - } - - Widget _buildTableHeader(String label) { - return TableCell( - child: Padding( - padding: const EdgeInsets.all(12), - child: Text( - label, - style: context.textTheme.bodySmall!.copyWith( - fontSize: 13, - color: ColorsManager.grayColor, - ), - ), - ), - ); - } - - void _showAddScheduleDialog(BuildContext context, - {ScheduleEntry? schedule, int? index}) { - final bloc = context.read(); - - if (schedule != null) { - bloc.add(InitializeAddScheduleEvent( - selectedTime: schedule.time, - selectedDays: schedule.selectedDays, - functionOn: schedule.functionOn, - isEditing: true, - index: index, - )); - } else { - bloc.add( - const InitializeAddScheduleEvent( - selectedDays: [false, false, false, false, false, false, false], - functionOn: false, - isEditing: false, - index: null, - selectedTime: null, - ), - ); - } - - showDialog( - context: context, - builder: (ctx) { - return BlocProvider.value( - value: bloc, - child: BlocBuilder( - builder: (context, state) { - if (state is WaterHeaterDeviceStatusLoaded) { - return AlertDialog( - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(20), - ), - content: Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - const SizedBox(), - Text( - 'Scheduling', - style: context.textTheme.titleLarge!.copyWith( - color: ColorsManager.dialogBlueTitle, - fontWeight: FontWeight.bold, - ), - ), - const SizedBox(), - ], - ), - const SizedBox(height: 24), - SizedBox( - width: 150, - height: 40, - child: DefaultButton( - padding: 8, - backgroundColor: ColorsManager.boxColor, - borderRadius: 15, - onPressed: () async { - TimeOfDay? time = await showTimePicker( - context: context, - initialTime: - state.selectedTime ?? TimeOfDay.now(), - ); - if (time != null) { - bloc.add(UpdateSelectedTimeEvent(time)); - } - }, - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Text( - state.selectedTime == null - ? 'Time' - : state.selectedTime!.format(context), - style: context.textTheme.bodySmall!.copyWith( - color: ColorsManager.grayColor, - ), - ), - const Icon( - Icons.access_time, - color: ColorsManager.grayColor, - size: 18, - ), - ], - ), - ), - ), - const SizedBox(height: 16), - _buildDayCheckboxes(context, state.selectedDays), - const SizedBox(height: 16), - _buildFunctionSwitch(context, state.functionOn), - ], - ), - actions: [ - SizedBox( - width: 200, - child: DefaultButton( - height: 40, - onPressed: () { - Navigator.pop(context); - }, - backgroundColor: ColorsManager.boxColor, - child: Text( - 'Cancel', - style: context.textTheme.bodyMedium, - ), - ), - ), - SizedBox( - width: 200, - child: DefaultButton( - height: 40, - onPressed: () { - if (state.selectedTime != null) { - if (state.isEditing && index != null) { - bloc.add(UpdateScheduleEntryEvent( - deviceId: state.status.uuid, - category: 'kg', - functionOn: state.functionOn, - )); - } else { - bloc.add(AddScheduleEvent( - category: 'kg', - time: state.selectedTime!, - selectedDays: state.selectedDays, - functionOn: state.functionOn, - )); - } - Navigator.pop(context); - } - }, - backgroundColor: ColorsManager.primaryColor, - child: const Text('Save'), - ), - ), - ], - ); - } - return const SizedBox(); - }, - ), - ); - }, - ); - } - - Widget _buildDayCheckboxes(BuildContext context, List selectedDays) { - return Row( - children: List.generate(7, (index) { - final dayLabels = ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun']; - return Row( - children: [ - Checkbox( - value: selectedDays[index], - onChanged: (bool? value) { - context - .read() - .add(UpdateSelectedDayEvent(index, value!)); - }, - ), - Text(dayLabels[index]), - ], - ); - }), - ); - } - - Widget _buildFunctionSwitch(BuildContext context, bool isOn) { - return Row( - children: [ - Text( - 'Function:', - style: context.textTheme.bodySmall! - .copyWith(color: ColorsManager.grayColor), - ), - const SizedBox(width: 10), - Radio( - value: true, - groupValue: isOn, - onChanged: (bool? value) { - context - .read() - .add(const UpdateFunctionOnEvent(true)); - }, - ), - const Text('On'), - const SizedBox(width: 10), - Radio( - value: false, - groupValue: isOn, - onChanged: (bool? value) { - context - .read() - .add(const UpdateFunctionOnEvent(false)); - }, - ), - const Text('Off'), - ], - ); - } - - Center _buildSaveStopCancelButtons( - BuildContext context, WaterHeaterDeviceStatusLoaded state) { - return Center( - child: SizedBox( - width: 400, - height: 50, - child: Row( - mainAxisAlignment: MainAxisAlignment.center, - mainAxisSize: MainAxisSize.max, - children: [ - Expanded( - child: DefaultButton( - height: 40, - onPressed: () { - Navigator.pop(context); - }, - backgroundColor: ColorsManager.boxColor, - child: Text( - 'Cancel', - style: context.textTheme.bodyMedium, - ), - ), - ), - const SizedBox(width: 20), - Expanded( - child: - (state.countdownRemaining != null && state.isActive == true) - ? DefaultButton( - height: 40, - onPressed: () { - late String code; - if (state.scheduleMode == ScheduleModes.countdown) { - code = 'countdown_1'; - } else if (state.scheduleMode == - ScheduleModes.inching) { - code = 'switch_inching'; - } - context - .read() - .add(StopScheduleEvent(widget.status.uuid)); - context.read().add( - ToggleWaterHeaterEvent( - deviceId: widget.status.uuid, - code: code, - value: 0, - ), - ); - }, - backgroundColor: Colors.red, - child: const Text('Stop'), - ) - : DefaultButton( - height: 40, - onPressed: () { - late String code; - if (state.scheduleMode == ScheduleModes.countdown) { - code = 'countdown_1'; - } else if (state.scheduleMode == - ScheduleModes.inching) { - code = 'switch_inching'; - } - context.read().add( - ToggleWaterHeaterEvent( - deviceId: widget.status.uuid, - code: code, - value: Duration( - hours: state.hours ?? 0, - minutes: state.minutes ?? 0) - .inSeconds, - ), - ); - }, - backgroundColor: ColorsManager.primaryColor, - child: const Text('Save'), - ), - ), - ], - ), - ), - ); - } - - List _buildCountDownAngInchingView( - BuildContext context, WaterHeaterDeviceStatusLoaded state) { - final isCountDown = - state.scheduleMode?.name == ScheduleModes.countdown.name; - return [ - Text( - isCountDown ? 'Countdown:' : 'Inching:', - style: context.textTheme.bodySmall!.copyWith( - fontSize: 13, - color: ColorsManager.grayColor, - ), - ), - const SizedBox(height: 8), - Visibility( - visible: !isCountDown, - child: const Text( - 'Once enabled this feature, each time the device is turned on, it will automatically turn of after a period time as pre-set.'), - ), - const SizedBox(height: 8), - _hourMinutesWheel(state, context) - ]; - } - - Row _hourMinutesWheel( - WaterHeaterDeviceStatusLoaded state, BuildContext context) { - final isActive = - (state.countdownRemaining != null && state.isActive == true); - return Row( - mainAxisAlignment: MainAxisAlignment.start, - children: [ - _buildPickerColumn(context, 'h', state.hours ?? 0, 24, (value) { - context.read().add(UpdateScheduleEvent( - scheduleMode: state.scheduleMode ?? ScheduleModes.countdown, - hours: value, - minutes: state.minutes ?? 0, - )); - }, isActive: isActive), - const SizedBox(width: 10), - _buildPickerColumn(context, 'm', state.minutes ?? 0, 60, (value) { - context.read().add(UpdateScheduleEvent( - scheduleMode: state.scheduleMode ?? ScheduleModes.countdown, - hours: state.hours ?? 0, - minutes: value, - )); - }, isActive: isActive), - ], - ); - } - - Widget _buildPickerColumn(BuildContext context, String label, - int initialValue, int itemCount, ValueChanged onSelected, - {required bool isActive}) { - return Row( - mainAxisSize: MainAxisSize.min, - children: [ - Container( - height: 40, - width: 80, - padding: const EdgeInsets.symmetric(horizontal: 16), - decoration: BoxDecoration( - color: ColorsManager.boxColor, - borderRadius: BorderRadius.circular(8), - ), - child: ListWheelScrollView.useDelegate( - key: ValueKey('$label-$initialValue'), - controller: FixedExtentScrollController( - initialItem: initialValue, - ), - itemExtent: 40.0, - physics: const FixedExtentScrollPhysics(), - onSelectedItemChanged: onSelected, - childDelegate: ListWheelChildBuilderDelegate( - builder: (context, index) { - return Center( - child: Text( - index.toString().padLeft(2, '0'), - style: TextStyle( - fontSize: 24, - color: isActive ? ColorsManager.grayColor : Colors.black, - ), - ), - ); - }, - childCount: itemCount, - ), - ), - ), - const SizedBox(width: 8), - Text( - label, - style: const TextStyle( - color: ColorsManager.grayColor, - fontSize: 18, - ), - ), - ], - ); - } } diff --git a/lib/pages/device_managment/water_heater/widgets/schedule_header.dart b/lib/pages/device_managment/water_heater/widgets/schedule_header.dart new file mode 100644 index 00000000..87afe430 --- /dev/null +++ b/lib/pages/device_managment/water_heater/widgets/schedule_header.dart @@ -0,0 +1,46 @@ +import 'package:flutter/material.dart'; +import 'package:syncrow_web/utils/color_manager.dart'; + +class ScheduleHeader extends StatelessWidget { + const ScheduleHeader({super.key}); + + @override + Widget build(BuildContext context) { + return Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + const SizedBox(), + Text( + 'Scheduling', + style: TextStyle( + fontWeight: FontWeight.bold, + fontSize: 22, + color: ColorsManager.dialogBlueTitle, + ), + ), + Container( + width: 25, + decoration: BoxDecoration( + color: Colors.transparent, + shape: BoxShape.circle, + border: Border.all( + color: Colors.grey, + width: 1.0, + ), + ), + child: IconButton( + padding: const EdgeInsets.all(1), + icon: const Icon( + Icons.close, + color: Colors.grey, + size: 18, + ), + onPressed: () { + Navigator.of(context).pop(); + }, + ), + ), + ], + ); + } +} diff --git a/lib/pages/device_managment/water_heater/widgets/schedule_managment_ui.dart b/lib/pages/device_managment/water_heater/widgets/schedule_managment_ui.dart new file mode 100644 index 00000000..1710c439 --- /dev/null +++ b/lib/pages/device_managment/water_heater/widgets/schedule_managment_ui.dart @@ -0,0 +1,50 @@ +import 'package:flutter/material.dart'; +import 'package:syncrow_web/pages/common/buttons/default_button.dart'; +import 'package:syncrow_web/pages/device_managment/water_heater/bloc/water_heater_bloc.dart'; +import 'package:syncrow_web/pages/device_managment/water_heater/widgets/schedule_table.dart'; +import 'package:syncrow_web/utils/color_manager.dart'; +import 'package:syncrow_web/utils/extension/build_context_x.dart'; + +class ScheduleManagementUI extends StatelessWidget { + final WaterHeaterDeviceStatusLoaded state; + final Function onAddSchedule; + + const ScheduleManagementUI({ + super.key, + required this.state, + required this.onAddSchedule, + }); + + @override + Widget build(BuildContext context) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + SizedBox( + width: 170, + height: 40, + child: DefaultButton( + borderColor: ColorsManager.boxColor, + padding: 2, + backgroundColor: ColorsManager.graysColor, + borderRadius: 15, + onPressed: () => onAddSchedule(), + child: Row( + children: [ + const Icon(Icons.add, color: ColorsManager.primaryColor), + Text( + ' Add new schedule', + style: context.textTheme.bodySmall!.copyWith( + color: ColorsManager.blackColor, + ), + ), + ], + ), + ), + ), + const SizedBox(height: 20), + ScheduleTableWidget(state: state), + ], + ); + } +} diff --git a/lib/pages/device_managment/water_heater/widgets/schedule_mode_buttons.dart b/lib/pages/device_managment/water_heater/widgets/schedule_mode_buttons.dart new file mode 100644 index 00000000..f1307d5f --- /dev/null +++ b/lib/pages/device_managment/water_heater/widgets/schedule_mode_buttons.dart @@ -0,0 +1,45 @@ +import 'package:flutter/material.dart'; +import 'package:syncrow_web/pages/common/buttons/default_button.dart'; +import 'package:syncrow_web/utils/color_manager.dart'; +import 'package:syncrow_web/utils/extension/build_context_x.dart'; + +class ScheduleModeButtons extends StatelessWidget { + final VoidCallback onSave; + + const ScheduleModeButtons({ + super.key, + required this.onSave, + }); + + @override + Widget build(BuildContext context) { + return Row( + mainAxisAlignment: MainAxisAlignment.center, + mainAxisSize: MainAxisSize.max, + children: [ + Expanded( + child: DefaultButton( + height: 40, + onPressed: () { + Navigator.pop(context); + }, + backgroundColor: ColorsManager.boxColor, + child: Text( + 'Cancel', + style: context.textTheme.bodyMedium, + ), + ), + ), + const SizedBox(width: 20), + Expanded( + child: DefaultButton( + height: 40, + onPressed: onSave, + backgroundColor: ColorsManager.primaryColor, + child: const Text('Save'), + ), + ), + ], + ); + } +} diff --git a/lib/pages/device_managment/water_heater/widgets/schedule_mode_selector.dart b/lib/pages/device_managment/water_heater/widgets/schedule_mode_selector.dart new file mode 100644 index 00000000..7afd96de --- /dev/null +++ b/lib/pages/device_managment/water_heater/widgets/schedule_mode_selector.dart @@ -0,0 +1,86 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:syncrow_web/pages/device_managment/water_heater/bloc/water_heater_bloc.dart'; +import 'package:syncrow_web/utils/color_manager.dart'; +import 'package:syncrow_web/utils/extension/build_context_x.dart'; +import 'package:syncrow_web/pages/device_managment/water_heater/models/water_heater_status_model.dart'; + +class ScheduleModeSelector extends StatelessWidget { + final WaterHeaterDeviceStatusLoaded state; + + const ScheduleModeSelector({super.key, required this.state}); + + @override + Widget build(BuildContext context) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'Type:', + style: context.textTheme.bodySmall!.copyWith( + fontSize: 13, + color: ColorsManager.grayColor, + ), + ), + const SizedBox(height: 4), + Row( + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + children: [ + _buildRadioTile( + context, 'Countdown', ScheduleModes.countdown, state), + _buildRadioTile(context, 'Schedule', ScheduleModes.schedule, state), + _buildRadioTile( + context, 'Circulate', ScheduleModes.circulate, state), + _buildRadioTile(context, 'Inching', ScheduleModes.inching, state), + ], + ), + ], + ); + } + + Widget _buildRadioTile(BuildContext context, String label, ScheduleModes mode, + WaterHeaterDeviceStatusLoaded state) { + return Flexible( + child: ListTile( + contentPadding: EdgeInsets.zero, + title: Text( + label, + style: context.textTheme.bodySmall!.copyWith( + fontSize: 13, + color: ColorsManager.blackColor, + ), + ), + leading: Radio( + value: mode, + groupValue: state.scheduleMode, + onChanged: (ScheduleModes? value) { + if (value != null) { + if (value == ScheduleModes.countdown) { + context.read().add(UpdateScheduleEvent( + scheduleMode: value, + hours: state.countdownHours ?? 0, + minutes: state.countdownMinutes ?? 0, + )); + } else if (value == ScheduleModes.inching) { + context.read().add(UpdateScheduleEvent( + scheduleMode: value, + hours: state.inchingHours ?? 0, + minutes: state.inchingMinutes ?? 0, + )); + } + + if (value == ScheduleModes.schedule) { + context.read().add( + GetSchedulesEvent( + category: 'kg', + uuid: state.status.uuid, + ), + ); + } + } + }, + ), + ), + ); + } +} diff --git a/lib/pages/device_managment/water_heater/widgets/schedule_row_widget.dart b/lib/pages/device_managment/water_heater/widgets/schedule_row_widget.dart new file mode 100644 index 00000000..760b86c7 --- /dev/null +++ b/lib/pages/device_managment/water_heater/widgets/schedule_row_widget.dart @@ -0,0 +1,76 @@ +import 'package:flutter/material.dart'; +import 'package:syncrow_web/pages/device_managment/water_heater/models/schedule_model.dart'; +import 'package:syncrow_web/utils/format_date_time.dart'; +import 'package:syncrow_web/utils/color_manager.dart'; + +class ScheduleRowWidget extends StatelessWidget { + final ScheduleModel schedule; + final int index; + final Function onEdit; + final Function onDelete; + + const ScheduleRowWidget({ + super.key, + required this.schedule, + required this.index, + required this.onEdit, + required this.onDelete, + }); + + @override + Widget build(BuildContext context) { + return Table( + border: TableBorder.all(color: ColorsManager.graysColor), + defaultVerticalAlignment: TableCellVerticalAlignment.middle, + children: [ + TableRow( + children: [ + Center( + child: schedule.function.value + ? const Icon(Icons.radio_button_checked, + color: ColorsManager.blueColor) + : const Icon(Icons.radio_button_unchecked), + ), + Center(child: Text(_getSelectedDays(schedule.selectedDays ?? []))), + Center(child: Text(formatIsoStringToTime(schedule.time))), + Center(child: Text(schedule.function.value ? 'On' : 'Off')), + Center( + child: Wrap( + runAlignment: WrapAlignment.center, + children: [ + TextButton( + style: TextButton.styleFrom(padding: EdgeInsets.zero), + onPressed: () => onEdit(), + child: const Text( + 'Edit', + style: TextStyle(color: ColorsManager.blueColor), + ), + ), + TextButton( + style: TextButton.styleFrom(padding: EdgeInsets.zero), + onPressed: () => onDelete(), + child: const Text( + 'Delete', + style: TextStyle(color: ColorsManager.blueColor), + ), + ), + ], + ), + ), + ], + ), + ], + ); + } + + String _getSelectedDays(List selectedDays) { + final days = ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun']; + List selectedDaysStr = []; + for (int i = 0; i < selectedDays.length; i++) { + if (selectedDays[i]) { + selectedDaysStr.add(days[i]); + } + } + return selectedDaysStr.join(', '); + } +} diff --git a/lib/pages/device_managment/water_heater/widgets/schedule_table.dart b/lib/pages/device_managment/water_heater/widgets/schedule_table.dart new file mode 100644 index 00000000..b997c7e0 --- /dev/null +++ b/lib/pages/device_managment/water_heater/widgets/schedule_table.dart @@ -0,0 +1,198 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:flutter_svg/flutter_svg.dart'; +import 'package:syncrow_web/pages/device_managment/water_heater/bloc/water_heater_bloc.dart'; +import 'package:syncrow_web/pages/device_managment/water_heater/models/schedule_model.dart'; +import 'package:syncrow_web/utils/color_manager.dart'; +import 'package:syncrow_web/utils/constants/assets.dart'; +import 'package:syncrow_web/utils/extension/build_context_x.dart'; +import 'package:syncrow_web/utils/format_date_time.dart'; + +import '../helper/add_schedule_dialog_helper.dart'; + +class ScheduleTableWidget extends StatelessWidget { + final WaterHeaterDeviceStatusLoaded state; + + const ScheduleTableWidget({ + super.key, + required this.state, + }); + + @override + Widget build(BuildContext context) { + return Column( + children: [ + Table( + border: TableBorder.all( + color: ColorsManager.graysColor, + borderRadius: const BorderRadius.only( + topLeft: Radius.circular(20), topRight: Radius.circular(20)), + ), + children: [ + TableRow( + decoration: const BoxDecoration( + color: ColorsManager.boxColor, + borderRadius: BorderRadius.only( + topLeft: Radius.circular(20), + topRight: Radius.circular(20), + ), + ), + children: [ + _buildTableHeader('Active'), + _buildTableHeader('Days'), + _buildTableHeader('Time'), + _buildTableHeader('Function'), + _buildTableHeader('Action'), + ], + ), + ], + ), + BlocBuilder( + builder: (context, state) { + if (state is ScheduleLoadingState) { + return const SizedBox( + height: 200, + child: Center(child: CircularProgressIndicator())); + } + if (state is WaterHeaterDeviceStatusLoaded && + state.schedules.isEmpty) { + return _buildEmptyState(context); + } else if (state is WaterHeaterDeviceStatusLoaded) { + return Container( + height: 200, + decoration: BoxDecoration( + border: Border.all(color: ColorsManager.graysColor), + borderRadius: const BorderRadius.only( + bottomLeft: Radius.circular(20), + bottomRight: Radius.circular(20)), + ), + child: _buildTableBody(state, context)); + } + return const SizedBox(); + }, + ), + ], + ); + } + + Widget _buildEmptyState(BuildContext context) { + return Container( + height: 200, + decoration: BoxDecoration( + border: Border.all(color: ColorsManager.graysColor), + borderRadius: const BorderRadius.only( + bottomLeft: Radius.circular(20), bottomRight: Radius.circular(20)), + ), + child: Center( + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + SvgPicture.asset(Assets.emptyRecords, width: 40, height: 40), + const SizedBox(height: 8), + Padding( + padding: const EdgeInsets.all(8.0), + child: Text( + 'No schedules added yet', + style: context.textTheme.bodySmall!.copyWith( + fontSize: 13, + color: ColorsManager.grayColor, + ), + ), + ), + ], + ), + ), + ); + } + + Widget _buildTableBody( + WaterHeaterDeviceStatusLoaded state, BuildContext context) { + return SingleChildScrollView( + child: Table( + border: TableBorder.all(color: ColorsManager.graysColor), + defaultVerticalAlignment: TableCellVerticalAlignment.middle, + children: [ + for (int i = 0; i < state.schedules.length; i++) + _buildScheduleRow(state.schedules[i], i, context), + ], + ), + ); + } + + Widget _buildTableHeader(String label) { + return TableCell( + child: Padding( + padding: const EdgeInsets.all(12), + child: Text( + label, + style: const TextStyle( + fontSize: 13, + color: ColorsManager.grayColor, + ), + ), + ), + ); + } + + TableRow _buildScheduleRow( + ScheduleModel schedule, int index, BuildContext context) { + return TableRow( + children: [ + Center( + child: schedule.function.value + ? const Icon(Icons.radio_button_checked, + color: ColorsManager.blueColor) + : const Icon(Icons.radio_button_unchecked)), + Center( + child: Text(_getSelectedDays( + ScheduleModel.parseSelectedDays(schedule.days)))), + Center(child: Text(formatIsoStringToTime(schedule.time))), + Center(child: Text(schedule.function.value ? 'On' : 'Off')), + Center( + child: Wrap( + runAlignment: WrapAlignment.center, + children: [ + TextButton( + style: TextButton.styleFrom(padding: EdgeInsets.zero), + onPressed: () { + ScheduleDialogHelper.showAddScheduleDialog(context, + schedule: schedule, index: index, isEdit: true); + }, + child: Text( + 'Edit', + style: context.textTheme.bodySmall! + .copyWith(color: ColorsManager.blueColor), + ), + ), + TextButton( + style: TextButton.styleFrom(padding: EdgeInsets.zero), + onPressed: () { + context.read().add(DeleteScheduleEvent( + index: index, + scheduleId: schedule.scheduleId, + )); + }, + child: Text( + 'Delete', + style: context.textTheme.bodySmall! + .copyWith(color: ColorsManager.blueColor), + ), + ), + ], + ), + ), + ], + ); + } + + String _getSelectedDays(List selectedDays) { + final days = ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun']; + List selectedDaysStr = []; + for (int i = 0; i < selectedDays.length; i++) { + if (selectedDays[i]) { + selectedDaysStr.add(days[i]); + } + } + return selectedDaysStr.join(', '); + } +} diff --git a/lib/services/devices_mang_api.dart b/lib/services/devices_mang_api.dart index 049538d3..9ac42eca 100644 --- a/lib/services/devices_mang_api.dart +++ b/lib/services/devices_mang_api.dart @@ -195,6 +195,29 @@ class DevicesManagementApi { } } + Future> getDeviceSchedules( + String uuid, String category) async { + try { + final response = await HTTPService().get( + path: ApiEndpoints.scheduleByDeviceId + .replaceAll('{deviceUuid}', uuid) + .replaceAll('{category}', category), + showServerMessage: true, + expectedResponseModel: (json) { + List schedules = []; + for (var schedule in json['schedules']) { + schedules.add(ScheduleModel.fromJson(schedule)); + } + return schedules; + }, + ); + return response; + } catch (e) { + debugPrint('Error fetching $e'); + return []; + } + } + Future updateScheduleRecord( {required bool enable, required String uuid, diff --git a/lib/utils/constants/api_const.dart b/lib/utils/constants/api_const.dart index 0c17e2bc..2b39fc03 100644 --- a/lib/utils/constants/api_const.dart +++ b/lib/utils/constants/api_const.dart @@ -40,6 +40,8 @@ abstract class ApiEndpoints { '/device/report-logs/{uuid}?code={code}&startTime={startTime}&endTime={endTime}'; static const String scheduleByDeviceId = '/schedule/{deviceUuid}'; + static const String getScheduleByDeviceId = + '/schedule/{deviceUuid}?category={category}'; static const String deleteScheduleByDeviceId = '/schedule/{deviceUuid}/{scheduleUuid}'; static const String updateScheduleByDeviceId = diff --git a/lib/utils/format_date_time.dart b/lib/utils/format_date_time.dart index e214b46c..470acb97 100644 --- a/lib/utils/format_date_time.dart +++ b/lib/utils/format_date_time.dart @@ -23,4 +23,9 @@ String formatTimeOfDayToISO(TimeOfDay time, {DateTime? currentDate}) { ); return dateTime.toUtc().toIso8601String(); -} \ No newline at end of file +} + +String formatIsoStringToTime(String isoString) { + final dateTime = DateTime.parse(isoString); + return DateFormat('hh:mm a').format(dateTime); +}