refactor schedule view and cleaning

This commit is contained in:
ashrafzarkanisala
2024-09-22 20:47:34 +03:00
parent b3d891b2c8
commit 3a28f0ef9a
21 changed files with 1540 additions and 902 deletions

View File

@ -5,7 +5,6 @@ import 'package:bloc/bloc.dart';
import 'package:equatable/equatable.dart'; import 'package:equatable/equatable.dart';
import 'package:flutter/material.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/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/schedule_model.dart';
import 'package:syncrow_web/pages/device_managment/water_heater/models/water_heater_status_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/services/devices_mang_api.dart';
@ -21,17 +20,20 @@ class WaterHeaterBloc extends Bloc<WaterHeaterEvent, WaterHeaterState> {
on<UpdateScheduleEvent>(_updateScheduleEvent); on<UpdateScheduleEvent>(_updateScheduleEvent);
on<StopScheduleEvent>(_stopScheduleEvent); on<StopScheduleEvent>(_stopScheduleEvent);
on<DecrementCountdownEvent>(_onDecrementCountdown); on<DecrementCountdownEvent>(_onDecrementCountdown);
on<AddScheduleEvent>(_onAddSchedule);
on<DeleteScheduleEvent>(_onDeleteSchedule);
on<UpdateScheduleEntryEvent>(_onUpdateSchedule);
on<InitializeAddScheduleEvent>(_initializeAddSchedule); on<InitializeAddScheduleEvent>(_initializeAddSchedule);
on<UpdateSelectedTimeEvent>(_updateSelectedTime); on<UpdateSelectedTimeEvent>(_updateSelectedTime);
on<UpdateSelectedDayEvent>(_updateSelectedDay); on<UpdateSelectedDayEvent>(_updateSelectedDay);
on<UpdateFunctionOnEvent>(_updateFunctionOn); on<UpdateFunctionOnEvent>(_updateFunctionOn);
on<GetSchedulesEvent>(_getSchedule);
on<AddScheduleEvent>(_onAddSchedule);
on<DeleteScheduleEvent>(_onDeleteSchedule);
on<UpdateScheduleEntryEvent>(_onUpdateSchedule);
} }
late WaterHeaterStatusModel deviceStatus; late WaterHeaterStatusModel deviceStatus;
Timer? _countdownTimer; Timer? _countdownTimer;
// Timer? _inchingTimer;
FutureOr<void> _initializeAddSchedule( FutureOr<void> _initializeAddSchedule(
InitializeAddScheduleEvent event, InitializeAddScheduleEvent event,
@ -87,22 +89,37 @@ class WaterHeaterBloc extends Bloc<WaterHeaterEvent, WaterHeaterState> {
) async { ) async {
final currentState = state; final currentState = state;
if (currentState is WaterHeaterDeviceStatusLoaded) { if (currentState is WaterHeaterDeviceStatusLoaded) {
if (event.scheduleMode == ScheduleModes.schedule) {
emit(currentState.copyWith(
scheduleMode: ScheduleModes.schedule,
));
}
if (event.scheduleMode == ScheduleModes.countdown) {
final countdownRemaining = final countdownRemaining =
// currentState.isActive == true
// ? currentState.countdownRemaining
// :
Duration(hours: event.hours, minutes: event.minutes); Duration(hours: event.hours, minutes: event.minutes);
emit(currentState.copyWith( emit(currentState.copyWith(
scheduleMode: event.scheduleMode, scheduleMode: ScheduleModes.countdown,
hours: countdownRemaining.inHours, countdownHours: countdownRemaining.inHours,
minutes: countdownRemaining.inMinutes % 60, countdownMinutes: countdownRemaining.inMinutes % 60,
isActive: currentState.isActive, isCountdownActive: currentState.isCountdownActive,
countdownRemaining: countdownRemaining, countdownRemaining: countdownRemaining,
)); ));
if (!currentState.isActive! && countdownRemaining > Duration.zero) { if (!currentState.isCountdownActive! &&
_startCountdown(emit, countdownRemaining); 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,29 +147,40 @@ class WaterHeaterBloc extends Bloc<WaterHeaterEvent, WaterHeaterState> {
emit: emit, emit: emit,
); );
if (success && if (success) {
(event.code == "countdown_1" || event.code == "switch_inching")) { if (event.code == "countdown_1") {
final countdownDuration = Duration(seconds: event.value); final countdownDuration = Duration(seconds: event.value);
emit(currentState.copyWith( emit(currentState.copyWith(
status: deviceStatus, countdownHours: countdownDuration.inHours,
scheduleMode: deviceStatus.scheduleMode, countdownMinutes: countdownDuration.inMinutes % 60,
hours: countdownDuration.inHours,
minutes: (countdownDuration.inMinutes % 60),
isActive: true,
countdownRemaining: countdownDuration, countdownRemaining: countdownDuration,
isCountdownActive: true,
)); ));
if (countdownDuration.inSeconds > 0) { if (countdownDuration.inSeconds > 0) {
_startCountdown(emit, countdownDuration); _startCountdownTimer(emit, countdownDuration);
} else { } else {
_countdownTimer?.cancel(); _countdownTimer?.cancel();
emit(currentState.copyWith( emit(currentState.copyWith(
hours: 0, countdownHours: 0,
minutes: 0, countdownMinutes: 0,
isActive: false,
countdownRemaining: Duration.zero, 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<WaterHeaterEvent, WaterHeaterState> {
) async { ) async {
if (state is WaterHeaterDeviceStatusLoaded) { if (state is WaterHeaterDeviceStatusLoaded) {
final currentState = state as WaterHeaterDeviceStatusLoaded; final currentState = state as WaterHeaterDeviceStatusLoaded;
final isCountDown = currentState.scheduleMode == ScheduleModes.countdown;
_countdownTimer?.cancel(); _countdownTimer?.cancel();
deviceStatus = deviceStatus.copyWith( if (isCountDown) {
emit(currentState.copyWith(
countdownHours: 0, countdownHours: 0,
countdownMinutes: 0, countdownMinutes: 0,
scheduleMode: ScheduleModes.countdown,
);
emit(currentState.copyWith(
status: deviceStatus,
scheduleMode: ScheduleModes.countdown,
hours: 0,
minutes: 0,
isActive: false,
countdownRemaining: Duration.zero, countdownRemaining: Duration.zero,
isCountdownActive: false,
)); ));
} else if (currentState.scheduleMode == ScheduleModes.inching) {
emit(currentState.copyWith(
inchingHours: 0,
inchingMinutes: 0,
isInchingActive: false,
));
}
try { try {
final status = await DevicesManagementApi().deviceControl( final status = await DevicesManagementApi().deviceControl(
event.deviceId, event.deviceId,
Status(code: 'countdown_1', value: 0), Status(
code: isCountDown ? 'countdown_1' : 'switch_inching', value: 0),
); );
if (!status) { if (!status) {
emit(const WaterHeaterFailedState(error: 'Failed to stop schedule.')); emit(const WaterHeaterFailedState(error: 'Failed to stop schedule.'));
@ -207,30 +237,66 @@ class WaterHeaterBloc extends Bloc<WaterHeaterEvent, WaterHeaterState> {
deviceStatus = deviceStatus =
WaterHeaterStatusModel.fromJson(event.deviceId, status.status); WaterHeaterStatusModel.fromJson(event.deviceId, status.status);
if (deviceStatus.countdownHours > 0 || if (deviceStatus.scheduleMode == ScheduleModes.countdown) {
deviceStatus.countdownMinutes > 0) { final countdownRemaining = Duration(
final remainingDuration = Duration(
hours: deviceStatus.countdownHours, hours: deviceStatus.countdownHours,
minutes: deviceStatus.countdownMinutes, minutes: deviceStatus.countdownMinutes,
); );
if (countdownRemaining > Duration.zero) {
emit(WaterHeaterDeviceStatusLoaded( emit(WaterHeaterDeviceStatusLoaded(
deviceStatus, deviceStatus,
scheduleMode: deviceStatus.scheduleMode, scheduleMode: ScheduleModes.countdown,
hours: deviceStatus.countdownHours, countdownHours: deviceStatus.countdownHours,
minutes: deviceStatus.countdownMinutes, countdownMinutes: deviceStatus.countdownMinutes,
isActive: true, isCountdownActive: true,
countdownRemaining: remainingDuration, 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 { } else {
emit(WaterHeaterDeviceStatusLoaded( emit(WaterHeaterDeviceStatusLoaded(
deviceStatus, deviceStatus,
scheduleMode: deviceStatus.scheduleMode, scheduleMode: deviceStatus.scheduleMode,
hours: 0, countdownHours: 0,
minutes: 0, countdownMinutes: 0,
isActive: false, inchingHours: 0,
inchingMinutes: 0,
isCountdownActive: false,
isInchingActive: false,
)); ));
} }
} catch (e) { } catch (e) {
@ -238,6 +304,28 @@ class WaterHeaterBloc extends Bloc<WaterHeaterEvent, WaterHeaterState> {
} }
} }
void _startCountdownTimer(
Emitter<WaterHeaterState> emit,
Duration countdownRemaining,
) {
_countdownTimer?.cancel();
_countdownTimer = Timer.periodic(const Duration(minutes: 1), (timer) {
add(DecrementCountdownEvent());
});
}
// void _startInchingTimer(
// Emitter<WaterHeaterState> emit,
// Duration inchingDuration,
// ) {
// _inchingTimer?.cancel();
// _inchingTimer = Timer.periodic(const Duration(minutes: 1), (timer) {
// add(DecrementInchingEvent());
// });
// }
_onDecrementCountdown( _onDecrementCountdown(
DecrementCountdownEvent event, DecrementCountdownEvent event,
Emitter<WaterHeaterState> emit, Emitter<WaterHeaterState> emit,
@ -253,9 +341,9 @@ class WaterHeaterBloc extends Bloc<WaterHeaterEvent, WaterHeaterState> {
if (newRemaining <= Duration.zero) { if (newRemaining <= Duration.zero) {
_countdownTimer?.cancel(); _countdownTimer?.cancel();
emit(currentState.copyWith( emit(currentState.copyWith(
hours: 0, countdownHours: 0,
minutes: 0, countdownMinutes: 0,
isActive: false, isCountdownActive: false,
countdownRemaining: Duration.zero, countdownRemaining: Duration.zero,
)); ));
return; return;
@ -267,22 +355,44 @@ class WaterHeaterBloc extends Bloc<WaterHeaterEvent, WaterHeaterState> {
int newMinutes = (totalSeconds % 3600) ~/ 60; int newMinutes = (totalSeconds % 3600) ~/ 60;
emit(currentState.copyWith( emit(currentState.copyWith(
hours: newHours, countdownHours: newHours,
minutes: newMinutes, countdownMinutes: newMinutes,
countdownRemaining: newRemaining, countdownRemaining: newRemaining,
)); ));
} }
} }
} }
void _startCountdown( // FutureOr<void> _onDecrementInching(
Emitter<WaterHeaterState> emit, Duration countdownRemaining) { // DecrementInchingEvent event,
_countdownTimer?.cancel(); // Emitter<WaterHeaterState> emit,
// ) {
// if (state is WaterHeaterDeviceStatusLoaded) {
// final currentState = state as WaterHeaterDeviceStatusLoaded;
_countdownTimer = Timer.periodic(const Duration(minutes: 1), (timer) { // if (currentState.inchingHours > 0 || currentState.inchingMinutes > 0) {
add(DecrementCountdownEvent()); // 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<bool> _runDebounce({ Future<bool> _runDebounce({
required String deviceId, required String deviceId,
@ -353,6 +463,36 @@ class WaterHeaterBloc extends Bloc<WaterHeaterEvent, WaterHeaterState> {
} }
} }
@override
Future<void> close() {
_countdownTimer?.cancel();
return super.close();
}
FutureOr<void> _getSchedule(
GetSchedulesEvent event, Emitter<WaterHeaterState> emit) async {
emit(ScheduleLoadingState());
try {
// List<ScheduleModel> schedules = await DevicesManagementApi()
// .getDeviceSchedules(deviceStatus.uuid, event.category);
List<ScheduleModel> 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<void> _onAddSchedule( FutureOr<void> _onAddSchedule(
AddScheduleEvent event, AddScheduleEvent event,
Emitter<WaterHeaterState> emit, Emitter<WaterHeaterState> emit,
@ -360,59 +500,30 @@ class WaterHeaterBloc extends Bloc<WaterHeaterEvent, WaterHeaterState> {
if (state is WaterHeaterDeviceStatusLoaded) { if (state is WaterHeaterDeviceStatusLoaded) {
final currentState = state as WaterHeaterDeviceStatusLoaded; final currentState = state as WaterHeaterDeviceStatusLoaded;
ScheduleModel sendSchedule = ScheduleModel( ScheduleModel newSchedule = ScheduleModel(
category: event.category, category: event.category,
time: formatTimeOfDayToISO(event.time), time: formatTimeOfDayToISO(event.time),
function: Status(code: 'switch_1', value: event.functionOn), function: Status(code: 'switch_1', value: event.functionOn),
days: _getSelectedDaysString(event.selectedDays), days: ScheduleModel.convertSelectedDaysToStrings(event.selectedDays),
); );
// emit(ScheduleLoadingState());
bool success = await DevicesManagementApi() bool success = await DevicesManagementApi()
.addScheduleRecord(sendSchedule, currentState.status.uuid); .addScheduleRecord(newSchedule, currentState.status.uuid);
if (success) { if (success) {
final newSchedule = ScheduleEntry(
selectedDays: event.selectedDays,
time: event.time,
functionOn: event.functionOn,
category: event.category,
);
final updatedSchedules = final updatedSchedules =
List<ScheduleEntry>.from(currentState.schedules)..add(newSchedule); List<ScheduleModel>.from(currentState.schedules)..add(newSchedule);
emit(currentState.copyWith(schedules: updatedSchedules)); emit(currentState.copyWith(schedules: updatedSchedules));
} else { } else {
emit(const WaterHeaterFailedState( emit(currentState);
error: 'Failed to add schedule. Please try again.')); //emit(const WaterHeaterFailedState(error: 'Failed to add schedule.'));
} }
} }
} }
List<String> _getSelectedDaysString(List<bool> selectedDays) {
final days = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'];
List<String> selectedDaysStr = [];
for (int i = 0; i < selectedDays.length; i++) {
if (selectedDays[i]) {
selectedDaysStr.add(days[i]);
}
}
return selectedDaysStr;
}
FutureOr<void> _onDeleteSchedule(
DeleteScheduleEvent event,
Emitter<WaterHeaterState> emit,
) {
if (state is WaterHeaterDeviceStatusLoaded) {
final currentState = state as WaterHeaterDeviceStatusLoaded;
final updatedSchedules = List<ScheduleEntry>.from(currentState.schedules)
..removeAt(event.index);
emit(currentState.copyWith(schedules: updatedSchedules));
}
}
FutureOr<void> _onUpdateSchedule( FutureOr<void> _onUpdateSchedule(
UpdateScheduleEntryEvent event, UpdateScheduleEntryEvent event,
Emitter<WaterHeaterState> emit, Emitter<WaterHeaterState> emit,
@ -420,44 +531,53 @@ class WaterHeaterBloc extends Bloc<WaterHeaterEvent, WaterHeaterState> {
if (state is WaterHeaterDeviceStatusLoaded) { if (state is WaterHeaterDeviceStatusLoaded) {
final currentState = state as WaterHeaterDeviceStatusLoaded; final currentState = state as WaterHeaterDeviceStatusLoaded;
// Get the current schedule ID or UUID (assuming it's stored in the schedules) ScheduleModel updatedSchedule = currentState.schedules[event.index]
String scheduleId = .copyWith(
""; // Retrieve the actual schedule ID based on your model function: Status(code: 'switch_1', value: event.functionOn));
// emit(ScheduleLoadingState());
// Call the API to update the schedule
bool success = await DevicesManagementApi().updateScheduleRecord( bool success = await DevicesManagementApi().updateScheduleRecord(
enable: event.functionOn, enable: event.functionOn,
uuid: event.deviceId, uuid: currentState.status.uuid,
scheduleId: scheduleId, scheduleId: event.scheduleId,
); );
if (success) { if (success) {
final updatedSchedules = final updatedSchedules =
List<ScheduleEntry>.from(currentState.schedules); List<ScheduleModel>.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 { } else {
emit(const WaterHeaterFailedState( emit(currentState);
error: 'Failed to update schedule. Please try again.')); // emit(const WaterHeaterFailedState(error: 'Failed to update schedule.'));
} }
} }
} }
@override FutureOr<void> _onDeleteSchedule(
Future<void> close() { DeleteScheduleEvent event,
_countdownTimer?.cancel(); Emitter<WaterHeaterState> emit,
return super.close(); ) 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<ScheduleModel>.from(currentState.schedules)
..removeAt(event.index);
emit(currentState.copyWith(schedules: updatedSchedules));
} else {
emit(currentState);
// emit(const WaterHeaterFailedState(error: 'Failed to delete schedule.'));
}
}
} }
} }

View File

@ -82,26 +82,44 @@ final class AddScheduleEvent extends WaterHeaterEvent {
final class DeleteScheduleEvent extends WaterHeaterEvent { final class DeleteScheduleEvent extends WaterHeaterEvent {
final int index; final int index;
final String scheduleId;
const DeleteScheduleEvent(this.index); const DeleteScheduleEvent({required this.index, required this.scheduleId});
@override @override
List<Object?> get props => [index]; List<Object?> get props => [index, scheduleId];
} }
final class UpdateScheduleEntryEvent extends WaterHeaterEvent { final class UpdateScheduleEntryEvent extends WaterHeaterEvent {
final bool functionOn; final bool functionOn;
final String category; final String category;
final String deviceId; final String deviceId;
final int index;
final String scheduleId;
const UpdateScheduleEntryEvent({ const UpdateScheduleEntryEvent({
required this.functionOn, required this.functionOn,
required this.category, required this.category,
required this.deviceId, required this.deviceId,
required this.scheduleId,
required this.index,
}); });
@override @override
List<Object?> get props => [category, functionOn, deviceId]; List<Object?> 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<Object?> get props => [uuid, category];
} }
class InitializeAddScheduleEvent extends WaterHeaterEvent { class InitializeAddScheduleEvent extends WaterHeaterEvent {

View File

@ -13,14 +13,24 @@ final class WaterHeaterInitial extends WaterHeaterState {}
final class WaterHeaterLoadingState extends WaterHeaterState {} final class WaterHeaterLoadingState extends WaterHeaterState {}
final class ScheduleLoadingState extends WaterHeaterState {}
class WaterHeaterDeviceStatusLoaded extends WaterHeaterState { class WaterHeaterDeviceStatusLoaded extends WaterHeaterState {
final WaterHeaterStatusModel status; final WaterHeaterStatusModel status;
final ScheduleModes? scheduleMode; final ScheduleModes? scheduleMode;
final int? hours;
final int? minutes; // Countdown-specific
final bool? isActive; final int? countdownHours;
final int? countdownMinutes;
final Duration? countdownRemaining; final Duration? countdownRemaining;
final List<ScheduleEntry> schedules; final bool? isCountdownActive;
// Inching-specific
final int? inchingHours;
final int? inchingMinutes;
final bool? isInchingActive;
final List<ScheduleModel> schedules;
final List<bool> selectedDays; final List<bool> selectedDays;
final TimeOfDay? selectedTime; final TimeOfDay? selectedTime;
final bool functionOn; final bool functionOn;
@ -29,10 +39,13 @@ class WaterHeaterDeviceStatusLoaded extends WaterHeaterState {
const WaterHeaterDeviceStatusLoaded( const WaterHeaterDeviceStatusLoaded(
this.status, { this.status, {
this.scheduleMode, this.scheduleMode,
this.hours, this.countdownHours,
this.minutes, this.countdownMinutes,
this.isActive,
this.countdownRemaining, this.countdownRemaining,
this.isCountdownActive,
this.inchingHours,
this.inchingMinutes,
this.isInchingActive,
this.schedules = const [], this.schedules = const [],
this.selectedDays = const [false, false, false, false, false, false, false], this.selectedDays = const [false, false, false, false, false, false, false],
this.selectedTime, this.selectedTime,
@ -44,10 +57,13 @@ class WaterHeaterDeviceStatusLoaded extends WaterHeaterState {
List<Object?> get props => [ List<Object?> get props => [
status, status,
scheduleMode, scheduleMode,
hours, countdownHours,
minutes, countdownMinutes,
isActive,
countdownRemaining, countdownRemaining,
isCountdownActive,
inchingHours,
inchingMinutes,
isInchingActive,
schedules, schedules,
selectedDays, selectedDays,
selectedTime, selectedTime,
@ -58,11 +74,14 @@ class WaterHeaterDeviceStatusLoaded extends WaterHeaterState {
WaterHeaterDeviceStatusLoaded copyWith({ WaterHeaterDeviceStatusLoaded copyWith({
WaterHeaterStatusModel? status, WaterHeaterStatusModel? status,
ScheduleModes? scheduleMode, ScheduleModes? scheduleMode,
int? hours, int? countdownHours,
int? minutes, int? countdownMinutes,
bool? isActive,
Duration? countdownRemaining, Duration? countdownRemaining,
List<ScheduleEntry>? schedules, bool? isCountdownActive,
int? inchingHours,
int? inchingMinutes,
bool? isInchingActive,
List<ScheduleModel>? schedules,
List<bool>? selectedDays, List<bool>? selectedDays,
TimeOfDay? selectedTime, TimeOfDay? selectedTime,
bool? functionOn, bool? functionOn,
@ -71,10 +90,13 @@ class WaterHeaterDeviceStatusLoaded extends WaterHeaterState {
return WaterHeaterDeviceStatusLoaded( return WaterHeaterDeviceStatusLoaded(
status ?? this.status, status ?? this.status,
scheduleMode: scheduleMode ?? this.scheduleMode, scheduleMode: scheduleMode ?? this.scheduleMode,
hours: hours ?? this.hours, countdownHours: countdownHours ?? this.countdownHours,
minutes: minutes ?? this.minutes, countdownMinutes: countdownMinutes ?? this.countdownMinutes,
isActive: isActive ?? this.isActive,
countdownRemaining: countdownRemaining ?? this.countdownRemaining, countdownRemaining: countdownRemaining ?? this.countdownRemaining,
isCountdownActive: isCountdownActive ?? this.isCountdownActive,
inchingHours: inchingHours ?? this.inchingHours,
inchingMinutes: inchingMinutes ?? this.inchingMinutes,
isInchingActive: isInchingActive ?? this.isInchingActive,
schedules: schedules ?? this.schedules, schedules: schedules ?? this.schedules,
selectedDays: selectedDays ?? this.selectedDays, selectedDays: selectedDays ?? this.selectedDays,
selectedTime: selectedTime ?? this.selectedTime, selectedTime: selectedTime ?? this.selectedTime,

View File

@ -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<WaterHeaterBloc>();
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<WaterHeaterBloc, WaterHeaterState>(
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<bool> _convertDaysStringToBooleans(List<String> selectedDays) {
final daysOfWeek = ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun'];
List<bool> 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<bool> 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<WaterHeaterBloc>()
.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<bool>(
value: true,
groupValue: isOn,
onChanged: (bool? value) {
context
.read<WaterHeaterBloc>()
.add(const UpdateFunctionOnEvent(true));
},
),
const Text('On'),
const SizedBox(width: 10),
Radio<bool>(
value: false,
groupValue: isOn,
onChanged: (bool? value) {
context
.read<WaterHeaterBloc>()
.add(const UpdateFunctionOnEvent(false));
},
),
const Text('Off'),
],
);
}
}

View File

@ -1,19 +1,19 @@
import 'package:flutter/material.dart'; // import 'package:flutter/material.dart';
class ScheduleEntry { // class ScheduleEntry {
final List<bool> selectedDays; // final List<bool> selectedDays;
final TimeOfDay time; // final TimeOfDay time;
final bool functionOn; // final bool functionOn;
final String category; // final String category;
ScheduleEntry({ // ScheduleEntry({
required this.selectedDays, // required this.selectedDays,
required this.time, // required this.time,
required this.functionOn, // required this.functionOn,
required this.category, // required this.category,
}); // });
@override // @override
String toString() => // String toString() =>
'ScheduleEntry(selectedDays: $selectedDays, time: $time, functionOn: $functionOn)'; // 'ScheduleEntry(selectedDays: $selectedDays, time: $time, functionOn: $functionOn)';
} // }

View File

@ -1,49 +1,27 @@
import 'dart:convert'; import 'dart:convert';
import 'package:flutter/material.dart';
import 'package:flutter/foundation.dart';
import 'package:syncrow_web/pages/device_managment/all_devices/models/device_status.dart'; import 'package:syncrow_web/pages/device_managment/all_devices/models/device_status.dart';
import 'package:flutter/foundation.dart';
/*
{
"category": "kg",
"time": "2024-09-22T10:31:54Z",
"function": {
"code": "switch_1",
"value": true
},
"days": [
"Sun"
]
}
*/
class ScheduleModel { class ScheduleModel {
final String scheduleId;
final String category; final String category;
final String time; final String time;
final Status function; final Status function;
final List<String> days; final List<String> days;
final TimeOfDay? timeOfDay;
final List<bool>? selectedDays;
ScheduleModel({ ScheduleModel({
required this.category, required this.category,
required this.time, required this.time,
required this.function, required this.function,
required this.days, required this.days,
this.timeOfDay,
this.selectedDays,
this.scheduleId = '',
}); });
ScheduleModel copyWith({
String? category,
String? time,
Status? function,
List<String>? days,
}) {
return ScheduleModel(
category: category ?? this.category,
time: time ?? this.time,
function: function ?? this.function,
days: days ?? this.days,
);
}
Map<String, dynamic> toMap() { Map<String, dynamic> toMap() {
return { return {
'category': category, 'category': category,
@ -55,10 +33,15 @@ class ScheduleModel {
factory ScheduleModel.fromMap(Map<String, dynamic> map) { factory ScheduleModel.fromMap(Map<String, dynamic> map) {
return ScheduleModel( return ScheduleModel(
scheduleId: map['scheduleId'] ?? '',
category: map['category'] ?? '', category: map['category'] ?? '',
time: map['time'] ?? '', time: map['time'] ?? '',
function: Status.fromMap(map['function']), function: Status.fromMap(map['function']),
days: List<String>.from(map['days']), days: List<String>.from(map['days']),
timeOfDay:
parseTimeOfDay(map['time']),
selectedDays:
parseSelectedDays(map['days']),
); );
} }
@ -67,9 +50,54 @@ class ScheduleModel {
factory ScheduleModel.fromJson(String source) => factory ScheduleModel.fromJson(String source) =>
ScheduleModel.fromMap(json.decode(source)); ScheduleModel.fromMap(json.decode(source));
ScheduleModel copyWith({
String? category,
String? time,
Status? function,
List<String>? days,
TimeOfDay? timeOfDay,
List<bool>? 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<bool> parseSelectedDays(List<String> days) {
const allDays = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'];
return allDays.map((day) => days.contains(day)).toList();
}
static List<String> convertSelectedDaysToStrings(List<bool> selectedDays) {
const allDays = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'];
List<String> result = [];
for (int i = 0; i < selectedDays.length; i++) {
if (selectedDays[i]) {
result.add(allDays[i]);
}
}
return result;
}
@override @override
String toString() { 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 @override
@ -80,7 +108,9 @@ class ScheduleModel {
other.category == category && other.category == category &&
other.time == time && other.time == time &&
other.function == function && other.function == function &&
listEquals(other.days, days); listEquals(other.days, days) &&
timeOfDay == other.timeOfDay &&
listEquals(other.selectedDays, selectedDays);
} }
@override @override
@ -88,6 +118,8 @@ class ScheduleModel {
return category.hashCode ^ return category.hashCode ^
time.hashCode ^ time.hashCode ^
function.hashCode ^ function.hashCode ^
days.hashCode; days.hashCode ^
timeOfDay.hashCode ^
selectedDays.hashCode;
} }
} }

View File

@ -1,5 +1,6 @@
import 'package:equatable/equatable.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/device_status.dart';
import 'package:syncrow_web/pages/device_managment/water_heater/models/schedule_model.dart';
enum ScheduleModes { countdown, schedule, circulate, inching } enum ScheduleModes { countdown, schedule, circulate, inching }
@ -8,11 +9,14 @@ class WaterHeaterStatusModel extends Equatable {
final bool heaterSwitch; final bool heaterSwitch;
final int countdownHours; final int countdownHours;
final int countdownMinutes; final int countdownMinutes;
final int inchingHours;
final int inchingMinutes;
final ScheduleModes scheduleMode; final ScheduleModes scheduleMode;
final String relayStatus; final String relayStatus;
final String cycleTiming; final String cycleTiming;
final List<ScheduleModel> schedules;
WaterHeaterStatusModel({ const WaterHeaterStatusModel({
required this.uuid, required this.uuid,
required this.heaterSwitch, required this.heaterSwitch,
required this.countdownHours, required this.countdownHours,
@ -20,6 +24,9 @@ class WaterHeaterStatusModel extends Equatable {
required this.relayStatus, required this.relayStatus,
required this.cycleTiming, required this.cycleTiming,
required this.scheduleMode, required this.scheduleMode,
required this.schedules,
this.inchingHours = 0,
this.inchingMinutes = 0,
}); });
factory WaterHeaterStatusModel.fromJson(String id, List<Status> jsonList) { factory WaterHeaterStatusModel.fromJson(String id, List<Status> jsonList) {
@ -60,6 +67,7 @@ class WaterHeaterStatusModel extends Equatable {
relayStatus: relayStatus, relayStatus: relayStatus,
cycleTiming: cycleTiming, cycleTiming: cycleTiming,
scheduleMode: scheduleMode, scheduleMode: scheduleMode,
schedules: const [],
); );
} }
@ -71,6 +79,7 @@ class WaterHeaterStatusModel extends Equatable {
String? relayStatus, String? relayStatus,
String? cycleTiming, String? cycleTiming,
ScheduleModes? scheduleMode, ScheduleModes? scheduleMode,
List<ScheduleModel>? schedules,
}) { }) {
return WaterHeaterStatusModel( return WaterHeaterStatusModel(
uuid: uuid ?? this.uuid, uuid: uuid ?? this.uuid,
@ -80,6 +89,7 @@ class WaterHeaterStatusModel extends Equatable {
relayStatus: relayStatus ?? this.relayStatus, relayStatus: relayStatus ?? this.relayStatus,
cycleTiming: cycleTiming ?? this.cycleTiming, cycleTiming: cycleTiming ?? this.cycleTiming,
scheduleMode: scheduleMode ?? this.scheduleMode, scheduleMode: scheduleMode ?? this.scheduleMode,
schedules: schedules ?? this.schedules,
); );
} }

View File

@ -29,16 +29,12 @@ class WaterHeaterDeviceControl extends StatelessWidget
return const Center(child: CircularProgressIndicator()); return const Center(child: CircularProgressIndicator());
} else if (state is WaterHeaterDeviceStatusLoaded) { } else if (state is WaterHeaterDeviceStatusLoaded) {
return _buildStatusControls(context, state.status); return _buildStatusControls(context, state.status);
} } else if (state is WaterHeaterFailedState ||
// else if (state is WaterHeaterScheduleViewState) {
// final status = context.read<WaterHeaterBloc>().deviceStatus;
// return _buildStatusControls(context, status);
// }
else if (state is WaterHeaterFailedState ||
state is WaterHeaterBatchFailedState) { state is WaterHeaterBatchFailedState) {
return const Center(child: Text('Error fetching status')); return const Center(child: Text('Error fetching status'));
} else { } else {
return const Center(child: CircularProgressIndicator()); return const SizedBox(
height: 200, child: Center(child: SizedBox()));
} }
}, },
)); ));

View File

@ -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<WaterHeaterBloc>()
.add(StopScheduleEvent(deviceId));
context.read<WaterHeaterBloc>().add(
ToggleWaterHeaterEvent(
deviceId: deviceId,
code: 'countdown_1',
value: 0,
),
);
},
backgroundColor: Colors.red,
child: const Text('Stop'),
)
: DefaultButton(
height: 40,
onPressed: () {
context.read<WaterHeaterBloc>().add(
ToggleWaterHeaterEvent(
deviceId: deviceId,
code: 'countdown_1',
value: Duration(hours: hours, minutes: minutes)
.inSeconds,
),
);
},
backgroundColor: ColorsManager.primaryColor,
child: const Text('Save'),
),
),
],
);
}
}

View File

@ -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<WaterHeaterBloc>().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<WaterHeaterBloc>().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<int> 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,
),
),
],
);
}
}

View File

@ -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<WaterHeaterBloc>()
.add(StopScheduleEvent(deviceId));
context.read<WaterHeaterBloc>().add(
ToggleWaterHeaterEvent(
deviceId: deviceId,
code: 'switch_inching',
value: 0,
),
);
},
backgroundColor: Colors.red,
child: const Text('Stop'),
)
: DefaultButton(
height: 40,
onPressed: () {
context.read<WaterHeaterBloc>().add(
ToggleWaterHeaterEvent(
deviceId: deviceId,
code: 'switch_inching',
value: Duration(hours: hours, minutes: minutes)
.inSeconds,
),
);
},
backgroundColor: ColorsManager.primaryColor,
child: const Text('Save'),
),
),
],
);
}
}

View File

@ -1,13 +1,15 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.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/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/pages/device_managment/water_heater/models/water_heater_status_model.dart';
import 'package:syncrow_web/utils/color_manager.dart'; import 'package:syncrow_web/pages/device_managment/water_heater/widgets/count_down_button.dart';
import 'package:syncrow_web/utils/constants/assets.dart'; import 'package:syncrow_web/pages/device_managment/water_heater/widgets/count_down_inching_view.dart';
import 'package:syncrow_web/utils/extension/build_context_x.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 { class BuildScheduleView extends StatefulWidget {
const BuildScheduleView({super.key, required this.status}); const BuildScheduleView({super.key, required this.status});
@ -15,7 +17,7 @@ class BuildScheduleView extends StatefulWidget {
final WaterHeaterStatusModel status; final WaterHeaterStatusModel status;
@override @override
_BuildScheduleViewState createState() => _BuildScheduleViewState(); State<BuildScheduleView> createState() => _BuildScheduleViewState();
} }
class _BuildScheduleViewState extends State<BuildScheduleView> { class _BuildScheduleViewState extends State<BuildScheduleView> {
@ -42,20 +44,50 @@ class _BuildScheduleViewState extends State<BuildScheduleView> {
return Column( return Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
_scheduleHeader(context), const ScheduleHeader(),
const SizedBox(height: 20), const SizedBox(height: 20),
_buildScheduleModeSelector(context, state), ScheduleModeSelector(state: state),
const SizedBox(height: 20), const SizedBox(height: 20),
if (state.scheduleMode == ScheduleModes.schedule) 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 || if (state.scheduleMode == ScheduleModes.countdown ||
state.scheduleMode == ScheduleModes.inching) state.scheduleMode == ScheduleModes.inching)
..._buildCountDownAngInchingView(context, state), CountdownInchingView(state: state),
const SizedBox(height: 20), 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(); return const SizedBox();
}, },
), ),
@ -65,673 +97,4 @@ class _BuildScheduleViewState extends State<BuildScheduleView> {
), ),
); );
} }
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<ScheduleModes>(
value: mode,
groupValue: state.scheduleMode,
onChanged: (ScheduleModes? value) {
if (value != null) {
context.read<WaterHeaterBloc>().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<WaterHeaterBloc>()
.add(DeleteScheduleEvent(index));
},
child: Text(
'Delete',
style: context.textTheme.bodySmall!
.copyWith(color: ColorsManager.blueColor),
),
),
],
),
),
],
);
}
String _getSelectedDays(List<bool> selectedDays) {
final days = ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun'];
List<String> 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<WaterHeaterBloc>();
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<WaterHeaterBloc, WaterHeaterState>(
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<bool> 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<WaterHeaterBloc>()
.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<bool>(
value: true,
groupValue: isOn,
onChanged: (bool? value) {
context
.read<WaterHeaterBloc>()
.add(const UpdateFunctionOnEvent(true));
},
),
const Text('On'),
const SizedBox(width: 10),
Radio<bool>(
value: false,
groupValue: isOn,
onChanged: (bool? value) {
context
.read<WaterHeaterBloc>()
.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<WaterHeaterBloc>()
.add(StopScheduleEvent(widget.status.uuid));
context.read<WaterHeaterBloc>().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<WaterHeaterBloc>().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<Widget> _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<WaterHeaterBloc>().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<WaterHeaterBloc>().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<int> 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,
),
),
],
);
}
} }

View File

@ -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();
},
),
),
],
);
}
}

View File

@ -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),
],
);
}
}

