Compare commits

..

15 Commits

Author SHA1 Message Date
3c98365338 change the validation from static code to backend 2025-06-03 10:44:34 +03:00
f07dbad1ea Merge pull request #220 from SyncrowIOT/SP-1664-FE-Sider-bar-tree-behavior-issues-on-Analytics-page
Sp 1664 fe sider bar tree behavior issues on analytics page
2025-06-01 16:45:19 +03:00
87df8e4091 Merge pull request #222 from SyncrowIOT/SP-1389-FE-On-Login-page-Email-field-is-case-sensitive-it-should-not-be
Normalize email to lowercase when logging in
2025-06-01 16:38:53 +03:00
2d68fc23a3 Normalize email to lowercase when logging in 2025-06-01 16:21:22 +03:00
15ea1b4c5a Merge pull request #221 from SyncrowIOT/enable-hot-reload
enable hot reload on web.
2025-06-01 16:00:46 +03:00
17f6985dbf enable hot reload on web. 2025-06-01 15:59:29 +03:00
d1ddf75a42 Merge pull request #219 from SyncrowIOT/SP-1607-FE-Adjust-Padding-Between-Comparison-Signs-for-Visual-Consistency
Sp 1607 fe adjust padding between comparison signs for visual consistency
2025-06-01 15:50:53 +03:00
393a5361f0 Apply correct business logic in AirQualityDataLoadingStrategy. 2025-06-01 15:40:12 +03:00
a56e93d0d7 removed the interface method onSelectChildSpace, because all the clients dont use it and instead pass the onSpaceSelected, which isn't a good design. 2025-06-01 15:38:14 +03:00
94847fa936 SP-1664-Fe-Sider-bar-tree-behavior-issues-on-Analytics-page. 2025-06-01 15:36:52 +03:00
78f42dacf6 Adjust ConditionToggle widget dimensions and colors for improved UI consistency 2025-06-01 14:37:42 +03:00
fdabfe5d95 Merge pull request #217 from SyncrowIOT/SP-1584-FE-Block-Energy-Device-from-Being-Added-to-Then-Section-with-Validation-Message
Refactor energy clamp dialog to handle empty functions list gracefully
2025-06-01 14:13:53 +03:00
8916000696 Refactor visibility logic in Energy Clamp Dialog to handle empty functions list more elegantly 2025-06-01 14:11:21 +03:00
305d695358 Refactor energy clamp dialog to handle empty functions list gracefully 2025-06-01 13:12:58 +03:00
0a9d53e5bd Refactor ConditionToggle widget to display icons with corresponding conditions 2025-05-29 10:48:12 +03:00
42 changed files with 564 additions and 1241 deletions

3
.vscode/launch.json vendored
View File

@ -16,6 +16,7 @@
"3000",
"-t",
"lib/main_dev.dart",
"--web-experimental-hot-reload",
],
"flutterMode": "debug"
@ -35,6 +36,7 @@
"3000",
"-t",
"lib/main_staging.dart",
"--web-experimental-hot-reload",
],
"flutterMode": "debug"
@ -54,6 +56,7 @@
"3000",
"-t",
"lib/main.dart",
"--web-experimental-hot-reload",
],
"flutterMode": "debug"

View File

@ -1,57 +0,0 @@
import 'package:equatable/equatable.dart';
import 'package:flutter/material.dart';
import 'package:syncrow_web/utils/color_manager.dart';
class AirQualityDataModel extends Equatable {
const AirQualityDataModel({
required this.date,
required this.data,
});
final DateTime date;
final List<AirQualityPercentageData> data;
factory AirQualityDataModel.fromJson(Map<String, dynamic> json) {
return AirQualityDataModel(
date: DateTime.parse(json['date'] as String),
data: (json['data'] as List<dynamic>)
.map((e) => AirQualityPercentageData.fromJson(e as Map<String, dynamic>))
.toList(),
);
}
static final Map<String, Color> metricColors = {
'good': ColorsManager.goodGreen.withValues(alpha: 0.7),
'moderate': ColorsManager.moderateYellow.withValues(alpha: 0.7),
'poor': ColorsManager.poorOrange.withValues(alpha: 0.7),
'unhealthy': ColorsManager.unhealthyRed.withValues(alpha: 0.7),
'severe': ColorsManager.severePink.withValues(alpha: 0.7),
'hazardous': ColorsManager.hazardousPurple.withValues(alpha: 0.7),
};
@override
List<Object?> get props => [date, data];
}
class AirQualityPercentageData extends Equatable {
const AirQualityPercentageData({
required this.type,
required this.name,
required this.percentage,
});
final String type;
final String name;
final double percentage;
factory AirQualityPercentageData.fromJson(Map<String, dynamic> json) {
return AirQualityPercentageData(
type: json['type'] as String? ?? '',
name: json['name'] as String? ?? '',
percentage: (json['percentage'] as num?)?.toDouble() ?? 0,
);
}
@override
List<Object?> get props => [type, name, percentage];
}

View File

@ -1,49 +1,18 @@
import 'package:equatable/equatable.dart';
class RangeOfAqi extends Equatable {
final double min;
final double avg;
final double max;
final DateTime date;
final List<RangeOfAqiValue> data;
const RangeOfAqi({
required this.data,
required this.min,
required this.avg,
required this.max,
required this.date,
});
factory RangeOfAqi.fromJson(Map<String, dynamic> json) {
return RangeOfAqi(
date: DateTime.parse(json['date'] as String),
data: (json['data'] as List<dynamic>)
.map((e) => RangeOfAqiValue.fromJson(e as Map<String, dynamic>))
.toList(),
);
}
@override
List<Object?> get props => [data, date];
}
class RangeOfAqiValue extends Equatable {
final String type;
final double min;
final double average;
final double max;
const RangeOfAqiValue({
required this.type,
required this.min,
required this.average,
required this.max,
});
factory RangeOfAqiValue.fromJson(Map<String, dynamic> json) {
return RangeOfAqiValue(
type: json['type'] as String,
min: (json['min'] as num).toDouble(),
average: (json['average'] as num).toDouble(),
max: (json['max'] as num).toDouble(),
);
}
@override
List<Object?> get props => [type, min, average, max];
List<Object?> get props => [min, avg, max, date];
}

View File

@ -1,81 +0,0 @@
import 'package:bloc/bloc.dart';
import 'package:equatable/equatable.dart';
import 'package:syncrow_web/pages/analytics/models/air_quality_data_model.dart';
import 'package:syncrow_web/pages/analytics/modules/air_quality/widgets/aqi_type_dropdown.dart';
import 'package:syncrow_web/pages/analytics/params/get_air_quality_distribution_param.dart';
import 'package:syncrow_web/pages/analytics/services/air_quality_distribution/air_quality_distribution_service.dart';
part 'air_quality_distribution_event.dart';
part 'air_quality_distribution_state.dart';
class AirQualityDistributionBloc
extends Bloc<AirQualityDistributionEvent, AirQualityDistributionState> {
final AirQualityDistributionService _aqiDistributionService;
AirQualityDistributionBloc(
this._aqiDistributionService,
) : super(const AirQualityDistributionState()) {
on<LoadAirQualityDistribution>(_onLoadAirQualityDistribution);
on<ClearAirQualityDistribution>(_onClearAirQualityDistribution);
on<UpdateAqiTypeEvent>(_onUpdateAqiTypeEvent);
}
Future<void> _onLoadAirQualityDistribution(
LoadAirQualityDistribution event,
Emitter<AirQualityDistributionState> emit,
) async {
try {
emit(state.copyWith(status: AirQualityDistributionStatus.loading));
final result = await _aqiDistributionService.getAirQualityDistribution(
event.param,
);
emit(
state.copyWith(
status: AirQualityDistributionStatus.success,
chartData: result,
filteredChartData: _arrangeChartDataByType(result, state.selectedAqiType),
),
);
} catch (e) {
emit(
AirQualityDistributionState(
status: AirQualityDistributionStatus.failure,
errorMessage: e.toString(),
selectedAqiType: state.selectedAqiType,
),
);
}
}
Future<void> _onClearAirQualityDistribution(
ClearAirQualityDistribution event,
Emitter<AirQualityDistributionState> emit,
) async {
emit(const AirQualityDistributionState());
}
void _onUpdateAqiTypeEvent(
UpdateAqiTypeEvent event,
Emitter<AirQualityDistributionState> emit,
) {
emit(
state.copyWith(
selectedAqiType: event.aqiType,
filteredChartData: _arrangeChartDataByType(state.chartData, event.aqiType),
),
);
}
List<AirQualityDataModel> _arrangeChartDataByType(
List<AirQualityDataModel> data,
AqiType aqiType,
) {
final filteredData = data.map(
(data) => AirQualityDataModel(
date: data.date,
data: data.data.where((value) => value.type == aqiType.code).toList(),
),
);
return filteredData.toList();
}
}

View File

@ -1,30 +0,0 @@
part of 'air_quality_distribution_bloc.dart';
sealed class AirQualityDistributionEvent extends Equatable {
const AirQualityDistributionEvent();
@override
List<Object> get props => [];
}
final class LoadAirQualityDistribution extends AirQualityDistributionEvent {
final GetAirQualityDistributionParam param;
const LoadAirQualityDistribution(this.param);
@override
List<Object> get props => [param];
}
final class UpdateAqiTypeEvent extends AirQualityDistributionEvent {
const UpdateAqiTypeEvent(this.aqiType);
final AqiType aqiType;
@override
List<Object> get props => [aqiType];
}
final class ClearAirQualityDistribution extends AirQualityDistributionEvent {
const ClearAirQualityDistribution();
}

View File

@ -1,43 +0,0 @@
part of 'air_quality_distribution_bloc.dart';
enum AirQualityDistributionStatus {
initial,
loading,
success,
failure,
}
class AirQualityDistributionState extends Equatable {
const AirQualityDistributionState({
this.status = AirQualityDistributionStatus.initial,
this.chartData = const [],
this.filteredChartData = const [],
this.errorMessage,
this.selectedAqiType = AqiType.aqi,
});
final AirQualityDistributionStatus status;
final List<AirQualityDataModel> chartData;
final List<AirQualityDataModel> filteredChartData;
final String? errorMessage;
final AqiType selectedAqiType;
AirQualityDistributionState copyWith({
AirQualityDistributionStatus? status,
List<AirQualityDataModel>? chartData,
List<AirQualityDataModel>? filteredChartData,
String? errorMessage,
AqiType? selectedAqiType,
}) {
return AirQualityDistributionState(
status: status ?? this.status,
chartData: chartData ?? this.chartData,
filteredChartData: filteredChartData ?? this.filteredChartData,
errorMessage: errorMessage ?? this.errorMessage,
selectedAqiType: selectedAqiType ?? this.selectedAqiType,
);
}
@override
List<Object?> get props => [status, chartData, errorMessage, selectedAqiType];
}

View File

