Compare commits

..

23 Commits

Author SHA1 Message Date
8e11749ed7 Prepared for aqi distribution API Integration. 2025-06-02 16:13:58 +03:00
7bc9079212 reverted a comment. 2025-06-02 14:30:07 +03:00
97801872e0 Implemented an initial remote implementation of RangeOfAqiService. 2025-06-02 14:29:04 +03:00
fa9210f387 added fromJson factory methods to RangeOfAqi, and to RangeOfAqiValue data models. 2025-06-02 14:28:50 +03:00
57b6f01177 SP-1593 Implemented the agreed upon api contract. 2025-06-02 14:26:47 +03:00
066f967cd1 shows tooltip with data. 2025-06-01 14:28:40 +03:00
e28f3c3c03 reduced bar width size. 2025-06-01 14:28:40 +03:00
2be15e648a added loading widget to AqiDistributionChartTitle. 2025-06-01 14:28:40 +03:00
2e12d73151 randomize generated fake data in FakeAirQualityDistributionService. 2025-06-01 14:28:40 +03:00
c50ed693ae loads and clears aqi distribution in FetchAirQualityDataHelper. 2025-06-01 14:28:40 +03:00
8dc7d2b3d0 Connected AirQualityDistributionBloc into AqiDistributionChartBox. 2025-06-01 14:28:40 +03:00
accafb150e . 2025-06-01 14:24:07 +03:00
736e0c3d9c Injected AirQualityDistributionBloc into AnalyticsPage. 2025-06-01 14:23:14 +03:00
455d9c1f01 Created AirQualityDistributionBloc. 2025-06-01 14:22:25 +03:00
4479ed04b7 Created a AirQualityDistributionService along with its fake implementation. 2025-06-01 14:22:25 +03:00
286dea3f51 created a GetAirQualityDistributionParam. 2025-06-01 14:22:25 +03:00
44c4648941 made the first element of the bar rods to have only a top sides radius to match the design. 2025-06-01 14:22:25 +03:00
ca1feb9600 made charts based on states and not based on metrics. 2025-06-01 14:22:25 +03:00
7b31914e1c made progress towards aqi distribution chart. 2025-06-01 14:22:25 +03:00
10f35d3747 added more mock data to AqiDistributionChart. 2025-06-01 14:22:25 +03:00
1998a629b6 added some opacity to metric colors. 2025-06-01 14:22:25 +03:00
5940e52826 Implemented an initial version of AqiDistributionChart. 2025-06-01 14:22:25 +03:00
7c55e8bbf9 Prepared widgets for the aqi distribution chart. 2025-06-01 14:22:25 +03:00
42 changed files with 1240 additions and 563 deletions

3
.vscode/launch.json vendored
View File

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

View File