View File

@ -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'),
),
),
],
);
}
}

View File

@ -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<ScheduleModes>(
value: mode,
groupValue: state.scheduleMode,
onChanged: (ScheduleModes? value) {
if (value != null) {
if (value == ScheduleModes.countdown) {
context.read<WaterHeaterBloc>().add(UpdateScheduleEvent(
scheduleMode: value,
hours: state.countdownHours ?? 0,
minutes: state.countdownMinutes ?? 0,
));
} else if (value == ScheduleModes.inching) {
context.read<WaterHeaterBloc>().add(UpdateScheduleEvent(
scheduleMode: value,
hours: state.inchingHours ?? 0,
minutes: state.inchingMinutes ?? 0,
));
}
if (value == ScheduleModes.schedule) {
context.read<WaterHeaterBloc>().add(
GetSchedulesEvent(
category: 'kg',
uuid: state.status.uuid,
),
);
}
}
},
),
),
);
}
}

View File

@ -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<bool> selectedDays) {
final days = ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun'];
List<String> selectedDaysStr = [];
for (int i = 0; i < selectedDays.length; i++) {
if (selectedDays[i]) {
selectedDaysStr.add(days[i]);
}
}
return selectedDaysStr.join(', ');
}
}

View File

@ -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<WaterHeaterBloc, WaterHeaterState>(
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<WaterHeaterBloc>().add(DeleteScheduleEvent(
index: index,
scheduleId: schedule.scheduleId,
));
},
child: Text(
'Delete',
style: context.textTheme.bodySmall!
.copyWith(color: ColorsManager.blueColor),
),
),
],
),
),
],
);
}
String _getSelectedDays(List<bool> selectedDays) {
final days = ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun'];
List<String> selectedDaysStr = [];
for (int i = 0; i < selectedDays.length; i++) {
if (selectedDays[i]) {
selectedDaysStr.add(days[i]);
}
}
return selectedDaysStr.join(', ');
}
}