@ -1,7 +1,6 @@
import 'package:bloc/bloc.dart';
import 'package:equatable/equatable.dart';
import 'package:syncrow_web/pages/analytics/models/range_of_aqi.dart';
import 'package:syncrow_web/pages/analytics/modules/air_quality/widgets/aqi_type_dropdown.dart';
import 'package:syncrow_web/pages/analytics/params/get_range_of_aqi_param.dart';
import 'package:syncrow_web/pages/analytics/services/range_of_aqi/range_of_aqi_service.dart';
@ -12,7 +11,6 @@ class RangeOfAqiBloc extends Bloc<RangeOfAqiEvent, RangeOfAqiState> {
RangeOfAqiBloc(this._rangeOfAqiService) : super(const RangeOfAqiState()) {
on<LoadRangeOfAqiEvent>(_onLoadRangeOfAqiEvent);
on<ClearRangeOfAqiEvent>(_onClearRangeOfAqiEvent);
on<UpdateAqiTypeEvent>(_onUpdateAqiTypeEvent);
}
final RangeOfAqiService _rangeOfAqiService;
@ -22,55 +20,19 @@ class RangeOfAqiBloc extends Bloc<RangeOfAqiEvent, RangeOfAqiState> {
Emitter<RangeOfAqiState> emit,
) async {
emit(
state.copyWith(status: RangeOfAqiStatus.loading),
RangeOfAqiState(
status: RangeOfAqiStatus.loading,
rangeOfAqi: state.rangeOfAqi,
),
);
try {
final rangeOfAqi = await _rangeOfAqiService.load(event.param);
emit(
state.copyWith(
status: RangeOfAqiStatus.loaded,
rangeOfAqi: rangeOfAqi,
filteredRangeOfAqi: _arrangeChartDataByType(
rangeOfAqi,
state.selectedAqiType,
),
),
);
emit(RangeOfAqiState(status: RangeOfAqiStatus.loaded, rangeOfAqi: rangeOfAqi));
} catch (e) {
emit(
state.copyWith(
status: RangeOfAqiStatus.failure,
errorMessage: '$e',
),
);
emit(RangeOfAqiState(status: RangeOfAqiStatus.failure, errorMessage: '$e'));
}
}
void _onUpdateAqiTypeEvent(
UpdateAqiTypeEvent event,
Emitter<RangeOfAqiState> emit,
) {
emit(
state.copyWith(
selectedAqiType: event.aqiType,
filteredRangeOfAqi: _arrangeChartDataByType(state.rangeOfAqi, event.aqiType),
),
);
}
List<RangeOfAqi> _arrangeChartDataByType(
List<RangeOfAqi> rangeOfAqi,
AqiType aqiType,
) {
final filteredRangeOfAqi = rangeOfAqi.map(
(data) => RangeOfAqi(
date: data.date,
data: data.data.where((value) => value.type == aqiType.code).toList(),
),
);
return filteredRangeOfAqi.toList();
}
void _onClearRangeOfAqiEvent(
ClearRangeOfAqiEvent event,
Emitter<RangeOfAqiState> emit,

View File

@ -16,15 +16,6 @@ class LoadRangeOfAqiEvent extends RangeOfAqiEvent {
List<Object> get props => [param];
}
class UpdateAqiTypeEvent extends RangeOfAqiEvent {
const UpdateAqiTypeEvent(this.aqiType);
final AqiType aqiType;
@override
List<Object> get props => [aqiType];
}
class ClearRangeOfAqiEvent extends RangeOfAqiEvent {
const ClearRangeOfAqiEvent();
}

View File

@ -5,35 +5,14 @@ enum RangeOfAqiStatus { initial, loading, loaded, failure }
final class RangeOfAqiState extends Equatable {
const RangeOfAqiState({
this.rangeOfAqi = const [],
this.filteredRangeOfAqi = const [],
this.status = RangeOfAqiStatus.initial,
this.errorMessage,
this.selectedAqiType = AqiType.aqi,
});
final RangeOfAqiStatus status;
final List<RangeOfAqi> rangeOfAqi;
final List<RangeOfAqi> filteredRangeOfAqi;
final String? errorMessage;
final AqiType selectedAqiType;
RangeOfAqiState copyWith({
RangeOfAqiStatus? status,
List<RangeOfAqi>? rangeOfAqi,
List<RangeOfAqi>? filteredRangeOfAqi,
String? errorMessage,
AqiType? selectedAqiType,
}) {
return RangeOfAqiState(
status: status ?? this.status,
rangeOfAqi: rangeOfAqi ?? this.rangeOfAqi,
filteredRangeOfAqi: filteredRangeOfAqi ?? this.filteredRangeOfAqi,
errorMessage: errorMessage ?? this.errorMessage,
selectedAqiType: selectedAqiType ?? this.selectedAqiType,
);
}
@override
List<Object?> get props =>
[status, rangeOfAqi, filteredRangeOfAqi, errorMessage, selectedAqiType];
List<Object?> get props => [status, rangeOfAqi, errorMessage];
}

View File

@ -1,11 +1,10 @@
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:syncrow_web/pages/analytics/modules/air_quality/blocs/air_quality_distribution/air_quality_distribution_bloc.dart';
import 'package:syncrow_web/pages/analytics/modules/air_quality/blocs/range_of_aqi/range_of_aqi_bloc.dart';
import 'package:syncrow_web/pages/analytics/modules/air_quality/widgets/aqi_type_dropdown.dart';
import 'package:syncrow_web/pages/analytics/modules/analytics/blocs/analytics_date_picker_bloc/analytics_date_picker_bloc.dart';
import 'package:syncrow_web/pages/analytics/modules/analytics/blocs/analytics_devices/analytics_devices_bloc.dart';
import 'package:syncrow_web/pages/analytics/modules/energy_management/blocs/realtime_device_changes/realtime_device_changes_bloc.dart';
import 'package:syncrow_web/pages/analytics/params/get_air_quality_distribution_param.dart';
import 'package:syncrow_web/pages/analytics/params/get_analytics_devices_param.dart';
import 'package:syncrow_web/pages/analytics/params/get_range_of_aqi_param.dart';
@ -14,10 +13,8 @@ abstract final class FetchAirQualityDataHelper {
static void loadAirQualityData(
BuildContext context, {
required DateTime date,
required String communityUuid,
required String spaceUuid,
bool shouldFetchAnalyticsDevices = true,
}) {
final date = context.read<AnalyticsDatePickerBloc>().state.monthlyDate;
loadAnalyticsDevices(
@ -29,11 +26,7 @@ abstract final class FetchAirQualityDataHelper {
context,
spaceUuid: spaceUuid,
date: date,
);
loadAirQualityDistribution(
context,
spaceUuid: spaceUuid,
date: date,
aqiType: AqiType.aqi,
);
}
@ -44,9 +37,7 @@ abstract final class FetchAirQualityDataHelper {
context.read<RealtimeDeviceChangesBloc>().add(
const RealtimeDeviceChangesClosed(),
);
context.read<AirQualityDistributionBloc>().add(
const ClearAirQualityDistribution(),
);
context.read<RangeOfAqiBloc>().add(const ClearRangeOfAqiEvent());
}
@ -76,26 +67,16 @@ abstract final class FetchAirQualityDataHelper {
BuildContext context, {
required String spaceUuid,
required DateTime date,
required AqiType aqiType,
}) {
context.read<RangeOfAqiBloc>().add(
LoadRangeOfAqiEvent(
GetRangeOfAqiParam(
date: date,
spaceUuid: spaceUuid,
aqiType: aqiType,
),
),
);
}
static void loadAirQualityDistribution(
BuildContext context, {
required String spaceUuid,
required DateTime date,
}) {
context.read<AirQualityDistributionBloc>().add(
LoadAirQualityDistribution(
GetAirQualityDistributionParam(spaceUuid: spaceUuid, date: date),
),
);
}
}

View File

@ -1,6 +1,5 @@
import 'package:flutter/material.dart';
import 'package:syncrow_web/pages/analytics/modules/air_quality/widgets/air_quality_end_side_widget.dart';
import 'package:syncrow_web/pages/analytics/modules/air_quality/widgets/aqi_distribution_chart_box.dart';
import 'package:syncrow_web/pages/analytics/modules/air_quality/widgets/range_of_aqi_chart_box.dart';
class AirQualityView extends StatelessWidget {
@ -24,14 +23,8 @@ class AirQualityView extends StatelessWidget {
height: height * 1.2,
child: const AirQualityEndSideWidget(),
),
SizedBox(
height: height * 0.5,
child: const RangeOfAqiChartBox(),
),
SizedBox(
height: height * 0.5,
child: const AqiDistributionChartBox(),
),
SizedBox(height: height * 0.5, child: const RangeOfAqiChartBox()),
SizedBox(height: height * 0.5, child: const Placeholder()),
],
),
);
@ -53,7 +46,7 @@ class AirQualityView extends StatelessWidget {
spacing: 20,
children: [
Expanded(child: RangeOfAqiChartBox()),
Expanded(child: AqiDistributionChartBox()),
Expanded(child: Placeholder()),
],
),
),

View File

@ -1,174 +0,0 @@
import 'package:fl_chart/fl_chart.dart';
import 'package:flutter/material.dart';
import 'package:intl/intl.dart';
import 'package:syncrow_web/pages/analytics/models/air_quality_data_model.dart';
import 'package:syncrow_web/pages/analytics/modules/energy_management/helpers/energy_management_charts_helper.dart';
import 'package:syncrow_web/utils/color_manager.dart';
import 'package:syncrow_web/utils/extension/build_context_x.dart';
class AqiDistributionChart extends StatelessWidget {
const AqiDistributionChart({super.key, required this.chartData});
final List<AirQualityDataModel> chartData;
static const _rodStackItemsSpacing = 0.4;
static const _barWidth = 13.0;
static final _barBorderRadius = BorderRadius.circular(22);
@override
Widget build(BuildContext context) {
final sortedData = List<AirQualityDataModel>.from(chartData)
..sort(
(a, b) => a.date.compareTo(b.date),
);
return BarChart(
BarChartData(
maxY: 100.1,
gridData: EnergyManagementChartsHelper.gridData(
horizontalInterval: 20,
),
borderData: EnergyManagementChartsHelper.borderData(),
barTouchData: _barTouchData(context),
titlesData: _titlesData(context),
barGroups: _buildBarGroups(sortedData),
),
duration: Duration.zero,
);
}
List<BarChartGroupData> _buildBarGroups(List<AirQualityDataModel> sortedData) {
return List.generate(sortedData.length, (index) {
final data = sortedData[index];
final stackItems = <BarChartRodData>[];
double currentY = 0;
bool isFirstElement = true;
// Sort data by type to ensure consistent order
final sortedPercentageData = List<AirQualityPercentageData>.from(data.data)
..sort((a, b) => a.type.compareTo(b.type));
for (final percentageData in sortedPercentageData) {
stackItems.add(
BarChartRodData(
fromY: currentY,
toY: currentY + percentageData.percentage ,
color: AirQualityDataModel.metricColors[percentageData.name]!,
borderRadius: isFirstElement
? const BorderRadius.only(
topLeft: Radius.circular(22),
topRight: Radius.circular(22),
)
: _barBorderRadius,
width: _barWidth,
),
);
currentY += percentageData.percentage + _rodStackItemsSpacing;
isFirstElement = false;
}
return BarChartGroupData(
x: index,
barRods: stackItems,
groupVertically: true,
);
});
}
BarTouchData _barTouchData(BuildContext context) {
return BarTouchData(
touchTooltipData: BarTouchTooltipData(
getTooltipColor: (_) => ColorsManager.whiteColors,
tooltipBorder: const BorderSide(
color: ColorsManager.semiTransparentBlack,
),
tooltipRoundedRadius: 16,
tooltipPadding: const EdgeInsets.all(8),
getTooltipItem: (group, groupIndex, rod, rodIndex) {
final data = chartData[group.x.toInt()];
final List<TextSpan> children = [];
final textStyle = context.textTheme.bodySmall?.copyWith(
color: ColorsManager.blackColor,
fontSize: 12,
);
// Sort data by type to ensure consistent order
final sortedPercentageData = List<AirQualityPercentageData>.from(data.data)
..sort((a, b) => a.type.compareTo(b.type));
for (final percentageData in sortedPercentageData) {
children.add(TextSpan(
text:
'\n${percentageData.type.toUpperCase()}: ${percentageData.percentage.toStringAsFixed(1)}%',
style: textStyle,
));
}
return BarTooltipItem(
DateFormat('dd/MM/yyyy').format(data.date),
context.textTheme.bodyMedium!.copyWith(
color: ColorsManager.blackColor,
fontSize: 16,
fontWeight: FontWeight.w600,
),
children: children,
);
},
),
);
}
FlTitlesData _titlesData(BuildContext context) {
final titlesData = EnergyManagementChartsHelper.titlesData(
context,
leftTitlesInterval: 20,
);
final leftTitles = titlesData.leftTitles.copyWith(
sideTitles: titlesData.leftTitles.sideTitles.copyWith(
reservedSize: 70,
interval: 20,
maxIncluded: false,
minIncluded: true,
getTitlesWidget: (value, meta) => Padding(
padding: const EdgeInsetsDirectional.only(end: 12),
child: FittedBox(
alignment: AlignmentDirectional.centerStart,
fit: BoxFit.scaleDown,
child: Text(
'${value.toStringAsFixed(0)}%',
style: context.textTheme.bodySmall?.copyWith(
fontSize: 12,
color: ColorsManager.lightGreyColor,
),
),
),
),
),
);
final bottomTitles = AxisTitles(
sideTitles: SideTitles(
showTitles: true,
getTitlesWidget: (value, _) => FittedBox(
alignment: AlignmentDirectional.bottomCenter,
fit: BoxFit.scaleDown,
child: Text(
chartData[value.toInt()].date.day.toString(),
style: context.textTheme.bodySmall?.copyWith(
color: ColorsManager.lightGreyColor,
fontSize: 8,
),
),
),
reservedSize: 36,
),
);
return titlesData.copyWith(
leftTitles: leftTitles,
bottomTitles: bottomTitles,
);
}
}