@ -0,0 +1,57 @@
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,18 +1,49 @@
import 'package:equatable/equatable.dart'; import 'package:equatable/equatable.dart';
class RangeOfAqi extends Equatable { class RangeOfAqi extends Equatable {
final double min;
final double avg;
final double max;
final DateTime date; final DateTime date;
final List<RangeOfAqiValue> data;
const RangeOfAqi({ const RangeOfAqi({
required this.min, required this.data,
required this.avg,
required this.max,
required this.date, 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 @override
List<Object?> get props => [min, avg, max, date]; 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];
} }

View File

@ -0,0 +1,81 @@
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

@ -0,0 +1,30 @@
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

@ -0,0 +1,43 @@
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,6 +1,7 @@
import 'package:bloc/bloc.dart'; import 'package:bloc/bloc.dart';
import 'package:equatable/equatable.dart'; import 'package:equatable/equatable.dart';
import 'package:syncrow_web/pages/analytics/models/range_of_aqi.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/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/pages/analytics/services/range_of_aqi/range_of_aqi_service.dart';
@ -11,6 +12,7 @@ class RangeOfAqiBloc extends Bloc<RangeOfAqiEvent, RangeOfAqiState> {
RangeOfAqiBloc(this._rangeOfAqiService) : super(const RangeOfAqiState()) { RangeOfAqiBloc(this._rangeOfAqiService) : super(const RangeOfAqiState()) {
on<LoadRangeOfAqiEvent>(_onLoadRangeOfAqiEvent); on<LoadRangeOfAqiEvent>(_onLoadRangeOfAqiEvent);
on<ClearRangeOfAqiEvent>(_onClearRangeOfAqiEvent); on<ClearRangeOfAqiEvent>(_onClearRangeOfAqiEvent);
on<UpdateAqiTypeEvent>(_onUpdateAqiTypeEvent);
} }
final RangeOfAqiService _rangeOfAqiService; final RangeOfAqiService _rangeOfAqiService;
@ -20,19 +22,55 @@ class RangeOfAqiBloc extends Bloc<RangeOfAqiEvent, RangeOfAqiState> {
Emitter<RangeOfAqiState> emit, Emitter<RangeOfAqiState> emit,
) async { ) async {
emit( emit(
RangeOfAqiState( state.copyWith(status: RangeOfAqiStatus.loading),
status: RangeOfAqiStatus.loading,
rangeOfAqi: state.rangeOfAqi,
),
); );
try { try {
final rangeOfAqi = await _rangeOfAqiService.load(event.param); final rangeOfAqi = await _rangeOfAqiService.load(event.param);
emit(RangeOfAqiState(status: RangeOfAqiStatus.loaded, rangeOfAqi: rangeOfAqi)); emit(
state.copyWith(
status: RangeOfAqiStatus.loaded,
rangeOfAqi: rangeOfAqi,
filteredRangeOfAqi: _arrangeChartDataByType(
rangeOfAqi,
state.selectedAqiType,
),
),
);
} catch (e) { } catch (e) {
emit(RangeOfAqiState(status: RangeOfAqiStatus.failure, errorMessage: '$e')); emit(
state.copyWith(
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( void _onClearRangeOfAqiEvent(
ClearRangeOfAqiEvent event, ClearRangeOfAqiEvent event,
Emitter<RangeOfAqiState> emit, Emitter<RangeOfAqiState> emit,

View File

@ -16,6 +16,15 @@ class LoadRangeOfAqiEvent extends RangeOfAqiEvent {
List<Object> get props => [param]; 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 { class ClearRangeOfAqiEvent extends RangeOfAqiEvent {
const ClearRangeOfAqiEvent(); const ClearRangeOfAqiEvent();
} }

View File

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

View File

@ -1,10 +1,11 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_bloc/flutter_bloc.dart';
import 'package: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/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_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/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/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_analytics_devices_param.dart';
import 'package:syncrow_web/pages/analytics/params/get_range_of_aqi_param.dart'; import 'package:syncrow_web/pages/analytics/params/get_range_of_aqi_param.dart';
@ -13,8 +14,10 @@ abstract final class FetchAirQualityDataHelper {
static void loadAirQualityData( static void loadAirQualityData(
BuildContext context, { BuildContext context, {
required DateTime date,
required String communityUuid, required String communityUuid,
required String spaceUuid, required String spaceUuid,
bool shouldFetchAnalyticsDevices = true,
}) { }) {
final date = context.read<AnalyticsDatePickerBloc>().state.monthlyDate; final date = context.read<AnalyticsDatePickerBloc>().state.monthlyDate;
loadAnalyticsDevices( loadAnalyticsDevices(
@ -26,7 +29,11 @@ abstract final class FetchAirQualityDataHelper {
context, context,
spaceUuid: spaceUuid, spaceUuid: spaceUuid,
date: date, date: date,
aqiType: AqiType.aqi, );
loadAirQualityDistribution(
context,
spaceUuid: spaceUuid,
date: date,
); );
} }
@ -37,7 +44,9 @@ abstract final class FetchAirQualityDataHelper {
context.read<RealtimeDeviceChangesBloc>().add( context.read<RealtimeDeviceChangesBloc>().add(
const RealtimeDeviceChangesClosed(), const RealtimeDeviceChangesClosed(),
); );
context.read<AirQualityDistributionBloc>().add(
const ClearAirQualityDistribution(),
);
context.read<RangeOfAqiBloc>().add(const ClearRangeOfAqiEvent()); context.read<RangeOfAqiBloc>().add(const ClearRangeOfAqiEvent());
} }
@ -67,16 +76,26 @@ abstract final class FetchAirQualityDataHelper {
BuildContext context, { BuildContext context, {
required String spaceUuid, required String spaceUuid,
required DateTime date, required DateTime date,
required AqiType aqiType,
}) { }) {
context.read<RangeOfAqiBloc>().add( context.read<RangeOfAqiBloc>().add(
LoadRangeOfAqiEvent( LoadRangeOfAqiEvent(
GetRangeOfAqiParam( GetRangeOfAqiParam(
date: date, date: date,
spaceUuid: spaceUuid, 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,5 +1,6 @@
import 'package:flutter/material.dart'; 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/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'; import 'package:syncrow_web/pages/analytics/modules/air_quality/widgets/range_of_aqi_chart_box.dart';
class AirQualityView extends StatelessWidget { class AirQualityView extends StatelessWidget {
@ -23,8 +24,14 @@ class AirQualityView extends StatelessWidget {
height: height * 1.2, height: height * 1.2,
child: const AirQualityEndSideWidget(), child: const AirQualityEndSideWidget(),
), ),
SizedBox(height: height * 0.5, child: const RangeOfAqiChartBox()), SizedBox(
SizedBox(height: height * 0.5, child: const Placeholder()), height: height * 0.5,
child: const RangeOfAqiChartBox(),
),
SizedBox(
height: height * 0.5,
child: const AqiDistributionChartBox(),
),
], ],
), ),
); );
@ -46,7 +53,7 @@ class AirQualityView extends StatelessWidget {
spacing: 20, spacing: 20,
children: [ children: [
Expanded(child: RangeOfAqiChartBox()), Expanded(child: RangeOfAqiChartBox()),
Expanded(child: Placeholder()), Expanded(child: AqiDistributionChartBox()),
], ],
), ),
), ),

View File

@ -0,0 +1,174 @@
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

@ -0,0 +1,44 @@
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

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

View File

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

View File

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

View File

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

View File

@ -1,6 +1,7 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:syncrow_web/pages/analytics/modules/air_quality/helpers/fetch_air_quality_data_helper.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/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_bloc.dart';
import 'package:syncrow_web/pages/space_tree/bloc/space_tree_event.dart'; import 'package:syncrow_web/pages/space_tree/bloc/space_tree_event.dart';
@ -26,10 +27,10 @@ final class AirQualityDataLoadingStrategy implements AnalyticsDataLoadingStrateg
final spaceTreeBloc = context.read<SpaceTreeBloc>(); final spaceTreeBloc = context.read<SpaceTreeBloc>();
final isSpaceSelected = spaceTreeBloc.state.selectedSpaces.contains(space.uuid); final isSpaceSelected = spaceTreeBloc.state.selectedSpaces.contains(space.uuid);
final hasSelectedSpaces = spaceTreeBloc.state.selectedSpaces.isNotEmpty; if (isSpaceSelected) {
if (hasSelectedSpaces) clearData(context); clearData(context);
return;
if (isSpaceSelected) return; }
spaceTreeBloc spaceTreeBloc
..add(const SpaceTreeClearSelectionEvent()) ..add(const SpaceTreeClearSelectionEvent())
@ -39,14 +40,22 @@ final class AirQualityDataLoadingStrategy implements AnalyticsDataLoadingStrateg
context, context,
communityUuid: community.uuid, communityUuid: community.uuid,
spaceUuid: space.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 @override
void clearData(BuildContext context) { void clearData(BuildContext context) {
context.read<SpaceTreeBloc>().add( context.read<SpaceTreeBloc>().add(const SpaceTreeClearSelectionEvent());
const AnalyticsClearAllSpaceTreeSelectionsEvent(),
);
FetchAirQualityDataHelper.clearAllData(context); FetchAirQualityDataHelper.clearAllData(context);
} }
} }

View File

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

View File

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

View File

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

View File

@ -1,5 +1,6 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_bloc/flutter_bloc.dart';
import 'package: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/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_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/analytics/blocs/analytics_devices/analytics_devices_bloc.dart';
@ -13,6 +14,7 @@ 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/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/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/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/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_energy_management_analytics_devices_service.dart';
import 'package:syncrow_web/pages/analytics/services/analytics_devices/remote_occupancy_analytics_devices_service.dart'; import 'package:syncrow_web/pages/analytics/services/analytics_devices/remote_occupancy_analytics_devices_service.dart';
@ -101,6 +103,11 @@ class _AnalyticsPageState extends State<AnalyticsPage> {
FakeRangeOfAqiService(), FakeRangeOfAqiService(),
), ),
), ),
BlocProvider(
create: (context) => AirQualityDistributionBloc(
FakeAirQualityDistributionService(),
),
),
], ],
child: const AnalyticsPageForm(), child: const AnalyticsPageForm(),
); );

View File

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

View File

@ -1,5 +1,6 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_bloc/flutter_bloc.dart';
import 'package: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_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/blocs/analytics_tab/analytics_tab_bloc.dart';
import 'package:syncrow_web/pages/analytics/modules/analytics/enums/analytics_page_tab.dart'; import 'package:syncrow_web/pages/analytics/modules/analytics/enums/analytics_page_tab.dart';
@ -56,33 +57,16 @@ class AnalyticsPageTabsAndChildren extends StatelessWidget {
const Spacer(), const Spacer(),
Visibility( Visibility(
key: ValueKey(selectedTab), key: ValueKey(selectedTab),
visible: selectedTab == AnalyticsPageTab.energyManagement, visible: selectedTab == AnalyticsPageTab.energyManagement ||
selectedTab == AnalyticsPageTab.airQuality,
child: Expanded( child: Expanded(
flex: 2, flex: 2,
child: FittedBox( child: FittedBox(
fit: BoxFit.scaleDown, fit: BoxFit.scaleDown,
alignment: AlignmentDirectional.centerEnd, alignment: AlignmentDirectional.centerEnd,
child: AnalyticsDateFilterButton( child: AnalyticsDateFilterButton(
onDateSelected: (DateTime value) { onDateSelected: (value) {
context.read<AnalyticsDatePickerBloc>().add( _onDateChanged(context, value, selectedTab);
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 selectedDate: context
.watch<AnalyticsDatePickerBloc>() .watch<AnalyticsDatePickerBloc>()
@ -112,4 +96,73 @@ class AnalyticsPageTabsAndChildren extends StatelessWidget {
child: child, 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

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

View File

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

View File

@ -0,0 +1,8 @@
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

@ -0,0 +1,95 @@
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

@ -0,0 +1,36 @@
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,4 +1,5 @@
import 'package:syncrow_web/pages/analytics/models/range_of_aqi.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/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/pages/analytics/services/range_of_aqi/range_of_aqi_service.dart';
@ -19,9 +20,14 @@ class FakeRangeOfAqiService implements RangeOfAqiService {
final max = (avg + maxDelta).clamp(0.0, 301.0); final max = (avg + maxDelta).clamp(0.0, 301.0);
return RangeOfAqi( return RangeOfAqi(
min: min, data: [
avg: avg, RangeOfAqiValue(type: AqiType.aqi.code, min: min, average: avg, max: max),
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),
],
date: date, date: date,
); );
}); });

View File

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

View File

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

View File

@ -12,53 +12,22 @@ class ConditionToggle extends StatelessWidget {
}); });
static const _conditions = ["<", "==", ">"]; static const _conditions = ["<", "==", ">"];
static const _icons = [
Icons.chevron_left,
Icons.drag_handle,
Icons.chevron_right
];
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final selectedIndex = _conditions.indexOf(currentCondition ?? "=="); return ToggleButtons(
onPressed: (index) => onChanged(_conditions[index]),
return Container( borderRadius: const BorderRadius.all(Radius.circular(8)),
height: 30, selectedBorderColor: ColorsManager.primaryColorWithOpacity,
width: MediaQuery.of(context).size.width * 0.1, selectedColor: Colors.white,
decoration: BoxDecoration( fillColor: ColorsManager.primaryColorWithOpacity,
color: ColorsManager.softGray.withOpacity(0.5), color: ColorsManager.primaryColorWithOpacity,
borderRadius: BorderRadius.circular(50), constraints: const BoxConstraints(
), minHeight: 40.0,
clipBehavior: Clip.antiAlias, minWidth: 40.0,
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,27 +99,7 @@ class _EnergyClampDialogState extends State<EnergyClampDialog> {
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
children: [ children: [
const DialogHeader('Energy Clamp Conditions'), const DialogHeader('Energy Clamp Conditions'),
Expanded( Expanded(child: _buildMainContent(context, state)),
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), _buildDialogFooter(context, state),
], ],
), ),

View File

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

View File

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

View File

@ -48,8 +48,7 @@ class _SpaceTreeViewState extends State<SpaceTreeView> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return BlocBuilder<SpaceTreeBloc, SpaceTreeState>( return BlocBuilder<SpaceTreeBloc, SpaceTreeState>(builder: (context, state) {
builder: (context, state) {
final communities = state.searchQuery.isNotEmpty final communities = state.searchQuery.isNotEmpty
? state.filteredCommunity ? state.filteredCommunity
: state.communityList; : state.communityList;
@ -133,118 +132,104 @@ class _SpaceTreeViewState extends State<SpaceTreeView> {
) )
else else
CustomSearchBar( CustomSearchBar(
onSearchChanged: (query) => onSearchChanged: (query) => context.read<SpaceTreeBloc>().add(
context.read<SpaceTreeBloc>().add( SearchQueryEvent(query),
SearchQueryEvent(query), ),
),
), ),
const SizedBox(height: 16), const SizedBox(height: 16),
Expanded( Expanded(
child: state.isSearching child: state.isSearching
? const Center(child: CircularProgressIndicator()) ? const Center(child: CircularProgressIndicator())
: communities.isEmpty : SidebarCommunitiesList(
? Center( onScrollToEnd: () {
child: Text( if (!state.paginationIsLoading) {
'No communities found', context.read<SpaceTreeBloc>().add(
style: context.textTheme.bodyMedium?.copyWith( PaginationEvent(
color: ColorsManager.textGray, state.paginationModel,
), state.communityList,
), ),
) );
: SidebarCommunitiesList( }
onScrollToEnd: () { },
if (!state.paginationIsLoading) { scrollController: _scrollController,
communities: communities,
itemBuilder: (context, index) {
return CustomExpansionTileSpaceTree(
title: communities[index].name,
isSelected: state.selectedCommunities
.contains(communities[index].uuid),
isSoldCheck: state.selectedCommunities
.contains(communities[index].uuid),
onExpansionChanged: () =>
context.read<SpaceTreeBloc>().add( context.read<SpaceTreeBloc>().add(
PaginationEvent( OnCommunityExpanded(
state.paginationModel, communities[index].uuid,
state.communityList,
), ),
); ),
} isExpanded: state.expandedCommunities.contains(
communities[index].uuid,
),
onItemSelected: () {
widget.onSelect();
context.read<SpaceTreeBloc>().add(
OnCommunitySelected(
communities[index].uuid,
communities[index].spaces,
),
);
}, },
scrollController: _scrollController, children: communities[index].spaces.map(
communities: communities, (space) {
itemBuilder: (context, index) { return CustomExpansionTileSpaceTree(
return CustomExpansionTileSpaceTree( title: space.name,
title: communities[index].name, isExpanded:
isSelected: state.selectedCommunities state.expandedSpaces.contains(space.uuid),
.contains(communities[index].uuid), onItemSelected: () {
isSoldCheck: state.selectedCommunities final isParentSelected = _isParentSelected(
.contains(communities[index].uuid), state,
onExpansionChanged: () => communities[index],
space,
);
if (widget
.shouldDisableDeselectingChildrenOfSelectedParent &&
isParentSelected) {
return;
}
widget.onSelect();
context.read<SpaceTreeBloc>().add( context.read<SpaceTreeBloc>().add(
OnCommunityExpanded( OnSpaceSelected(
communities[index].uuid, communities[index],
space.uuid ?? '',
space.children,
), ),
),
isExpanded:
state.expandedCommunities.contains(
communities[index].uuid,
),
onItemSelected: () {
widget.onSelect();
context.read<SpaceTreeBloc>().add(
OnCommunitySelected(
communities[index].uuid,
communities[index].spaces,
),
);
},
children: communities[index].spaces.map(
(space) {
return CustomExpansionTileSpaceTree(
title: space.name,
isExpanded: state.expandedSpaces
.contains(space.uuid),
onItemSelected: () {
final isParentSelected =
_isParentSelected(
state,
communities[index],
space,
); );
if (widget
.shouldDisableDeselectingChildrenOfSelectedParent &&
isParentSelected) {
return;
}
widget.onSelect();
context.read<SpaceTreeBloc>().add(
OnSpaceSelected(
communities[index],
space.uuid ?? '',
space.children,
),
);
},
onExpansionChanged: () =>
context.read<SpaceTreeBloc>().add(
OnSpaceExpanded(
communities[index].uuid,
space.uuid ?? '',
),
),
isSelected: state.selectedSpaces
.contains(space.uuid) ||
state.soldCheck
.contains(space.uuid),
isSoldCheck: state.soldCheck
.contains(space.uuid),
children: _buildNestedSpaces(
context,
state,
space,
communities[index],
),
);
}, },
).toList(), onExpansionChanged: () =>
); context.read<SpaceTreeBloc>().add(
}, OnSpaceExpanded(
), communities[index].uuid,
space.uuid ?? '',
),
),
isSelected: state.selectedSpaces
.contains(space.uuid) ||
state.soldCheck.contains(space.uuid),
isSoldCheck:
state.soldCheck.contains(space.uuid),
children: _buildNestedSpaces(
context,
state,
space,
communities[index],
),
);
},
).toList(),
);
},
),
), ),
if (state.paginationIsLoading) if (state.paginationIsLoading) const CircularProgressIndicator(),
const CircularProgressIndicator(),
], ],
), ),
); );

View File

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

View File

@ -1,26 +1,18 @@
import 'package:dio/dio.dart';
import 'package:syncrow_web/pages/auth/model/region_model.dart'; import 'package:syncrow_web/pages/auth/model/region_model.dart';
import 'package:syncrow_web/pages/auth/model/token.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/services/api/http_service.dart';
import 'package:syncrow_web/utils/constants/api_const.dart'; import 'package:syncrow_web/utils/constants/api_const.dart';
class AuthenticationAPI { class AuthenticationAPI {
static Future<Token> loginWithEmail({required var model}) async { static Future<Token> loginWithEmail({required var model}) async {
try { final response = await HTTPService().post(
final response = await HTTPService().post( path: ApiEndpoints.login,
path: ApiEndpoints.login, body: model.toJson(),
body: model.toJson(), showServerMessage: true,
showServerMessage: true, expectedResponseModel: (json) {
expectedResponseModel: (json) { return Token.fromJson(json['data']);
return Token.fromJson(json['data']); });
}); return response;
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({ static Future forgetPassword({
@ -28,18 +20,12 @@ class AuthenticationAPI {
required var password, required var password,
required var otpCode, required var otpCode,
}) async { }) async {
try { final response = await HTTPService().post(
final response = await HTTPService().post( path: ApiEndpoints.forgetPassword,
path: ApiEndpoints.forgetPassword, body: {"email": email, "password": password, "otpCode": otpCode},
body: {"email": email, "password": password, "otpCode": otpCode}, showServerMessage: true,
showServerMessage: true, expectedResponseModel: (json) {});
expectedResponseModel: (json) {}); return response;
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 { static Future<int?> sendOtp({required String email}) async {
@ -53,26 +39,19 @@ class AuthenticationAPI {
return response; return response;
} }
static Future verifyOtp( static Future verifyOtp({required String email, required String otpCode}) async {
{required String email, required String otpCode}) async { final response = await HTTPService().post(
try { path: ApiEndpoints.verifyOtp,
final response = await HTTPService().post( body: {"email": email, "type": "PASSWORD", "otpCode": otpCode},
path: ApiEndpoints.verifyOtp, showServerMessage: true,
body: {"email": email, "type": "PASSWORD", "otpCode": otpCode}, expectedResponseModel: (json) {
showServerMessage: true, if (json['message'] == 'Otp Verified Successfully') {
expectedResponseModel: (json) { return true;
if (json['message'] == 'Otp Verified Successfully') { } else {
return true; return false;
} else { }
return false; });
} return response;
});
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 { static Future<List<RegionModel>> fetchRegion() async {
@ -80,9 +59,7 @@ class AuthenticationAPI {
path: ApiEndpoints.getRegion, path: ApiEndpoints.getRegion,
showServerMessage: true, showServerMessage: true,
expectedResponseModel: (json) { expectedResponseModel: (json) {
return (json as List) return (json as List).map((zone) => RegionModel.fromJson(zone)).toList();
.map((zone) => RegionModel.fromJson(zone))
.toList();
}); });
return response; return response;
} }

View File

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