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: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<WaterHeaterEvent, WaterHeaterState> {
on<UpdateScheduleEvent>(_updateScheduleEvent);
on<StopScheduleEvent>(_stopScheduleEvent);
on<DecrementCountdownEvent>(_onDecrementCountdown);
on<AddScheduleEvent>(_onAddSchedule);
on<DeleteScheduleEvent>(_onDeleteSchedule);
on<UpdateScheduleEntryEvent>(_onUpdateSchedule);
on<InitializeAddScheduleEvent>(_initializeAddSchedule);
on<UpdateSelectedTimeEvent>(_updateSelectedTime);
on<UpdateSelectedDayEvent>(_updateSelectedDay);
on<UpdateFunctionOnEvent>(_updateFunctionOn);
on<GetSchedulesEvent>(_getSchedule);
on<AddScheduleEvent>(_onAddSchedule);
on<DeleteScheduleEvent>(_onDeleteSchedule);
on<UpdateScheduleEntryEvent>(_onUpdateSchedule);
}
late WaterHeaterStatusModel deviceStatus;
Timer? _countdownTimer;
// Timer? _inchingTimer;
FutureOr<void> _initializeAddSchedule(
InitializeAddScheduleEvent event,
@ -87,22 +89,37 @@ class WaterHeaterBloc extends Bloc<WaterHeaterEvent, WaterHeaterState> {
) 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<WaterHeaterEvent, WaterHeaterState> {
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<WaterHeaterEvent, WaterHeaterState> {
) 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<WaterHeaterEvent, WaterHeaterState> {
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<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(
DecrementCountdownEvent event,
Emitter<WaterHeaterState> emit,
@ -253,9 +341,9 @@ class WaterHeaterBloc extends Bloc<WaterHeaterEvent, WaterHeaterState> {
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<WaterHeaterEvent, WaterHeaterState> {
int newMinutes = (totalSeconds % 3600) ~/ 60;
emit(currentState.copyWith(
hours: newHours,
minutes: newMinutes,
countdownHours: newHours,
countdownMinutes: newMinutes,
countdownRemaining: newRemaining,
));
}
}
}
void _startCountdown(
Emitter<WaterHeaterState> emit, Duration countdownRemaining) {
_countdownTimer?.cancel();
// FutureOr<void> _onDecrementInching(
// DecrementInchingEvent event,
// Emitter<WaterHeaterState> 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<bool> _runDebounce({
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(
AddScheduleEvent event,
Emitter<WaterHeaterState> emit,
@ -360,59 +500,30 @@ class WaterHeaterBloc extends Bloc<WaterHeaterEvent, WaterHeaterState> {
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<ScheduleEntry>.from(currentState.schedules)..add(newSchedule);
List<ScheduleModel>.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<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(
UpdateScheduleEntryEvent event,
Emitter<WaterHeaterState> emit,
@ -420,44 +531,53 @@ class WaterHeaterBloc extends Bloc<WaterHeaterEvent, WaterHeaterState> {
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<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 {
emit(const WaterHeaterFailedState(
error: 'Failed to update schedule. Please try again.'));
emit(currentState);
// emit(const WaterHeaterFailedState(error: 'Failed to update schedule.'));
}
}
}
@override
Future<void> close() {
_countdownTimer?.cancel();
return super.close();
FutureOr<void> _onDeleteSchedule(
DeleteScheduleEvent event,
Emitter<WaterHeaterState> 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<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 int index;
final String scheduleId;
const DeleteScheduleEvent(this.index);
const DeleteScheduleEvent({required this.index, required this.scheduleId});
@override
List<Object?> get props => [index];
List<Object?> 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<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 {

View File

@ -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<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 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<Object?> 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<ScheduleEntry>? schedules,
bool? isCountdownActive,
int? inchingHours,
int? inchingMinutes,
bool? isInchingActive,
List<ScheduleModel>? schedules,
List<bool>? 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,

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 {
final List<bool> selectedDays;
final TimeOfDay time;
final bool functionOn;
final String category;
// class ScheduleEntry {
// final List<bool> 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)';
}
// @override
// String toString() =>
// 'ScheduleEntry(selectedDays: $selectedDays, time: $time, functionOn: $functionOn)';
// }

View File

@ -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<String> days;
final TimeOfDay? timeOfDay;
final List<bool>? 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<String>? days,
}) {
return ScheduleModel(
category: category ?? this.category,
time: time ?? this.time,
function: function ?? this.function,
days: days ?? this.days,
);
}
Map<String, dynamic> toMap() {
return {
'category': category,
@ -55,10 +33,15 @@ class ScheduleModel {
factory ScheduleModel.fromMap(Map<String, dynamic> map) {
return ScheduleModel(
scheduleId: map['scheduleId'] ?? '',
category: map['category'] ?? '',
time: map['time'] ?? '',
function: Status.fromMap(map['function']),
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) =>
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
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;
}
}

View File

@ -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<ScheduleModel> 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<Status> 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<ScheduleModel>? 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,
);
}

View File

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

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_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<BuildScheduleView> createState() => _BuildScheduleViewState();
}
class _BuildScheduleViewState extends State<BuildScheduleView> {
@ -42,20 +44,50 @@ class _BuildScheduleViewState extends State<BuildScheduleView> {
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<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(
{required bool enable,
required String uuid,

View File

@ -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 =

View File

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