View File

@ -1,44 +0,0 @@
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:syncrow_web/pages/analytics/modules/air_quality/blocs/air_quality_distribution/air_quality_distribution_bloc.dart';
import 'package:syncrow_web/pages/analytics/modules/air_quality/widgets/aqi_distribution_chart.dart';
import 'package:syncrow_web/pages/analytics/modules/air_quality/widgets/aqi_distribution_chart_title.dart';
import 'package:syncrow_web/pages/analytics/widgets/analytics_error_widget.dart';
import 'package:syncrow_web/utils/style.dart';
class AqiDistributionChartBox extends StatelessWidget {
const AqiDistributionChartBox({super.key});
@override
Widget build(BuildContext context) {
return BlocBuilder<AirQualityDistributionBloc, AirQualityDistributionState>(
builder: (context, state) {
return Container(
padding: const EdgeInsetsDirectional.all(30),
decoration: subSectionContainerDecoration.copyWith(
borderRadius: BorderRadius.circular(30),
),
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
if (state.errorMessage != null) ...[
AnalyticsErrorWidget(state.errorMessage),
const SizedBox(height: 10),
],
AqiDistributionChartTitle(
isLoading: state.status == AirQualityDistributionStatus.loading,
),
const SizedBox(height: 10),
const Divider(),
const SizedBox(height: 20),
Expanded(
child: AqiDistributionChart(chartData: state.filteredChartData),
),
],
),
);
},
);
}
}

View File

@ -1,44 +0,0 @@
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:syncrow_web/pages/analytics/modules/air_quality/blocs/air_quality_distribution/air_quality_distribution_bloc.dart';
import 'package:syncrow_web/pages/analytics/modules/air_quality/widgets/aqi_type_dropdown.dart';
import 'package:syncrow_web/pages/analytics/modules/energy_management/widgets/chart_title.dart';
import 'package:syncrow_web/pages/analytics/widgets/charts_loading_widget.dart';
class AqiDistributionChartTitle extends StatelessWidget {
const AqiDistributionChartTitle({required this.isLoading, super.key});
final bool isLoading;
@override
Widget build(BuildContext context) {
return Row(
children: [
ChartsLoadingWidget(isLoading: isLoading),
const Expanded(
flex: 3,
child: FittedBox(
fit: BoxFit.scaleDown,
alignment: AlignmentDirectional.centerStart,
child: ChartTitle(
title: Text('Distribution over Air Quality Index'),
),
),
),
FittedBox(
alignment: AlignmentDirectional.centerEnd,
fit: BoxFit.scaleDown,
child: AqiTypeDropdown(
onChanged: (value) {
if (value != null) {
context
.read<AirQualityDistributionBloc>()
.add(UpdateAqiTypeEvent(value));
}
},
),
),
],
);
}
}

View File

@ -3,18 +3,17 @@ import 'package:syncrow_web/utils/color_manager.dart';
import 'package:syncrow_web/utils/extension/build_context_x.dart';
enum AqiType {
aqi('AQI', '', 'aqi'),
pm25('PM2.5', 'µg/m³', 'pm25'),
pm10('PM10', 'µg/m³', 'pm10'),
hcho('HCHO', 'mg/m³', 'hcho'),
tvoc('TVOC', 'µg/m³', 'tvoc'),
co2('CO2', 'ppm', 'co2');
aqi('AQI', ''),
pm25('PM2.5', 'µg/m³'),
pm10('PM10', 'µg/m³'),
hcho('HCHO', 'mg/m³'),
tvoc('TVOC', 'µg/m³'),
co2('CO2', 'ppm');
const AqiType(this.value, this.unit, this.code);
const AqiType(this.value, this.unit);
final String value;
final String unit;
final String code;
}
class AqiTypeDropdown extends StatefulWidget {

View File

@ -13,37 +13,23 @@ class RangeOfAqiChart extends StatelessWidget {
required this.chartData,
});
List<(List<double> values, Color color, Color? dotColor)> get _lines {
final sortedData = List<RangeOfAqi>.from(chartData)
..sort((a, b) => a.date.compareTo(b.date));
return [
List<(List<double> values, Color color, Color? dotColor)> get _lines => [
(
sortedData.map((e) {
final value = e.data.firstOrNull;
return value?.max ?? 0;
}).toList(),
chartData.map((e) => e.max).toList(),
ColorsManager.maxPurple,
ColorsManager.maxPurpleDot,
),
(
sortedData.map((e) {
final value = e.data.firstOrNull;
return value?.average ?? 0;
}).toList(),
chartData.map((e) => e.avg).toList(),
Colors.white,
null,
),
(
sortedData.map((e) {
final value = e.data.firstOrNull;
return value?.min ?? 0;
}).toList(),
chartData.map((e) => e.min).toList(),
ColorsManager.minBlue,
ColorsManager.minBlueDot,
),
];
}
@override
Widget build(BuildContext context) {

View File

@ -32,7 +32,7 @@ class RangeOfAqiChartBox extends StatelessWidget {
const SizedBox(height: 10),
const Divider(),
const SizedBox(height: 20),
Expanded(child: RangeOfAqiChart(chartData: state.filteredRangeOfAqi)),
Expanded(child: RangeOfAqiChart(chartData: state.rangeOfAqi)),
],
),
);

View File

@ -1,18 +1,15 @@
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:syncrow_web/pages/analytics/modules/air_quality/blocs/range_of_aqi/range_of_aqi_bloc.dart';
import 'package:syncrow_web/pages/analytics/modules/air_quality/helpers/fetch_air_quality_data_helper.dart';
import 'package:syncrow_web/pages/analytics/modules/air_quality/widgets/aqi_type_dropdown.dart';
import 'package:syncrow_web/pages/analytics/modules/analytics/blocs/analytics_date_picker_bloc/analytics_date_picker_bloc.dart';
import 'package:syncrow_web/pages/analytics/modules/analytics/widgets/chart_informative_cell.dart';
import 'package:syncrow_web/pages/analytics/modules/energy_management/widgets/chart_title.dart';
import 'package:syncrow_web/pages/analytics/widgets/charts_loading_widget.dart';
import 'package:syncrow_web/pages/space_tree/bloc/space_tree_bloc.dart';
class RangeOfAqiChartTitle extends StatelessWidget {
const RangeOfAqiChartTitle({
required this.isLoading,
super.key,
});
const RangeOfAqiChartTitle({required this.isLoading, super.key});
final bool isLoading;
static const List<(Color color, String title, bool hasBorder)> _colors = [
@ -69,9 +66,12 @@ class RangeOfAqiChartTitle extends StatelessWidget {
if (spaceUuid == null) return;
if (value != null) {
context.read<RangeOfAqiBloc>().add(UpdateAqiTypeEvent(value));
}
FetchAirQualityDataHelper.loadRangeOfAqi(
context,
spaceUuid: spaceUuid,
date: context.read<AnalyticsDatePickerBloc>().state.monthlyDate,
aqiType: value ?? AqiType.aqi,
);
},
),
),

View File

@ -1,7 +1,6 @@
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:syncrow_web/pages/analytics/modules/air_quality/helpers/fetch_air_quality_data_helper.dart';
import 'package:syncrow_web/pages/analytics/modules/analytics/blocs/analytics_date_picker_bloc/analytics_date_picker_bloc.dart';
import 'package:syncrow_web/pages/analytics/modules/analytics/strategies/analytics_data_loading_strategy.dart';
import 'package:syncrow_web/pages/space_tree/bloc/space_tree_bloc.dart';
import 'package:syncrow_web/pages/space_tree/bloc/space_tree_event.dart';
@ -27,10 +26,10 @@ final class AirQualityDataLoadingStrategy implements AnalyticsDataLoadingStrateg
final spaceTreeBloc = context.read<SpaceTreeBloc>();
final isSpaceSelected = spaceTreeBloc.state.selectedSpaces.contains(space.uuid);
if (isSpaceSelected) {
clearData(context);
return;
}
final hasSelectedSpaces = spaceTreeBloc.state.selectedSpaces.isNotEmpty;
if (hasSelectedSpaces) clearData(context);
if (isSpaceSelected) return;
spaceTreeBloc
..add(const SpaceTreeClearSelectionEvent())
@ -40,22 +39,14 @@ final class AirQualityDataLoadingStrategy implements AnalyticsDataLoadingStrateg
context,
communityUuid: community.uuid,
spaceUuid: space.uuid ?? '',
date: context.read<AnalyticsDatePickerBloc>().state.monthlyDate,
);
}
@override
void onChildSpaceSelected(
BuildContext context,
CommunityModel community,
SpaceModel child,
) {
return onSpaceSelected(context, community, child);
}
@override
void clearData(BuildContext context) {
context.read<SpaceTreeBloc>().add(const SpaceTreeClearSelectionEvent());
context.read<SpaceTreeBloc>().add(
const AnalyticsClearAllSpaceTreeSelectionsEvent(),
);
FetchAirQualityDataHelper.clearAllData(context);
}
}

View File

@ -13,10 +13,5 @@ abstract class AnalyticsDataLoadingStrategy {
CommunityModel community,
SpaceModel space,
);
void onChildSpaceSelected(
BuildContext context,
CommunityModel community,
SpaceModel child,
);
void clearData(BuildContext context);
}

View File