View File

@ -195,6 +195,29 @@ class DevicesManagementApi {
} }
} }
Future<List<ScheduleModel>> 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<ScheduleModel> schedules = [];
for (var schedule in json['schedules']) {
schedules.add(ScheduleModel.fromJson(schedule));
}
return schedules;
},
);
return response;
} catch (e) {
debugPrint('Error fetching $e');
return [];
}
}
Future<bool> updateScheduleRecord( Future<bool> updateScheduleRecord(
{required bool enable, {required bool enable,
required String uuid, required String uuid,

View File

@ -40,6 +40,8 @@ abstract class ApiEndpoints {
'/device/report-logs/{uuid}?code={code}&startTime={startTime}&endTime={endTime}'; '/device/report-logs/{uuid}?code={code}&startTime={startTime}&endTime={endTime}';
static const String scheduleByDeviceId = '/schedule/{deviceUuid}'; static const String scheduleByDeviceId = '/schedule/{deviceUuid}';
static const String getScheduleByDeviceId =
'/schedule/{deviceUuid}?category={category}';
static const String deleteScheduleByDeviceId = static const String deleteScheduleByDeviceId =
'/schedule/{deviceUuid}/{scheduleUuid}'; '/schedule/{deviceUuid}/{scheduleUuid}';
static const String updateScheduleByDeviceId = static const String updateScheduleByDeviceId =

View File

@ -24,3 +24,8 @@ String formatTimeOfDayToISO(TimeOfDay time, {DateTime? currentDate}) {
return dateTime.toUtc().toIso8601String(); return dateTime.toUtc().toIso8601String();
} }
String formatIsoStringToTime(String isoString) {
final dateTime = DateTime.parse(isoString);
return DateFormat('hh:mm a').format(dateTime);
}