@ -14,24 +14,14 @@ class EnergyManagementDataLoadingStrategy implements AnalyticsDataLoadingStrateg
CommunityModel community,
List<SpaceModel> spaces,
) {
context.read<SpaceTreeBloc>().add(
OnCommunitySelected(
community.uuid,
spaces,
),
);
final spaceTreeBloc = context.read<SpaceTreeBloc>();
final isCommunitySelected =
spaceTreeBloc.state.selectedCommunities.contains(community.uuid);
final spaceTreeState = context.read<SpaceTreeBloc>().state;
if (spaceTreeState.selectedCommunities.contains(community.uuid)) {
if (isCommunitySelected) {
clearData(context);
return;
}
FetchEnergyManagementDataHelper.loadEnergyManagementData(
context,
communityId: community.uuid,
spaceId: spaces.isNotEmpty ? spaces.first.uuid ?? '' : '',
);
}
@override
@ -40,7 +30,24 @@ class EnergyManagementDataLoadingStrategy implements AnalyticsDataLoadingStrateg
CommunityModel community,
SpaceModel space,
) {
context.read<SpaceTreeBloc>().add(
final spaceTreeBloc = context.read<SpaceTreeBloc>();
final isSpaceSelected = spaceTreeBloc.state.selectedSpaces.contains(space.uuid);
final hasSelectedSpaces = spaceTreeBloc.state.selectedSpaces.isNotEmpty;
if (isSpaceSelected) {
final firstSelectedSpace = spaceTreeBloc.state.selectedSpaces.first;
final isTheFirstSelectedSpace = firstSelectedSpace == space.uuid;
if (isTheFirstSelectedSpace) {
clearData(context);
}
return;
}
if (hasSelectedSpaces) {
clearData(context);
}
spaceTreeBloc.add(
OnSpaceSelected(
community,
space.uuid ?? '',
@ -48,13 +55,6 @@ class EnergyManagementDataLoadingStrategy implements AnalyticsDataLoadingStrateg
),
);
final spaceTreeState = context.read<SpaceTreeBloc>().state;
if (spaceTreeState.selectedCommunities.contains(community.uuid) ||
spaceTreeState.selectedSpaces.contains(space.uuid)) {
clearData(context);
return;
}
FetchEnergyManagementDataHelper.loadEnergyManagementData(
context,
communityId: community.uuid,
@ -62,18 +62,11 @@ class EnergyManagementDataLoadingStrategy implements AnalyticsDataLoadingStrateg
);
}
@override
void onChildSpaceSelected(
BuildContext context,
CommunityModel community,
SpaceModel child,
) {
return onSpaceSelected(context, community, child);
}
@override
void clearData(BuildContext context) {
context.read<SpaceTreeBloc>().add(const SpaceTreeClearSelectionEvent());
context.read<SpaceTreeBloc>().add(
const AnalyticsClearAllSpaceTreeSelectionsEvent(),
);
FetchEnergyManagementDataHelper.clearAllData(context);
}
}

View File

@ -26,10 +26,10 @@ class OccupancyDataLoadingStrategy implements AnalyticsDataLoadingStrategy {
final spaceTreeBloc = context.read<SpaceTreeBloc>();
final isSpaceSelected = spaceTreeBloc.state.selectedSpaces.contains(space.uuid);
if (isSpaceSelected) {
clearData(context);
return;
}
final hasSelectedSpaces = spaceTreeBloc.state.selectedSpaces.isNotEmpty;
if (hasSelectedSpaces) clearData(context);
if (isSpaceSelected) return;
spaceTreeBloc
..add(const SpaceTreeClearSelectionEvent())
@ -42,18 +42,11 @@ class OccupancyDataLoadingStrategy implements AnalyticsDataLoadingStrategy {
);
}
@override
void onChildSpaceSelected(
BuildContext context,
CommunityModel community,
SpaceModel child,
) {
return onSpaceSelected(context, community, child);
}
@override
void clearData(BuildContext context) {
context.read<SpaceTreeBloc>().add(const SpaceTreeClearSelectionEvent());
context.read<SpaceTreeBloc>().add(
const AnalyticsClearAllSpaceTreeSelectionsEvent(),
);
FetchOccupancyDataHelper.clearAllData(context);
}
}

View File

@ -1,6 +1,5 @@
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:syncrow_web/pages/analytics/modules/air_quality/blocs/air_quality_distribution/air_quality_distribution_bloc.dart';
import 'package:syncrow_web/pages/analytics/modules/air_quality/blocs/range_of_aqi/range_of_aqi_bloc.dart';
import 'package:syncrow_web/pages/analytics/modules/analytics/blocs/analytics_date_picker_bloc/analytics_date_picker_bloc.dart';
import 'package:syncrow_web/pages/analytics/modules/analytics/blocs/analytics_devices/analytics_devices_bloc.dart';
@ -14,7 +13,6 @@ import 'package:syncrow_web/pages/analytics/modules/energy_management/blocs/real
import 'package:syncrow_web/pages/analytics/modules/energy_management/blocs/total_energy_consumption/total_energy_consumption_bloc.dart';
import 'package:syncrow_web/pages/analytics/modules/occupancy/blocs/occupancy/occupancy_bloc.dart';
import 'package:syncrow_web/pages/analytics/modules/occupancy/blocs/occupancy_heat_map/occupancy_heat_map_bloc.dart';
import 'package:syncrow_web/pages/analytics/services/air_quality_distribution/fake_air_quality_distribution_service.dart';
import 'package:syncrow_web/pages/analytics/services/analytics_devices/analytics_devices_service_delagate.dart';
import 'package:syncrow_web/pages/analytics/services/analytics_devices/remote_energy_management_analytics_devices_service.dart';
import 'package:syncrow_web/pages/analytics/services/analytics_devices/remote_occupancy_analytics_devices_service.dart';
@ -103,11 +101,6 @@ class _AnalyticsPageState extends State<AnalyticsPage> {
FakeRangeOfAqiService(),
),
),
BlocProvider(
create: (context) => AirQualityDistributionBloc(
FakeAirQualityDistributionService(),
),
),
],
child: const AnalyticsPageForm(),
);

View File

@ -21,7 +21,7 @@ class AnalyticsCommunitiesSidebar extends StatelessWidget {
strategy.onSpaceSelected(context, community, space);
},
onSelectChildSpace: (community, child) {
strategy.onChildSpaceSelected(context, community, child);
strategy.onSpaceSelected(context, community, child);
},
),
);

View File

@ -1,6 +1,5 @@
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:syncrow_web/pages/analytics/modules/air_quality/helpers/fetch_air_quality_data_helper.dart';
import 'package:syncrow_web/pages/analytics/modules/analytics/blocs/analytics_date_picker_bloc/analytics_date_picker_bloc.dart';
import 'package:syncrow_web/pages/analytics/modules/analytics/blocs/analytics_tab/analytics_tab_bloc.dart';
import 'package:syncrow_web/pages/analytics/modules/analytics/enums/analytics_page_tab.dart';
@ -57,16 +56,33 @@ class AnalyticsPageTabsAndChildren extends StatelessWidget {
const Spacer(),
Visibility(
key: ValueKey(selectedTab),
visible: selectedTab == AnalyticsPageTab.energyManagement ||
selectedTab == AnalyticsPageTab.airQuality,
visible: selectedTab == AnalyticsPageTab.energyManagement,
child: Expanded(
flex: 2,
child: FittedBox(
fit: BoxFit.scaleDown,
alignment: AlignmentDirectional.centerEnd,
child: AnalyticsDateFilterButton(
onDateSelected: (value) {
_onDateChanged(context, value, selectedTab);
onDateSelected: (DateTime value) {
context.read<AnalyticsDatePickerBloc>().add(
UpdateAnalyticsDatePickerEvent(montlyDate: value),
);
final spaceTreeState =
context.read<SpaceTreeBloc>().state;
if (spaceTreeState.selectedSpaces.isNotEmpty) {
FetchEnergyManagementDataHelper
.loadEnergyManagementData(
context,
shouldFetchAnalyticsDevices: false,
selectedDate: value,
communityId:
spaceTreeState.selectedCommunities.firstOrNull ??
'',
spaceId:
spaceTreeState.selectedSpaces.firstOrNull ?? '',
);
}
},
selectedDate: context
.watch<AnalyticsDatePickerBloc>()
@ -96,73 +112,4 @@ class AnalyticsPageTabsAndChildren extends StatelessWidget {
child: child,
);
}
void _onDateChanged(
BuildContext context,
DateTime date,
AnalyticsPageTab selectedTab,
) {
context.read<AnalyticsDatePickerBloc>().add(
UpdateAnalyticsDatePickerEvent(montlyDate: date),
);
final spaceTreeState = context.read<SpaceTreeBloc>().state;
final communities = spaceTreeState.selectedCommunities;
final spaces = spaceTreeState.selectedSpaces;
if (spaceTreeState.selectedSpaces.isNotEmpty) {
switch (selectedTab) {
case AnalyticsPageTab.energyManagement:
_onEnergyManagementDateChanged(
context,
date: date,
communityUuid: communities.firstOrNull ?? '',
spaceUuid: spaces.firstOrNull ?? '',
);
break;
case AnalyticsPageTab.airQuality:
_onAirQualityDateChanged(
context,
date: date,
communityUuid: communities.firstOrNull ?? '',
spaceUuid: spaces.firstOrNull ?? '',
);
default:
break;
}
}
}
void _onEnergyManagementDateChanged(
BuildContext context, {
required DateTime date,
required String communityUuid,
required String spaceUuid,
}) {
context.read<AnalyticsDatePickerBloc>().add(
UpdateAnalyticsDatePickerEvent(montlyDate: date),
);
FetchEnergyManagementDataHelper.loadEnergyManagementData(
context,
shouldFetchAnalyticsDevices: false,
selectedDate: date,
communityId: communityUuid,
spaceId: spaceUuid,
);
}
void _onAirQualityDateChanged(
BuildContext context, {
required DateTime date,
required String communityUuid,
required String spaceUuid,
}) {
FetchAirQualityDataHelper.loadAirQualityData(
context,
date: date,
communityUuid: communityUuid,
spaceUuid: spaceUuid,
shouldFetchAnalyticsDevices: false,
);
}
}

View File

@ -1,9 +0,0 @@
class GetAirQualityDistributionParam {
final DateTime date;
final String spaceUuid;
const GetAirQualityDistributionParam({
required this.date,
required this.spaceUuid,
});
}

View File

@ -1,12 +1,16 @@
import 'package:equatable/equatable.dart';
import 'package:syncrow_web/pages/analytics/modules/air_quality/widgets/aqi_type_dropdown.dart';
class GetRangeOfAqiParam extends Equatable {
final DateTime date;
final String spaceUuid;
final AqiType aqiType;
const GetRangeOfAqiParam({
const GetRangeOfAqiParam(
{
required this.date,
required this.spaceUuid,
required this.aqiType,
});
@override

View File

@ -1,8 +0,0 @@
import 'package:syncrow_web/pages/analytics/models/air_quality_data_model.dart';
import 'package:syncrow_web/pages/analytics/params/get_air_quality_distribution_param.dart';
abstract interface class AirQualityDistributionService {
Future<List<AirQualityDataModel>> getAirQualityDistribution(
GetAirQualityDistributionParam param,
);
}

View File

@ -1,95 +0,0 @@
import 'dart:math';
import 'package:syncrow_web/pages/analytics/models/air_quality_data_model.dart';
import 'package:syncrow_web/pages/analytics/modules/air_quality/widgets/aqi_type_dropdown.dart';
import 'package:syncrow_web/pages/analytics/params/get_air_quality_distribution_param.dart';
import 'package:syncrow_web/pages/analytics/services/air_quality_distribution/air_quality_distribution_service.dart';
class FakeAirQualityDistributionService implements AirQualityDistributionService {
final _random = Random();
@override
Future<List<AirQualityDataModel>> getAirQualityDistribution(
GetAirQualityDistributionParam param,
) async {
return Future.delayed(
const Duration(milliseconds: 400),
() => List.generate(30, (index) {
final date = DateTime(2025, 5, 1).add(Duration(days: index));
final values = _generateRandomPercentages();
final nullMask = List.generate(6, (_) => _shouldBeNull());
if (nullMask.every((isNull) => isNull)) {
nullMask[_random.nextInt(6)] = false;
}
final nonNullValues = _redistributePercentages(values, nullMask);
return AirQualityDataModel(
date: date,
data: [
AirQualityPercentageData(
type: AqiType.aqi.code,
percentage: nonNullValues[0],
name: 'good',
),
AirQualityPercentageData(
name: 'moderate',
type: AqiType.co2.code,
percentage: nonNullValues[1],
),
AirQualityPercentageData(
name: 'poor',
percentage: nonNullValues[2],
type: AqiType.hcho.code,
),
AirQualityPercentageData(
name: 'unhealthy',
percentage: nonNullValues[3],
type: AqiType.pm10.code,
),
AirQualityPercentageData(
name: 'severe',
type: AqiType.pm25.code,
percentage: nonNullValues[4],
),
AirQualityPercentageData(
name: 'hazardous',
percentage: nonNullValues[5],
type: AqiType.co2.code,
),
],
);
}),
);
}
List<double> _redistributePercentages(
List<double> originalValues,
List<bool> nullMask,
) {
double nonNullSum = 0;
for (int i = 0; i < originalValues.length; i++) {
if (!nullMask[i]) {
nonNullSum += originalValues[i];
}
}
return List.generate(originalValues.length, (i) {
if (nullMask[i]) return 0;
return (originalValues[i] / nonNullSum * 100).roundToDouble();
});
}
bool _shouldBeNull() => _random.nextDouble() < 0.6;
List<double> _generateRandomPercentages() {
final values = List.generate(6, (_) => _random.nextDouble());
final sum = values.reduce((a, b) => a + b);
return values.map((value) => (value / sum * 100).roundToDouble()).toList();
}
}

View File

@ -1,36 +0,0 @@
import 'package:syncrow_web/pages/analytics/models/air_quality_data_model.dart';
import 'package:syncrow_web/pages/analytics/params/get_air_quality_distribution_param.dart';
import 'package:syncrow_web/pages/analytics/services/air_quality_distribution/air_quality_distribution_service.dart';
import 'package:syncrow_web/services/api/http_service.dart';
class RemoteAirQualityDistributionService implements AirQualityDistributionService {
RemoteAirQualityDistributionService(this._httpService);
final HTTPService _httpService;
@override
Future<List<AirQualityDataModel>> getAirQualityDistribution(
GetAirQualityDistributionParam param,
) async {
try {
final response = await _httpService.get(
path: 'endpoint',
queryParameters: {
'spaceUuid': param.spaceUuid,
'date': param.date.toIso8601String(),
},
expectedResponseModel: (data) {
final json = data as Map<String, dynamic>? ?? {};
final mappedData = json['data'] as List<dynamic>? ?? [];
return mappedData.map((e) {
final jsonData = e as Map<String, dynamic>;
return AirQualityDataModel.fromJson(jsonData);
}).toList();
},
);
return response;
} catch (e) {
throw Exception('Failed to load energy consumption per phase: $e');
}
}
}

View File

@ -1,5 +1,4 @@
import 'package:syncrow_web/pages/analytics/models/range_of_aqi.dart';
import 'package:syncrow_web/pages/analytics/modules/air_quality/widgets/aqi_type_dropdown.dart';
import 'package:syncrow_web/pages/analytics/params/get_range_of_aqi_param.dart';
import 'package:syncrow_web/pages/analytics/services/range_of_aqi/range_of_aqi_service.dart';
@ -20,14 +19,9 @@ class FakeRangeOfAqiService implements RangeOfAqiService {
final max = (avg + maxDelta).clamp(0.0, 301.0);
return RangeOfAqi(
data: [
RangeOfAqiValue(type: AqiType.aqi.code, min: min, average: avg, max: max),
RangeOfAqiValue(type: AqiType.pm25.code, min: min, average: avg, max: max),
RangeOfAqiValue(type: AqiType.pm10.code, min: min, average: avg, max: max),
RangeOfAqiValue(type: AqiType.hcho.code, min: min, average: avg, max: max),
RangeOfAqiValue(type: AqiType.tvoc.code, min: min, average: avg, max: max),
RangeOfAqiValue(type: AqiType.co2.code, min: min, average: avg, max: max),
],
min: min,
avg: avg,
max: max,
date: date,
);
});

View File

@ -1,34 +0,0 @@
import 'package:syncrow_web/pages/analytics/models/range_of_aqi.dart';
import 'package:syncrow_web/pages/analytics/params/get_range_of_aqi_param.dart';
import 'package:syncrow_web/pages/analytics/services/range_of_aqi/range_of_aqi_service.dart';
import 'package:syncrow_web/services/api/http_service.dart';
final class RemoteRangeOfAqiService implements RangeOfAqiService {
const RemoteRangeOfAqiService(this._httpService);
final HTTPService _httpService;
@override
Future<List<RangeOfAqi>> load(GetRangeOfAqiParam param) async {
try {
final response = await _httpService.get(
path: 'endpoint',
queryParameters: {
'spaceUuid': param.spaceUuid,
'date': param.date.toIso8601String(),
},
expectedResponseModel: (data) {
final json = data as Map<String, dynamic>? ?? {};
final mappedData = json['data'] as List<dynamic>? ?? [];
return mappedData.map((e) {
final jsonData = e as Map<String, dynamic>;
return RangeOfAqi.fromJson(jsonData);
}).toList();
},
);
return response;
} catch (e) {
throw Exception('Failed to load energy consumption per phase: $e');
}
}
}

View File

@ -13,6 +13,7 @@ import 'package:syncrow_web/pages/common/bloc/project_manager.dart';
import 'package:syncrow_web/pages/home/bloc/home_bloc.dart';
import 'package:syncrow_web/pages/space_tree/bloc/space_tree_bloc.dart';
import 'package:syncrow_web/pages/space_tree/bloc/space_tree_event.dart';
import 'package:syncrow_web/services/api/api_exception.dart';
import 'package:syncrow_web/services/auth_api.dart';
import 'package:syncrow_web/utils/constants/strings_manager.dart';
import 'package:syncrow_web/utils/helpers/shared_preferences_helper.dart';
@ -99,7 +100,8 @@ class AuthBloc extends Bloc<AuthEvent, AuthState> {
emit(const TimerState(isButtonEnabled: true, remainingTime: 0));
}
Future<void> changePassword(ChangePasswordEvent event, Emitter<AuthState> emit) async {
Future<void> changePassword(
ChangePasswordEvent event, Emitter<AuthState> emit) async {
emit(LoadingForgetState());
try {
var response = await AuthenticationAPI.verifyOtp(
@ -113,14 +115,14 @@ class AuthBloc extends Bloc<AuthEvent, AuthState> {
emit(const TimerState(isButtonEnabled: true, remainingTime: 0));
emit(SuccessForgetState());
}
} on DioException catch (e) {
final errorData = e.response!.data;
String errorMessage = errorData['error']['message'] ?? 'something went wrong';
} on APIException catch (e) {
final errorMessage = e.message;
validate = errorMessage;
emit(AuthInitialState());
}
}
String? validateCode(String? value) {
if (value == null || value.isEmpty) {
return 'Code is required';
@ -149,6 +151,7 @@ class AuthBloc extends Bloc<AuthEvent, AuthState> {
static UserModel? user;
bool showValidationMessage = false;
void _login(LoginButtonPressed event, Emitter<AuthState> emit) async {
emit(AuthLoading());
if (isChecked) {
@ -161,25 +164,24 @@ class AuthBloc extends Bloc<AuthEvent, AuthState> {
token = await AuthenticationAPI.loginWithEmail(
model: LoginWithEmailModel(
email: event.username,
email: event.username.toLowerCase(),
password: event.password,
),
);
} on DioException catch (e) {
final errorData = e.response!.data;
String errorMessage = errorData['error']['message'];
if (errorMessage == "Access denied for web platform") {
validate = errorMessage;
} else {
validate = 'Invalid Credentials!';
}
} on APIException catch (e) {
validate = e.message;
emit(LoginInitial());
return;
} catch (e) {
validate = 'Something went wrong';
emit(LoginInitial());
return;
}
if (token.accessTokenIsNotEmpty) {
FlutterSecureStorage storage = const FlutterSecureStorage();
await storage.write(key: Token.loginAccessTokenKey, value: token.accessToken);
await storage.write(
key: Token.loginAccessTokenKey, value: token.accessToken);
const FlutterSecureStorage().write(
key: UserModel.userUuidKey,
value: Token.decodeToken(token.accessToken)['uuid'].toString());
@ -195,6 +197,7 @@ class AuthBloc extends Bloc<AuthEvent, AuthState> {
}
}
checkBoxToggle(
CheckBoxEvent event,
Emitter<AuthState> emit,

View File

@ -1,6 +1,7 @@
import 'dart:async';
import 'package:bloc/bloc.dart';
import 'package:dio/dio.dart';
import 'package:equatable/equatable.dart';
import 'package:flutter/material.dart';
import 'package:syncrow_web/pages/common/bloc/project_manager.dart';
@ -15,6 +16,7 @@ import 'package:syncrow_web/pages/routines/models/device_functions.dart';
import 'package:syncrow_web/pages/routines/models/routine_details_model.dart';
import 'package:syncrow_web/pages/routines/models/routine_model.dart';
import 'package:syncrow_web/pages/space_tree/bloc/space_tree_bloc.dart';
import 'package:syncrow_web/services/api/api_exception.dart';
import 'package:syncrow_web/services/devices_mang_api.dart';
import 'package:syncrow_web/services/routines_api.dart';
import 'package:syncrow_web/utils/constants/assets.dart';
@ -64,7 +66,8 @@ class RoutineBloc extends Bloc<RoutineEvent, RoutineState> {
TriggerSwitchTabsEvent event,
Emitter<RoutineState> emit,
) {
emit(state.copyWith(routineTab: event.isRoutineTab, createRoutineView: false));
emit(state.copyWith(
routineTab: event.isRoutineTab, createRoutineView: false));
add(ResetRoutineState());
if (event.isRoutineTab) {
add(const LoadScenes());
@ -90,8 +93,8 @@ class RoutineBloc extends Bloc<RoutineEvent, RoutineState> {
final updatedIfItems = List<Map<String, dynamic>>.from(state.ifItems);
// Find the index of the item in teh current itemsList
int index =
updatedIfItems.indexWhere((map) => map['uniqueCustomId'] == event.item['uniqueCustomId']);
int index = updatedIfItems.indexWhere(
(map) => map['uniqueCustomId'] == event.item['uniqueCustomId']);
// Replace the map if the index is valid
if (index != -1) {
updatedIfItems[index] = event.item;
@ -100,18 +103,21 @@ class RoutineBloc extends Bloc<RoutineEvent, RoutineState> {
}
if (event.isTabToRun) {
emit(state.copyWith(ifItems: updatedIfItems, isTabToRun: true, isAutomation: false));
emit(state.copyWith(
ifItems: updatedIfItems, isTabToRun: true, isAutomation: false));
} else {
emit(state.copyWith(ifItems: updatedIfItems, isTabToRun: false, isAutomation: true));
emit(state.copyWith(
ifItems: updatedIfItems, isTabToRun: false, isAutomation: true));
}
}
void _onAddToThenContainer(AddToThenContainer event, Emitter<RoutineState> emit) {
void _onAddToThenContainer(
AddToThenContainer event, Emitter<RoutineState> emit) {
final currentItems = List<Map<String, dynamic>>.from(state.thenItems);
// Find the index of the item in teh current itemsList
int index =
currentItems.indexWhere((map) => map['uniqueCustomId'] == event.item['uniqueCustomId']);
int index = currentItems.indexWhere(
(map) => map['uniqueCustomId'] == event.item['uniqueCustomId']);
// Replace the map if the index is valid
if (index != -1) {
currentItems[index] = event.item;
@ -122,7 +128,8 @@ class RoutineBloc extends Bloc<RoutineEvent, RoutineState> {
emit(state.copyWith(thenItems: currentItems));
}
void _onAddFunctionsToRoutine(AddFunctionToRoutine event, Emitter<RoutineState> emit) {
void _onAddFunctionsToRoutine(
AddFunctionToRoutine event, Emitter<RoutineState> emit) {
try {
if (event.functions.isEmpty) return;
@ -157,7 +164,8 @@ class RoutineBloc extends Bloc<RoutineEvent, RoutineState> {
// currentSelectedFunctions[event.uniqueCustomId] = List.from(event.functions);
// }
currentSelectedFunctions[event.uniqueCustomId] = List.from(event.functions);
currentSelectedFunctions[event.uniqueCustomId] =
List.from(event.functions);
emit(state.copyWith(selectedFunctions: currentSelectedFunctions));
} catch (e) {
@ -165,24 +173,30 @@ class RoutineBloc extends Bloc<RoutineEvent, RoutineState> {
}
}
Future<void> _onLoadScenes(LoadScenes event, Emitter<RoutineState> emit) async {
Future<void> _onLoadScenes(
LoadScenes event, Emitter<RoutineState> emit) async {
emit(state.copyWith(isLoading: true, errorMessage: null));
List<ScenesModel> scenes = [];
try {
BuildContext context = NavigationService.navigatorKey.currentContext!;
var createRoutineBloc = context.read<CreateRoutineBloc>();
final projectUuid = await ProjectManager.getProjectUUID() ?? '';
if (createRoutineBloc.selectedSpaceId == '' && createRoutineBloc.selectedCommunityId == '') {
if (createRoutineBloc.selectedSpaceId == '' &&
createRoutineBloc.selectedCommunityId == '') {
var spaceBloc = context.read<SpaceTreeBloc>();
for (var communityId in spaceBloc.state.selectedCommunities) {
List<String> spacesList = spaceBloc.state.selectedCommunityAndSpaces[communityId] ?? [];
List<String> spacesList =
spaceBloc.state.selectedCommunityAndSpaces[communityId] ?? [];
for (var spaceId in spacesList) {
scenes.addAll(await SceneApi.getScenes(spaceId, communityId, projectUuid));
scenes.addAll(
await SceneApi.getScenes(spaceId, communityId, projectUuid));
}
}
} else {
scenes.addAll(await SceneApi.getScenes(
createRoutineBloc.selectedSpaceId, createRoutineBloc.selectedCommunityId, projectUuid));
createRoutineBloc.selectedSpaceId,
createRoutineBloc.selectedCommunityId,
projectUuid));
}
emit(state.copyWith(
@ -199,7 +213,8 @@ class RoutineBloc extends Bloc<RoutineEvent, RoutineState> {
}
}
Future<void> _onLoadAutomation(LoadAutomation event, Emitter<RoutineState> emit) async {
Future<void> _onLoadAutomation(
LoadAutomation event, Emitter<RoutineState> emit) async {
emit(state.copyWith(isLoading: true, errorMessage: null));
List<ScenesModel> automations = [];
final projectId = await ProjectManager.getProjectUUID() ?? '';
@ -207,17 +222,22 @@ class RoutineBloc extends Bloc<RoutineEvent, RoutineState> {
BuildContext context = NavigationService.navigatorKey.currentContext!;
var createRoutineBloc = context.read<CreateRoutineBloc>();
try {
if (createRoutineBloc.selectedSpaceId == '' && createRoutineBloc.selectedCommunityId == '') {
if (createRoutineBloc.selectedSpaceId == '' &&
createRoutineBloc.selectedCommunityId == '') {
var spaceBloc = context.read<SpaceTreeBloc>();
for (var communityId in spaceBloc.state.selectedCommunities) {
List<String> spacesList = spaceBloc.state.selectedCommunityAndSpaces[communityId] ?? [];
List<String> spacesList =
spaceBloc.state.selectedCommunityAndSpaces[communityId] ?? [];
for (var spaceId in spacesList) {
automations.addAll(await SceneApi.getAutomation(spaceId, communityId, projectId));
automations.addAll(
await SceneApi.getAutomation(spaceId, communityId, projectId));
}
}
} else {
automations.addAll(await SceneApi.getAutomation(
createRoutineBloc.selectedSpaceId, createRoutineBloc.selectedCommunityId, projectId));
createRoutineBloc.selectedSpaceId,
createRoutineBloc.selectedCommunityId,
projectId));
}
emit(state.copyWith(
automations: automations,
@ -233,14 +253,16 @@ class RoutineBloc extends Bloc<RoutineEvent, RoutineState> {
}
}
FutureOr<void> _onSearchRoutines(SearchRoutines event, Emitter<RoutineState> emit) async {
FutureOr<void> _onSearchRoutines(
SearchRoutines event, Emitter<RoutineState> emit) async {
emit(state.copyWith(isLoading: true, errorMessage: null));
await Future.delayed(const Duration(seconds: 1));
emit(state.copyWith(isLoading: false, errorMessage: null));
emit(state.copyWith(searchText: event.query));
}
FutureOr<void> _onAddSelectedIcon(AddSelectedIcon event, Emitter<RoutineState> emit) {
FutureOr<void> _onAddSelectedIcon(
AddSelectedIcon event, Emitter<RoutineState> emit) {
emit(state.copyWith(selectedIcon: event.icon));
}
@ -254,7 +276,8 @@ class RoutineBloc extends Bloc<RoutineEvent, RoutineState> {
return actions.last['deviceId'] == 'delay';
}
Future<void> _onCreateScene(CreateSceneEvent event, Emitter<RoutineState> emit) async {
Future<void> _onCreateScene(
CreateSceneEvent event, Emitter<RoutineState> emit) async {
try {
// Check if first action is delay
// if (_isFirstActionDelay(state.thenItems)) {
@ -267,7 +290,8 @@ class RoutineBloc extends Bloc<RoutineEvent, RoutineState> {
if (_isLastActionDelay(state.thenItems)) {
emit(state.copyWith(
errorMessage: 'A delay condition cannot be the only or the last action',
errorMessage:
'A delay condition cannot be the only or the last action',
isLoading: false,
));
return;
@ -335,15 +359,18 @@ class RoutineBloc extends Bloc<RoutineEvent, RoutineState> {
errorMessage: 'Something went wrong',
));
}
} catch (e) {
} on APIException catch (e) {
final errorData = e.message;
String errorMessage = errorData;
emit(state.copyWith(
isLoading: false,
errorMessage: 'Something went wrong',
errorMessage: errorMessage,
));
}
}
Future<void> _onCreateAutomation(CreateAutomationEvent event, Emitter<RoutineState> emit) async {
Future<void> _onCreateAutomation(
CreateAutomationEvent event, Emitter<RoutineState> emit) async {
try {
final projectUuid = await ProjectManager.getProjectUUID() ?? '';
if (state.routineName == null || state.routineName!.isEmpty) {
@ -365,7 +392,8 @@ class RoutineBloc extends Bloc<RoutineEvent, RoutineState> {
if (_isLastActionDelay(state.thenItems)) {
emit(state.copyWith(
errorMessage: 'A delay condition cannot be the only or the last action',
errorMessage:
'A delay condition cannot be the only or the last action',
isLoading: false,
));
CustomSnackBar.redSnackBar('Cannot have delay as the last action');
@ -456,7 +484,8 @@ class RoutineBloc extends Bloc<RoutineEvent, RoutineState> {
actions: actions,
);
final result = await SceneApi.createAutomation(createAutomationModel, projectUuid);
final result =
await SceneApi.createAutomation(createAutomationModel, projectUuid);
if (result['success']) {
add(ResetRoutineState());
add(const LoadAutomation());
@ -468,26 +497,32 @@ class RoutineBloc extends Bloc<RoutineEvent, RoutineState> {
));
CustomSnackBar.redSnackBar('Something went wrong');
}
} catch (e) {
} on APIException catch (e) {
final errorData = e.message;
String errorMessage = errorData;
emit(state.copyWith(
isLoading: false,
errorMessage: 'Something went wrong',
errorMessage: errorMessage,
));
CustomSnackBar.redSnackBar('Something went wrong');
CustomSnackBar.redSnackBar(errorMessage);
}
}
FutureOr<void> _onRemoveDragCard(RemoveDragCard event, Emitter<RoutineState> emit) {
FutureOr<void> _onRemoveDragCard(
RemoveDragCard event, Emitter<RoutineState> emit) {
if (event.isFromThen) {
final thenItems = List<Map<String, dynamic>>.from(state.thenItems);
final selectedFunctions = Map<String, List<DeviceFunctionData>>.from(state.selectedFunctions);
final selectedFunctions =
Map<String, List<DeviceFunctionData>>.from(state.selectedFunctions);
thenItems.removeAt(event.index);
selectedFunctions.remove(event.key);
emit(state.copyWith(thenItems: thenItems, selectedFunctions: selectedFunctions));
emit(state.copyWith(
thenItems: thenItems, selectedFunctions: selectedFunctions));
} else {
final ifItems = List<Map<String, dynamic>>.from(state.ifItems);
final selectedFunctions = Map<String, List<DeviceFunctionData>>.from(state.selectedFunctions);
final selectedFunctions =
Map<String, List<DeviceFunctionData>>.from(state.selectedFunctions);
ifItems.removeAt(event.index);
selectedFunctions.remove(event.key);
@ -498,7 +533,8 @@ class RoutineBloc extends Bloc<RoutineEvent, RoutineState> {
isAutomation: false,
isTabToRun: false));
} else {
emit(state.copyWith(ifItems: ifItems, selectedFunctions: selectedFunctions));
emit(state.copyWith(
ifItems: ifItems, selectedFunctions: selectedFunctions));
}
}
}
@ -510,11 +546,13 @@ class RoutineBloc extends Bloc<RoutineEvent, RoutineState> {
));
}
FutureOr<void> _onEffectiveTimeEvent(EffectiveTimePeriodEvent event, Emitter<RoutineState> emit) {
FutureOr<void> _onEffectiveTimeEvent(
EffectiveTimePeriodEvent event, Emitter<RoutineState> emit) {
emit(state.copyWith(effectiveTime: event.effectiveTime));
}
FutureOr<void> _onSetRoutineName(SetRoutineName event, Emitter<RoutineState> emit) {
FutureOr<void> _onSetRoutineName(
SetRoutineName event, Emitter<RoutineState> emit) {
emit(state.copyWith(
routineName: event.name,
));
@ -641,7 +679,8 @@ class RoutineBloc extends Bloc<RoutineEvent, RoutineState> {
// return (thenItems, ifItems, currentFunctions);
// }
Future<void> _onGetSceneDetails(GetSceneDetails event, Emitter<RoutineState> emit) async {
Future<void> _onGetSceneDetails(
GetSceneDetails event, Emitter<RoutineState> emit) async {
try {
emit(state.copyWith(
isLoading: true,
@ -689,8 +728,10 @@ class RoutineBloc extends Bloc<RoutineEvent, RoutineState> {
// if (!deviceCards.containsKey(deviceId)) {
deviceCards[deviceId] = {
'entityId': action.entityId,
'deviceId': action.actionExecutor == 'delay' ? 'delay' : action.entityId,
'uniqueCustomId': action.type == 'automation' || action.actionExecutor == 'delay'
'deviceId':
action.actionExecutor == 'delay' ? 'delay' : action.entityId,
'uniqueCustomId':
action.type == 'automation' || action.actionExecutor == 'delay'
? action.entityId
: const Uuid().v4(),
'title': action.actionExecutor == 'delay'
@ -732,7 +773,8 @@ class RoutineBloc extends Bloc<RoutineEvent, RoutineState> {
),
);
// emit(state.copyWith(automationActionExecutor: action.actionExecutor));
} else if (action.executorProperty != null && action.actionExecutor != 'delay') {
} else if (action.executorProperty != null &&
action.actionExecutor != 'delay') {
final functions = matchingDevice?.functions ?? [];
final functionCode = action.executorProperty?.functionCode;
for (DeviceFunction function in functions) {
@ -798,7 +840,8 @@ class RoutineBloc extends Bloc<RoutineEvent, RoutineState> {
}
}
FutureOr<void> _onResetRoutineState(ResetRoutineState event, Emitter<RoutineState> emit) {
FutureOr<void> _onResetRoutineState(
ResetRoutineState event, Emitter<RoutineState> emit) {
emit(state.copyWith(
ifItems: [],
thenItems: [],
@ -822,7 +865,8 @@ class RoutineBloc extends Bloc<RoutineEvent, RoutineState> {
createRoutineView: false));
}
FutureOr<void> _deleteScene(DeleteScene event, Emitter<RoutineState> emit) async {
FutureOr<void> _deleteScene(
DeleteScene event, Emitter<RoutineState> emit) async {
try {
final projectId = await ProjectManager.getProjectUUID() ?? '';
@ -831,7 +875,8 @@ class RoutineBloc extends Bloc<RoutineEvent, RoutineState> {
var spaceBloc = context.read<SpaceTreeBloc>();
if (state.isTabToRun) {
await SceneApi.deleteScene(
unitUuid: spaceBloc.state.selectedSpaces[0], sceneId: state.sceneId ?? '');
unitUuid: spaceBloc.state.selectedSpaces[0],
sceneId: state.sceneId ?? '');
} else {
await SceneApi.deleteAutomation(
unitUuid: spaceBloc.state.selectedSpaces[0],
@ -854,11 +899,14 @@ class RoutineBloc extends Bloc<RoutineEvent, RoutineState> {
add(const LoadAutomation());
add(ResetRoutineState());
emit(state.copyWith(isLoading: false, createRoutineView: false));
} catch (e) {
} on APIException catch (e) {
final errorData = e.message;
String errorMessage = errorData;
emit(state.copyWith(
isLoading: false,
errorMessage: 'Failed to delete scene',
errorMessage: errorMessage,
));
CustomSnackBar.redSnackBar(errorMessage);
}
}
@ -876,7 +924,8 @@ class RoutineBloc extends Bloc<RoutineEvent, RoutineState> {
// }
// }
FutureOr<void> _fetchDevices(FetchDevicesInRoutine event, Emitter<RoutineState> emit) async {
FutureOr<void> _fetchDevices(
FetchDevicesInRoutine event, Emitter<RoutineState> emit) async {
emit(state.copyWith(isLoading: true));
try {
final projectUuid = await ProjectManager.getProjectUUID() ?? '';
@ -885,17 +934,21 @@ class RoutineBloc extends Bloc<RoutineEvent, RoutineState> {
var createRoutineBloc = context.read<CreateRoutineBloc>();
var spaceBloc = context.read<SpaceTreeBloc>();
if (createRoutineBloc.selectedSpaceId == '' && createRoutineBloc.selectedCommunityId == '') {
if (createRoutineBloc.selectedSpaceId == '' &&
createRoutineBloc.selectedCommunityId == '') {
for (var communityId in spaceBloc.state.selectedCommunities) {
List<String> spacesList = spaceBloc.state.selectedCommunityAndSpaces[communityId] ?? [];
List<String> spacesList =
spaceBloc.state.selectedCommunityAndSpaces[communityId] ?? [];
for (var spaceId in spacesList) {
devices.addAll(
await DevicesManagementApi().fetchDevices(communityId, spaceId, projectUuid));
devices.addAll(await DevicesManagementApi()
.fetchDevices(communityId, spaceId, projectUuid));
}
}
} else {
devices.addAll(await DevicesManagementApi().fetchDevices(
createRoutineBloc.selectedCommunityId, createRoutineBloc.selectedSpaceId, projectUuid));
createRoutineBloc.selectedCommunityId,
createRoutineBloc.selectedSpaceId,
projectUuid));
}
emit(state.copyWith(isLoading: false, devices: devices));
@ -904,7 +957,8 @@ class RoutineBloc extends Bloc<RoutineEvent, RoutineState> {
}
}
FutureOr<void> _onUpdateScene(UpdateScene event, Emitter<RoutineState> emit) async {
FutureOr<void> _onUpdateScene(
UpdateScene event, Emitter<RoutineState> emit) async {
try {
// Check if first action is delay
// if (_isFirstActionDelay(state.thenItems)) {
@ -918,7 +972,8 @@ class RoutineBloc extends Bloc<RoutineEvent, RoutineState> {
if (_isLastActionDelay(state.thenItems)) {
emit(state.copyWith(
errorMessage: 'A delay condition cannot be the only or the last action',
errorMessage:
'A delay condition cannot be the only or the last action',
isLoading: false,
));
return;
@ -971,7 +1026,8 @@ class RoutineBloc extends Bloc<RoutineEvent, RoutineState> {
actions: actions,
);
final result = await SceneApi.updateScene(createSceneModel, state.sceneId ?? '');
final result =
await SceneApi.updateScene(createSceneModel, state.sceneId ?? '');
if (result['success']) {
add(ResetRoutineState());
add(const LoadScenes());
@ -990,7 +1046,8 @@ class RoutineBloc extends Bloc<RoutineEvent, RoutineState> {
}
}
FutureOr<void> _onUpdateAutomation(UpdateAutomation event, Emitter<RoutineState> emit) async {
FutureOr<void> _onUpdateAutomation(
UpdateAutomation event, Emitter<RoutineState> emit) async {
try {
if (state.routineName == null || state.routineName!.isEmpty) {
emit(state.copyWith(
@ -1114,10 +1171,11 @@ class RoutineBloc extends Bloc<RoutineEvent, RoutineState> {
errorMessage: result['message'],
));
}
} catch (e) {
} on APIException catch (e) {
final errorData = e.message;
emit(state.copyWith(
isLoading: false,
errorMessage: 'Something went wrong',
errorMessage: errorData,
));
}
}
@ -1214,7 +1272,8 @@ class RoutineBloc extends Bloc<RoutineEvent, RoutineState> {
// if (!deviceThenCards.containsKey(deviceId)) {
deviceThenCards[deviceId] = {
'entityId': action.entityId,
'deviceId': action.actionExecutor == 'delay' ? 'delay' : action.entityId,
'deviceId':
action.actionExecutor == 'delay' ? 'delay' : action.entityId,
'uniqueCustomId': const Uuid().v4(),
'title': action.actionExecutor == 'delay'
? 'Delay'
@ -1249,7 +1308,8 @@ class RoutineBloc extends Bloc<RoutineEvent, RoutineState> {
updatedFunctions[uniqueCustomId] = [];
}
if (action.executorProperty != null && action.actionExecutor != 'delay') {
if (action.executorProperty != null &&
action.actionExecutor != 'delay') {
final functions = matchingDevice.functions;
final functionCode = action.executorProperty!.functionCode;
for (var function in functions) {
@ -1291,10 +1351,14 @@ class RoutineBloc extends Bloc<RoutineEvent, RoutineState> {
}
}
final ifItems = deviceIfCards.values.where((card) => card['type'] == 'condition').toList();
final ifItems = deviceIfCards.values
.where((card) => card['type'] == 'condition')
.toList();
final thenItems = deviceThenCards.values
.where((card) =>
card['type'] == 'action' || card['type'] == 'automation' || card['type'] == 'scene')
card['type'] == 'action' ||
card['type'] == 'automation' ||
card['type'] == 'scene')
.toList();
emit(state.copyWith(
@ -1316,7 +1380,8 @@ class RoutineBloc extends Bloc<RoutineEvent, RoutineState> {
}
}
Future<void> _onSceneTrigger(SceneTrigger event, Emitter<RoutineState> emit) async {
Future<void> _onSceneTrigger(
SceneTrigger event, Emitter<RoutineState> emit) async {
emit(state.copyWith(loadingSceneId: event.sceneId));
try {
@ -1358,24 +1423,29 @@ class RoutineBloc extends Bloc<RoutineEvent, RoutineState> {
if (success) {
final updatedAutomations = await SceneApi.getAutomationByUnitId(
event.automationStatusUpdate.spaceUuid, event.communityId, projectId);
event.automationStatusUpdate.spaceUuid,
event.communityId,
projectId);
// Remove from loading set safely
final updatedLoadingIds = {...state.loadingAutomationIds!}..remove(event.automationId);
final updatedLoadingIds = {...state.loadingAutomationIds!}
..remove(event.automationId);
emit(state.copyWith(
automations: updatedAutomations,
loadingAutomationIds: updatedLoadingIds,
));
} else {
final updatedLoadingIds = {...state.loadingAutomationIds!}..remove(event.automationId);
final updatedLoadingIds = {...state.loadingAutomationIds!}
..remove(event.automationId);
emit(state.copyWith(
loadingAutomationIds: updatedLoadingIds,
errorMessage: 'Update failed',
));
}
} catch (e) {
final updatedLoadingIds = {...state.loadingAutomationIds!}..remove(event.automationId);
final updatedLoadingIds = {...state.loadingAutomationIds!}
..remove(event.automationId);
emit(state.copyWith(
loadingAutomationIds: updatedLoadingIds,
errorMessage: 'Update error: ${e.toString()}',

View File

@ -12,22 +12,53 @@ class ConditionToggle extends StatelessWidget {
});
static const _conditions = ["<", "==", ">"];
static const _icons = [
Icons.chevron_left,
Icons.drag_handle,
Icons.chevron_right
];
@override
Widget build(BuildContext context) {
return ToggleButtons(
onPressed: (index) => onChanged(_conditions[index]),
borderRadius: const BorderRadius.all(Radius.circular(8)),
selectedBorderColor: ColorsManager.primaryColorWithOpacity,
selectedColor: Colors.white,
fillColor: ColorsManager.primaryColorWithOpacity,
color: ColorsManager.primaryColorWithOpacity,
constraints: const BoxConstraints(
minHeight: 40.0,
minWidth: 40.0,
final selectedIndex = _conditions.indexOf(currentCondition ?? "==");
return Container(
height: 30,
width: MediaQuery.of(context).size.width * 0.1,
decoration: BoxDecoration(
color: ColorsManager.softGray.withOpacity(0.5),
borderRadius: BorderRadius.circular(50),
),
clipBehavior: Clip.antiAlias,
child: Row(
mainAxisSize: MainAxisSize.min,
children: List.generate(_conditions.length, (index) {
final isSelected = index == selectedIndex;
return Expanded(
child: InkWell(
onTap: () => onChanged(_conditions[index]),
child: AnimatedContainer(
duration: const Duration(milliseconds: 180),
curve: Curves.ease,
decoration: BoxDecoration(
color:
isSelected ? ColorsManager.vividBlue : Colors.transparent,
),
child: Center(
child: Icon(
_icons[index],
size: 20,
color: isSelected
? ColorsManager.whiteColors
: ColorsManager.blackColor,
weight: isSelected ? 700 : 500,
),
),
),
),
);
}),
),
isSelected: _conditions.map((c) => c == (currentCondition ?? "==")).toList(),
children: _conditions.map((c) => Text(c)).toList(),
);
}
}

View File

@ -99,7 +99,27 @@ class _EnergyClampDialogState extends State<EnergyClampDialog> {
mainAxisSize: MainAxisSize.min,
children: [
const DialogHeader('Energy Clamp Conditions'),
Expanded(child: _buildMainContent(context, state)),
Expanded(
child: Visibility(
visible: _functions.isNotEmpty,
replacement: SizedBox(
child: Row(
crossAxisAlignment: CrossAxisAlignment.center,
mainAxisAlignment: MainAxisAlignment.center,
children: [
Center(
child: Text(
'You Cant add\n the Power Clamp to Then Section',
textAlign: TextAlign.center,
style: context.textTheme.bodyMedium!.copyWith(
color: ColorsManager.red,
fontWeight: FontWeight.w400),
)),
],
),
),
child: _buildMainContent(context, state),
)),
_buildDialogFooter(context, state),
],
),

View File

@ -24,6 +24,9 @@ class SpaceTreeBloc extends Bloc<SpaceTreeEvent, SpaceTreeState> {
on<PaginationEvent>(_fetchPaginationSpaces);
on<DebouncedSearchEvent>(_onDebouncedSearch);
on<SpaceTreeClearSelectionEvent>(_onSpaceTreeClearSelectionEvent);
on<AnalyticsClearAllSpaceTreeSelectionsEvent>(
_onAnalyticsClearAllSpaceTreeSelectionsEvent,
);
}
Timer _timer = Timer(const Duration(microseconds: 0), () {});
@ -493,6 +496,20 @@ class SpaceTreeBloc extends Bloc<SpaceTreeEvent, SpaceTreeState> {
);
}
void _onAnalyticsClearAllSpaceTreeSelectionsEvent(
AnalyticsClearAllSpaceTreeSelectionsEvent event,
Emitter<SpaceTreeState> emit,
) async {
emit(
state.copyWith(
selectedCommunities: [],
selectedCommunityAndSpaces: {},
selectedSpaces: [],
soldCheck: [],
),
);
}
@override
Future<void> close() async {
_timer.cancel();

View File

@ -112,3 +112,7 @@ class ClearCachedData extends SpaceTreeEvent {}
class SpaceTreeClearSelectionEvent extends SpaceTreeEvent {
const SpaceTreeClearSelectionEvent();
}
final class AnalyticsClearAllSpaceTreeSelectionsEvent extends SpaceTreeEvent {
const AnalyticsClearAllSpaceTreeSelectionsEvent();
}

View File

@ -48,7 +48,8 @@ class _SpaceTreeViewState extends State<SpaceTreeView> {
@override
Widget build(BuildContext context) {
return BlocBuilder<SpaceTreeBloc, SpaceTreeState>(builder: (context, state) {
return BlocBuilder<SpaceTreeBloc, SpaceTreeState>(
builder: (context, state) {
final communities = state.searchQuery.isNotEmpty
? state.filteredCommunity
: state.communityList;
@ -132,7 +133,8 @@ class _SpaceTreeViewState extends State<SpaceTreeView> {
)
else
CustomSearchBar(
onSearchChanged: (query) => context.read<SpaceTreeBloc>().add(
onSearchChanged: (query) =>
context.read<SpaceTreeBloc>().add(
SearchQueryEvent(query),
),
),
@ -140,6 +142,15 @@ class _SpaceTreeViewState extends State<SpaceTreeView> {
Expanded(
child: state.isSearching
? const Center(child: CircularProgressIndicator())
: communities.isEmpty
? Center(
child: Text(
'No communities found',
style: context.textTheme.bodyMedium?.copyWith(
color: ColorsManager.textGray,
),
),
)
: SidebarCommunitiesList(
onScrollToEnd: () {
if (!state.paginationIsLoading) {
@ -166,7 +177,8 @@ class _SpaceTreeViewState extends State<SpaceTreeView> {
communities[index].uuid,
),
),
isExpanded: state.expandedCommunities.contains(
isExpanded:
state.expandedCommunities.contains(
communities[index].uuid,
),
onItemSelected: () {
@ -182,10 +194,11 @@ class _SpaceTreeViewState extends State<SpaceTreeView> {
(space) {
return CustomExpansionTileSpaceTree(
title: space.name,
isExpanded:
state.expandedSpaces.contains(space.uuid),
isExpanded: state.expandedSpaces
.contains(space.uuid),
onItemSelected: () {
final isParentSelected = _isParentSelected(
final isParentSelected =
_isParentSelected(
state,
communities[index],
space,
@ -213,9 +226,10 @@ class _SpaceTreeViewState extends State<SpaceTreeView> {
),
isSelected: state.selectedSpaces
.contains(space.uuid) ||
state.soldCheck.contains(space.uuid),
isSoldCheck:
state.soldCheck.contains(space.uuid),
state.soldCheck
.contains(space.uuid),
isSoldCheck: state.soldCheck
.contains(space.uuid),
children: _buildNestedSpaces(
context,
state,
@ -229,7 +243,8 @@ class _SpaceTreeViewState extends State<SpaceTreeView> {
},
),
),
if (state.paginationIsLoading) const CircularProgressIndicator(),
if (state.paginationIsLoading)
const CircularProgressIndicator(),
],
),
);

View File

@ -0,0 +1,10 @@
class APIException implements Exception {
final String message;
APIException(this.message);
@override
String toString() {
return message;
}
}

View File

@ -1,10 +1,13 @@
import 'package:dio/dio.dart';
import 'package:syncrow_web/pages/auth/model/region_model.dart';
import 'package:syncrow_web/pages/auth/model/token.dart';
import 'package:syncrow_web/services/api/api_exception.dart';
import 'package:syncrow_web/services/api/http_service.dart';
import 'package:syncrow_web/utils/constants/api_const.dart';
class AuthenticationAPI {
static Future<Token> loginWithEmail({required var model}) async {
try {
final response = await HTTPService().post(
path: ApiEndpoints.login,
body: model.toJson(),
@ -13,6 +16,11 @@ class AuthenticationAPI {
return Token.fromJson(json['data']);
});
return response;
} on DioException catch (e) {
final message = e.response?.data['error']['message'] ??
'An error occurred while logging in';
throw APIException(message);
}
}
static Future forgetPassword({
@ -20,12 +28,18 @@ class AuthenticationAPI {
required var password,
required var otpCode,
}) async {
try {
final response = await HTTPService().post(
path: ApiEndpoints.forgetPassword,
body: {"email": email, "password": password, "otpCode": otpCode},
showServerMessage: true,
expectedResponseModel: (json) {});
return response;
} on DioException catch (e) {
final message = e.response?.data['error']['message'] ??
'An error occurred while resetting the password';
throw APIException(message);
}
}
static Future<int?> sendOtp({required String email}) async {
@ -39,7 +53,9 @@ class AuthenticationAPI {
return response;
}
static Future verifyOtp({required String email, required String otpCode}) async {
static Future verifyOtp(
{required String email, required String otpCode}) async {
try {
final response = await HTTPService().post(
path: ApiEndpoints.verifyOtp,
body: {"email": email, "type": "PASSWORD", "otpCode": otpCode},
@ -52,6 +68,11 @@ class AuthenticationAPI {
}
});
return response;
} on APIException catch (e) {
throw APIException(e.message);
} catch (e) {
throw APIException('An error occurred while verifying the OTP');
}
}
static Future<List<RegionModel>> fetchRegion() async {
@ -59,7 +80,9 @@ class AuthenticationAPI {
path: ApiEndpoints.getRegion,
showServerMessage: true,
expectedResponseModel: (json) {
return (json as List).map((zone) => RegionModel.fromJson(zone)).toList();
return (json as List)
.map((zone) => RegionModel.fromJson(zone))
.toList();
});
return response;
}

View File

@ -1,3 +1,4 @@
import 'package:dio/dio.dart';
import 'package:flutter/material.dart';
import 'package:syncrow_web/pages/routines/bloc/automation_scene_trigger_bloc/automation_status_update.dart';
import 'package:syncrow_web/pages/routines/models/create_scene_and_autoamtion/create_automation_model.dart';
@ -5,6 +6,7 @@ import 'package:syncrow_web/pages/routines/models/create_scene_and_autoamtion/cr
import 'package:syncrow_web/pages/routines/models/icon_model.dart';
import 'package:syncrow_web/pages/routines/models/routine_details_model.dart';
import 'package:syncrow_web/pages/routines/models/routine_model.dart';
import 'package:syncrow_web/services/api/api_exception.dart';
import 'package:syncrow_web/services/api/http_service.dart';
import 'package:syncrow_web/utils/constants/api_const.dart';
@ -26,9 +28,10 @@ class SceneApi {
);
debugPrint('create scene response: $response');
return response;
} catch (e) {
debugPrint(e.toString());
rethrow;
} on DioException catch (e) {
String errorMessage =
e.response?.data['error']['message'][0] ?? 'something went wrong';
throw APIException(errorMessage);
}
}
@ -48,9 +51,10 @@ class SceneApi {
);
debugPrint('create automation response: $response');
return response;
} catch (e) {
debugPrint(e.toString());
rethrow;
} on DioException catch (e) {
String errorMessage =
e.response?.data['error']['message'][0] ?? 'something went wrong';
throw APIException(errorMessage);
}
}
@ -165,8 +169,10 @@ class SceneApi {
},
);
return response;
} catch (e) {
rethrow;
} on DioException catch (e) {
String errorMessage =
e.response?.data['error']['message'][0] ?? 'something went wrong';
throw APIException(errorMessage);
}
}
@ -185,8 +191,10 @@ class SceneApi {
},
);
return response;
} catch (e) {
rethrow;
} on DioException catch (e) {
String errorMessage =
e.response?.data['error']['message'][0] ?? 'something went wrong';
throw APIException(errorMessage);
}
}
@ -217,8 +225,10 @@ class SceneApi {
expectedResponseModel: (json) => json['statusCode'] == 200,
);
return response;
} catch (e) {
rethrow;
} on DioException catch (e) {
String errorMessage =
e.response?.data['error']['message'][0] ?? 'something went wrong';
throw APIException(errorMessage);
}
}
@ -236,8 +246,10 @@ class SceneApi {
expectedResponseModel: (json) => json['statusCode'] == 200,
);
return response;
} catch (e) {
rethrow;
} on DioException catch (e) {
String errorMessage =
e.response?.data['error']['message'][0] ?? 'something went wrong';
throw APIException(errorMessage);
}
}