Compare commits

..

2 Commits

37 changed files with 325 additions and 211 deletions

View File

@ -15,9 +15,7 @@ class AirQualityDataModel extends Equatable {
return AirQualityDataModel(
date: DateTime.parse(json['date'] as String),
data: (json['data'] as List<dynamic>)
.map(
(e) => AirQualityPercentageData.fromJson(e as Map<String, dynamic>),
)
.map((e) => AirQualityPercentageData.fromJson(e as Map<String, dynamic>))
.toList(),
);
}
@ -25,9 +23,9 @@ class AirQualityDataModel extends Equatable {
static final Map<String, Color> metricColors = {
'good': ColorsManager.goodGreen.withValues(alpha: 0.7),
'moderate': ColorsManager.moderateYellow.withValues(alpha: 0.7),
'unhealthy_sensitive': ColorsManager.poorOrange.withValues(alpha: 0.7),
'poor': ColorsManager.poorOrange.withValues(alpha: 0.7),
'unhealthy': ColorsManager.unhealthyRed.withValues(alpha: 0.7),
'very_unhealthy': ColorsManager.severePink.withValues(alpha: 0.7),
'severe': ColorsManager.severePink.withValues(alpha: 0.7),
'hazardous': ColorsManager.hazardousPurple.withValues(alpha: 0.7),
};
@ -38,19 +36,22 @@ class AirQualityDataModel extends Equatable {
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, percentage];
List<Object?> get props => [type, name, percentage];
}

View File

@ -33,6 +33,7 @@ class AirQualityDistributionBloc
state.copyWith(
status: AirQualityDistributionStatus.success,
chartData: result,
filteredChartData: _arrangeChartDataByType(result, state.selectedAqiType),
),
);
} catch (e) {
@ -57,6 +58,24 @@ class AirQualityDistributionBloc
UpdateAqiTypeEvent event,
Emitter<AirQualityDistributionState> emit,
) {
emit(state.copyWith(selectedAqiType: event.aqiType));
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

@ -11,24 +11,28 @@ 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,
);

View File

@ -3,7 +3,6 @@ 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/device_location/device_location_bloc.dart';
import 'package:syncrow_web/pages/analytics/modules/air_quality/blocs/range_of_aqi/range_of_aqi_bloc.dart';
import 'package:syncrow_web/pages/analytics/modules/air_quality/widgets/aqi_type_dropdown.dart';
import 'package:syncrow_web/pages/analytics/modules/analytics/blocs/analytics_date_picker_bloc/analytics_date_picker_bloc.dart';
import 'package:syncrow_web/pages/analytics/modules/analytics/blocs/analytics_devices/analytics_devices_bloc.dart';
import 'package:syncrow_web/pages/analytics/modules/energy_management/blocs/realtime_device_changes/realtime_device_changes_bloc.dart';
@ -23,7 +22,6 @@ abstract final class FetchAirQualityDataHelper {
bool shouldFetchAnalyticsDevices = true,
}) {
final date = context.read<AnalyticsDatePickerBloc>().state.monthlyDate;
final aqiType = context.read<AirQualityDistributionBloc>().state.selectedAqiType;
loadAnalyticsDevices(
context,
communityUuid: communityUuid,
@ -38,7 +36,6 @@ abstract final class FetchAirQualityDataHelper {
context,
spaceUuid: spaceUuid,
date: date,
aqiType: aqiType,
);
}
@ -107,15 +104,10 @@ abstract final class FetchAirQualityDataHelper {
BuildContext context, {
required String spaceUuid,
required DateTime date,
required AqiType aqiType,
}) {
context.read<AirQualityDistributionBloc>().add(
LoadAirQualityDistribution(
GetAirQualityDistributionParam(
spaceUuid: spaceUuid,
date: date,
aqiType: aqiType,
),
GetAirQualityDistributionParam(spaceUuid: spaceUuid, date: date),
),
);
}

View File

@ -16,6 +16,11 @@ class AqiDistributionChart extends StatelessWidget {
@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,
@ -25,25 +30,29 @@ class AqiDistributionChart extends StatelessWidget {
borderData: EnergyManagementChartsHelper.borderData(),
barTouchData: _barTouchData(context),
titlesData: _titlesData(context),
barGroups: _buildBarGroups(),
barGroups: _buildBarGroups(sortedData),
),
duration: Duration.zero,
);
}
List<BarChartGroupData> _buildBarGroups() {
return List.generate(chartData.length, (index) {
final data = chartData[index];
List<BarChartGroupData> _buildBarGroups(List<AirQualityDataModel> sortedData) {
return List.generate(sortedData.length, (index) {
final data = sortedData[index];
final stackItems = <BarChartRodData>[];
double currentY = 0;
var isFirstElement = true;
bool isFirstElement = true;
for (final percentageData in data.data) {
// 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.type],
toY: currentY + percentageData.percentage ,
color: AirQualityDataModel.metricColors[percentageData.name]!,
borderRadius: isFirstElement
? const BorderRadius.only(
topLeft: Radius.circular(22),
@ -75,21 +84,23 @@ class AqiDistributionChart extends StatelessWidget {
tooltipRoundedRadius: 16,
tooltipPadding: const EdgeInsets.all(8),
getTooltipItem: (group, groupIndex, rod, rodIndex) {
final data = chartData[group.x];
final data = chartData[group.x.toInt()];
final children = <TextSpan>[];
final List<TextSpan> children = [];
final textStyle = context.textTheme.bodySmall?.copyWith(
color: ColorsManager.blackColor,
fontSize: 8,
fontSize: 12,
);
for (final percentageData in data.data) {
final percentage = percentageData.percentage.toStringAsFixed(1);
final type = percentageData.type[0].toUpperCase() +
percentageData.type.substring(1).replaceAll('_', ' ');
// 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$type: $percentage%',
text:
'\n${percentageData.type.toUpperCase()}: ${percentageData.percentage.toStringAsFixed(1)}%',
style: textStyle,
));
}
@ -98,10 +109,9 @@ class AqiDistributionChart extends StatelessWidget {
DateFormat('dd/MM/yyyy').format(data.date),
context.textTheme.bodyMedium!.copyWith(
color: ColorsManager.blackColor,
fontSize: 9,
fontSize: 16,
fontWeight: FontWeight.w600,
),
textAlign: TextAlign.start,
children: children,
);
},

View File

@ -33,7 +33,7 @@ class AqiDistributionChartBox extends StatelessWidget {
const Divider(),
const SizedBox(height: 20),
Expanded(
child: AqiDistributionChart(chartData: state.chartData),
child: AqiDistributionChart(chartData: state.filteredChartData),
),
],
),

View File

@ -2,11 +2,8 @@ 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/analytics/blocs/analytics_date_picker_bloc/analytics_date_picker_bloc.dart';
import 'package:syncrow_web/pages/analytics/modules/energy_management/widgets/chart_title.dart';
import 'package:syncrow_web/pages/analytics/params/get_air_quality_distribution_param.dart';
import 'package:syncrow_web/pages/analytics/widgets/charts_loading_widget.dart';
import 'package:syncrow_web/pages/space_tree/bloc/space_tree_bloc.dart';
class AqiDistributionChartTitle extends StatelessWidget {
const AqiDistributionChartTitle({required this.isLoading, super.key});
@ -34,15 +31,9 @@ class AqiDistributionChartTitle extends StatelessWidget {
child: AqiTypeDropdown(
onChanged: (value) {
if (value != null) {
final bloc = context.read<AirQualityDistributionBloc>();
try {
final param = _makeLoadAqiDistributionParam(context, value);
bloc.add(LoadAirQualityDistribution(param));
} catch (_) {
return;
} finally {
bloc.add(UpdateAqiTypeEvent(value));
}
context
.read<AirQualityDistributionBloc>()
.add(UpdateAqiTypeEvent(value));
}
},
),
@ -50,19 +41,4 @@ class AqiDistributionChartTitle extends StatelessWidget {
],
);
}
GetAirQualityDistributionParam _makeLoadAqiDistributionParam(
BuildContext context,
AqiType aqiType,
) {
final date = context.read<AnalyticsDatePickerBloc>().state.monthlyDate;
final spaceUuid =
context.read<SpaceTreeBloc>().state.selectedSpaces.firstOrNull ?? '';
if (spaceUuid.isEmpty) throw Exception('Space UUID is empty');
return GetAirQualityDistributionParam(
date: date,
spaceUuid: spaceUuid,
aqiType: aqiType,
);
}
}

View File

@ -6,8 +6,8 @@ enum AqiType {
aqi('AQI', '', 'aqi'),
pm25('PM2.5', 'µg/m³', 'pm25'),
pm10('PM10', 'µg/m³', 'pm10'),
hcho('HCHO', 'mg/m³', 'cho2'),
tvoc('TVOC', 'µg/m³', 'voc'),
hcho('HCHO', 'mg/m³', 'hcho'),
tvoc('TVOC', 'µg/m³', 'tvoc'),
co2('CO2', 'ppm', 'co2');
const AqiType(this.value, this.unit, this.code);

View File

@ -63,7 +63,7 @@ class RangeOfAqiChart extends StatelessWidget {
gradient: LinearGradient(
begin: Alignment.bottomCenter,
end: Alignment.topCenter,
stops: const [0.0, 0.2, 0.4, 0.6, 0.8, 1.0],
stops: [0.0, 0.2, 0.4, 0.6, 0.8, 1.0],
colors: RangeOfAqiChartsHelper.gradientData.map((e) {
final (color, _) = e;
return color.withValues(alpha: 0.6);

View File

@ -16,7 +16,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/occupancy/blocs/occupancy/occupancy_bloc.dart';
import 'package:syncrow_web/pages/analytics/modules/occupancy/blocs/occupancy_heat_map/occupancy_heat_map_bloc.dart';
import 'package:syncrow_web/pages/analytics/services/air_quality_distribution/remote_air_quality_distribution_service.dart';
import 'package:syncrow_web/pages/analytics/services/air_quality_distribution/fake_air_quality_distribution_service.dart';
import 'package:syncrow_web/pages/analytics/services/analytics_devices/analytics_devices_service_delagate.dart';
import 'package:syncrow_web/pages/analytics/services/analytics_devices/remote_energy_management_analytics_devices_service.dart';
import 'package:syncrow_web/pages/analytics/services/analytics_devices/remote_occupancy_analytics_devices_service.dart';
@ -27,7 +27,7 @@ import 'package:syncrow_web/pages/analytics/services/energy_consumption_per_devi
import 'package:syncrow_web/pages/analytics/services/occupacy/remote_occupancy_service.dart';
import 'package:syncrow_web/pages/analytics/services/occupancy_heat_map/remote_occupancy_heat_map_service.dart';
import 'package:syncrow_web/pages/analytics/services/power_clamp_info/remote_power_clamp_info_service.dart';
import 'package:syncrow_web/pages/analytics/services/range_of_aqi/remote_range_of_aqi_service.dart';
import 'package:syncrow_web/pages/analytics/services/range_of_aqi/fake_range_of_aqi_service.dart';
import 'package:syncrow_web/pages/analytics/services/realtime_device_service/firebase_realtime_device_service.dart';
import 'package:syncrow_web/pages/analytics/services/total_energy_consumption/remote_total_energy_consumption_service.dart';
import 'package:syncrow_web/pages/device_managment/shared/navigate_home_grid_view.dart';
@ -104,12 +104,12 @@ class _AnalyticsPageState extends State<AnalyticsPage> {
),
BlocProvider(
create: (context) => RangeOfAqiBloc(
RemoteRangeOfAqiService(_httpService),
FakeRangeOfAqiService(),
),
),
BlocProvider(
create: (context) => AirQualityDistributionBloc(
RemoteAirQualityDistributionService(_httpService),
FakeAirQualityDistributionService(),
),
),
BlocProvider(

View File

@ -20,7 +20,7 @@ class AnalyticsDateFilterButton extends StatefulWidget {
final void Function(DateTime)? onDateSelected;
final DatePickerType datePickerType;
static final Color _color = ColorsManager.blackColor.withValues(alpha: 0.8);
static final _color = ColorsManager.blackColor.withValues(alpha: 0.8);
@override
State<AnalyticsDateFilterButton> createState() =>
@ -60,21 +60,23 @@ class _AnalyticsDateFilterButtonState extends State<AnalyticsDateFilterButton> {
),
),
onPressed: () {
showDialog<void>(
showDialog(
context: context,
builder: (_) => switch (widget.datePickerType) {
DatePickerType.month => MonthPickerWidget(
selectedDate: widget.selectedDate,
onDateSelected: (value) {
widget.onDateSelected?.call(value);
},
),
DatePickerType.year => YearPickerWidget(
selectedDate: widget.selectedDate,
onDateSelected: (value) {
widget.onDateSelected?.call(value);
},
),
builder: (_) {
return switch (widget.datePickerType) {
DatePickerType.month => MonthPickerWidget(
selectedDate: widget.selectedDate,
onDateSelected: (value) {
widget.onDateSelected?.call(value);
},
),
DatePickerType.year => YearPickerWidget(
selectedDate: widget.selectedDate,
onDateSelected: (value) {
widget.onDateSelected?.call(value);
},
),
};
},
);
},

View File

@ -118,7 +118,7 @@ class AnalyticsPageTabsAndChildren extends StatelessWidget {
communityUuid: communities.firstOrNull ?? '',
spaceUuid: spaces.firstOrNull ?? '',
);
return;
break;
case AnalyticsPageTab.airQuality:
_onAirQualityDateChanged(
context,
@ -126,9 +126,8 @@ class AnalyticsPageTabsAndChildren extends StatelessWidget {
communityUuid: communities.firstOrNull ?? '',
spaceUuid: spaces.firstOrNull ?? '',
);
return;
default:
return;
break;
}
}
}
@ -158,7 +157,6 @@ class AnalyticsPageTabsAndChildren extends StatelessWidget {
required String communityUuid,
required String spaceUuid,
}) {
if (spaceUuid.isEmpty) return;
FetchAirQualityDataHelper.loadAirQualityData(
context,
date: date,

View File

@ -1,14 +1,9 @@
import 'package:syncrow_web/pages/analytics/modules/air_quality/widgets/aqi_type_dropdown.dart';
class GetAirQualityDistributionParam {
final DateTime date;
final String spaceUuid;
final AqiType aqiType;
const GetAirQualityDistributionParam(
{
const GetAirQualityDistributionParam({
required this.date,
required this.spaceUuid,
required this.aqiType,
});
}

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

@ -3,8 +3,7 @@ import 'package:syncrow_web/pages/analytics/params/get_air_quality_distribution_
import 'package:syncrow_web/pages/analytics/services/air_quality_distribution/air_quality_distribution_service.dart';
import 'package:syncrow_web/services/api/http_service.dart';
final class RemoteAirQualityDistributionService
implements AirQualityDistributionService {
class RemoteAirQualityDistributionService implements AirQualityDistributionService {
RemoteAirQualityDistributionService(this._httpService);
final HTTPService _httpService;
@ -15,10 +14,10 @@ final class RemoteAirQualityDistributionService
) async {
try {
final response = await _httpService.get(
path: '/aqi/distribution/space/${param.spaceUuid}',
path: 'endpoint',
queryParameters: {
'monthDate': _formatDate(param.date),
'pollutantType': param.aqiType.code,
'spaceUuid': param.spaceUuid,
'date': param.date.toIso8601String(),
},
expectedResponseModel: (data) {
final json = data as Map<String, dynamic>? ?? {};
@ -34,8 +33,4 @@ final class RemoteAirQualityDistributionService
throw Exception('Failed to load energy consumption per phase: $e');
}
}
static String _formatDate(DateTime date) {
return '${date.year}-${date.month.toString().padLeft(2, '0')}';
}
}

View File

@ -26,15 +26,15 @@ class DeviceLocationDetailsServiceDecorator implements DeviceLocationService {
if (data != null) {
final addressData = data['address'] as Map<String, dynamic>;
return deviceLocationInfo.copyWith(
city: addressData['city'] as String?,
country: addressData['country_code']?.toString().toUpperCase(),
address: addressData['state'] as String?,
city: addressData['city'],
country: addressData['country_code'].toString().toUpperCase(),
address: addressData['state'],
);
}
return deviceLocationInfo;
} catch (e) {
throw Exception('Failed to load device location info: $e');
throw Exception('Failed to load device location info: ${e.toString()}');
}
}
}

View File

@ -0,0 +1,36 @@
import 'package:syncrow_web/pages/analytics/models/range_of_aqi.dart';
import 'package:syncrow_web/pages/analytics/modules/air_quality/widgets/aqi_type_dropdown.dart';
import 'package:syncrow_web/pages/analytics/params/get_range_of_aqi_param.dart';
import 'package:syncrow_web/pages/analytics/services/range_of_aqi/range_of_aqi_service.dart';
class FakeRangeOfAqiService implements RangeOfAqiService {
@override
Future<List<RangeOfAqi>> load(GetRangeOfAqiParam param) async {
return await Future.delayed(const Duration(milliseconds: 800), () {
final random = DateTime.now().millisecondsSinceEpoch;
return List.generate(30, (index) {
final date = DateTime(2025, 5, 1).add(Duration(days: index));
final min = ((random + index * 17) % 200).toDouble();
final avgDelta = ((random + index * 23) % 50).toDouble() + 20;
final maxDelta = ((random + index * 31) % 50).toDouble() + 30;
final avg = (min + avgDelta).clamp(0.0, 301.0);
final max = (avg + maxDelta).clamp(0.0, 301.0);
return RangeOfAqi(
data: [
RangeOfAqiValue(type: AqiType.aqi.code, min: min, average: avg, max: max),
RangeOfAqiValue(type: AqiType.pm25.code, min: min, average: avg, max: max),
RangeOfAqiValue(type: AqiType.pm10.code, min: min, average: avg, max: max),
RangeOfAqiValue(type: AqiType.hcho.code, min: min, average: avg, max: max),
RangeOfAqiValue(type: AqiType.tvoc.code, min: min, average: avg, max: max),
RangeOfAqiValue(type: AqiType.co2.code, min: min, average: avg, max: max),
],
date: date,
);
});
});
}
}

View File

@ -12,8 +12,11 @@ final class RemoteRangeOfAqiService implements RangeOfAqiService {
Future<List<RangeOfAqi>> load(GetRangeOfAqiParam param) async {
try {
final response = await _httpService.get(
path: '/aqi/range/space/${param.spaceUuid}',
queryParameters: {'monthDate': _formatDate(param.date)},
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>? ?? [];
@ -25,11 +28,7 @@ final class RemoteRangeOfAqiService implements RangeOfAqiService {
);
return response;
} catch (e) {
throw Exception('Failed to load range of aqi: $e');
throw Exception('Failed to load energy consumption per phase: $e');
}
}
static String _formatDate(DateTime date) {
return '${date.year}-${date.month.toString().padLeft(2, '0')}';
}
}

View File

@ -78,7 +78,7 @@ class CustomWebTextField extends StatelessWidget {
controller: controller,
style: const TextStyle(color: Colors.black),
decoration: textBoxDecoration()!.copyWith(
errorStyle: const TextStyle(height: 0.01),
errorStyle: const TextStyle(height: 0),
hintStyle: context.textTheme.titleSmall!
.copyWith(color: Colors.grey, fontSize: 12),
hintText: hintText ?? 'Please enter'),

View File

@ -1,7 +1,10 @@
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_svg/svg.dart';
import 'package:syncrow_web/pages/device_managment/all_devices/bloc/device_mgmt_bloc/device_managment_bloc.dart';
import 'package:syncrow_web/pages/device_managment/all_devices/models/devices_model.dart';
import 'package:syncrow_web/pages/device_managment/device_setting/bloc/setting_bloc_bloc.dart';
import 'package:syncrow_web/pages/device_managment/device_setting/settings_model/device_info_model.dart';
import 'package:syncrow_web/pages/device_managment/device_setting/settings_model/sub_space_model.dart';
import 'package:syncrow_web/pages/device_managment/device_setting/sub_space_dialog.dart';
@ -66,14 +69,25 @@ class DeviceManagementContent extends StatelessWidget {
Padding(
padding: const EdgeInsets.all(10.0),
child: InkWell(
onTap: () {
showSubSpaceDialog(
onTap: () async {
final selectedSubSpace = await showSubSpaceDialog(
context,
communityUuid: device.community!.uuid!,
spaceUuid: device.spaces!.first.uuid!,
subSpaces: subSpaces,
selected: device.subspace!.uuid,
selected: deviceInfo.subspace.uuid,
);
if (selectedSubSpace != null) {
Future.delayed(const Duration(milliseconds: 500), () {
context.read<SettingDeviceBloc>().add(
SettingBlocAssignRoom(
communityUuid: device.community!.uuid!,
spaceUuid: device.spaces!.first.uuid!,
subSpaceUuid: selectedSubSpace.id ?? '',
),
);
});
}
},
child: infoRow(
label: 'Sub-Space:',

View File

@ -9,13 +9,11 @@ import 'package:syncrow_web/utils/extension/build_context_x.dart';
class SubSpaceDialog extends StatefulWidget {
final List<SubSpaceModel> subSpaces;
final String? selected;
final void Function(SubSpaceModel?) onConfirmed;
const SubSpaceDialog({
Key? key,
required this.subSpaces,
this.selected,
required this.onConfirmed,
}) : super(key: key);
@override
@ -86,30 +84,21 @@ class _SubSpaceDialogState extends State<SubSpaceDialog> {
}
}
void showSubSpaceDialog(
Future<SubSpaceModel?> showSubSpaceDialog(
BuildContext context, {
required List<SubSpaceModel> subSpaces,
String? selected,
required String communityUuid,
required String spaceUuid,
}) {
showDialog(
return showDialog<SubSpaceModel>(
context: context,
barrierDismissible: true,
builder: (ctx) => SubSpaceDialog(
subSpaces: subSpaces,
selected: selected,
onConfirmed: (selectedModel) {
if (selectedModel != null) {
context.read<SettingDeviceBloc>().add(
SettingBlocAssignRoom(
communityUuid: communityUuid,
spaceUuid: spaceUuid,
subSpaceUuid: selectedModel.id ?? '',
),
);
}
},
builder: (ctx) => BlocProvider.value(
value: BlocProvider.of<SettingDeviceBloc>(context),
child: SubSpaceDialog(
subSpaces: subSpaces,
selected: selected,
),
),
);
}

View File

@ -1,6 +1,4 @@
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:syncrow_web/pages/device_managment/device_setting/bloc/setting_bloc_bloc.dart';
import 'package:syncrow_web/pages/device_managment/device_setting/settings_model/sub_space_model.dart';
import 'package:syncrow_web/pages/device_managment/device_setting/sub_space_dialog.dart';
import 'package:syncrow_web/utils/color_manager.dart';
@ -62,11 +60,12 @@ class SubSpaceDialogButtons extends StatelessWidget {
? null
: () {
final selectedModel = widget.subSpaces.firstWhere(
(space) => space.id == _selectedId,
orElse: () =>
SubSpaceModel(id: null, name: '', devices: []));
widget.onConfirmed(selectedModel);
Navigator.of(context).pop();
(space) => space.id == _selectedId,
orElse: () =>
SubSpaceModel(id: null, name: '', devices: []),
);
Navigator.of(context)
.pop(selectedModel);
},
child: Text(
'Confirm',
@ -84,31 +83,3 @@ class SubSpaceDialogButtons extends StatelessWidget {
);
}
}
void showSubSpaceDialog(
BuildContext context, {
required List<SubSpaceModel> subSpaces,
String? selected,
required String communityUuid,
required String spaceUuid,
}) {
showDialog(
context: context,
barrierDismissible: true,
builder: (ctx) => SubSpaceDialog(
subSpaces: subSpaces,
selected: selected,
onConfirmed: (selectedModel) {
if (selectedModel != null) {
context.read<SettingDeviceBloc>().add(
SettingBlocAssignRoom(
communityUuid: communityUuid,
spaceUuid: spaceUuid,
subSpaceUuid: selectedModel.id ?? '',
),
);
}
},
),
);
}

View File

@ -40,7 +40,7 @@ class HomeCard extends StatelessWidget {
child: Text(
name,
style: const TextStyle(
fontSize: 30,
fontSize: 20,
color: Colors.white,
fontWeight: FontWeight.bold,
),

View File

@ -97,7 +97,7 @@ class _HomeWebPageState extends State<HomeWebPage> {
itemCount: homeBloc.homeItems.length,
gridDelegate:
const SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 4, // Adjust as needed.
crossAxisCount: 3, // Adjust as needed.
crossAxisSpacing: 20.0,
mainAxisSpacing: 20.0,
childAspectRatio: 1.5,

View File

@ -118,7 +118,6 @@ class DeviceDialogHelper {
uniqueCustomId: data['uniqueCustomId'],
deviceSelectedFunctions: deviceSelectedFunctions,
device: data['device'],
dialogType: dialogType,
);
case 'NCPS':
return FlushPresenceSensor.showFlushFunctionsDialog(

View File

@ -65,9 +65,7 @@ class ACHelper {
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
DialogHeader(dialogType == 'THEN'
? 'AC Functions'
: 'AC Conditions'),
const DialogHeader('AC Functions'),
Expanded(
child: Row(
crossAxisAlignment: CrossAxisAlignment.stretch,

View File

@ -96,9 +96,7 @@ class _WallPresenceSensorState extends State<FlushPresenceSensor> {
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
DialogHeader(widget.dialogType == 'THEN'
? 'Presence Sensor Functions'
: 'Presence Sensor Condition'),
const DialogHeader('Presence Sensor Condition'),
Expanded(child: _buildMainContent(context, state)),
_buildDialogFooter(context, state),
],

View File

@ -16,10 +16,9 @@ class GatewayDialog extends StatefulWidget {
required this.functions,
required this.deviceSelectedFunctions,
required this.device,
required this.dialogType,
super.key,
});
final String dialogType;
final String? uniqueCustomId;
final List<DeviceFunction> functions;
final List<DeviceFunctionData> deviceSelectedFunctions;
@ -56,9 +55,7 @@ class _GatewayDialogState extends State<GatewayDialog> {
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
DialogHeader(widget.dialogType == 'THEN'
? 'Gateway Functions'
: 'Gateway Conditions'),
const DialogHeader('Gateway Conditions'),
Expanded(child: _buildMainContent(context, state)),
_buildDialogFooter(context, state),
],

View File

@ -14,7 +14,6 @@ abstract final class GatewayHelper {
required String? uniqueCustomId,
required List<DeviceFunctionData> deviceSelectedFunctions,
required AllDevicesModel? device,
required String dialogType,
}) async {
return showDialog(
context: context,
@ -28,7 +27,6 @@ abstract final class GatewayHelper {
functions: functions,
deviceSelectedFunctions: deviceSelectedFunctions,
device: device,
dialogType:dialogType,
),
),
);

View File

@ -59,9 +59,7 @@ class OneGangSwitchHelper {
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
DialogHeader(dialogType == 'THEN'
? '1 Gang Light Switch Functions'
: '1 Gang Light Switch Condition'),
const DialogHeader('1 Gang Light Switch Condition'),
Expanded(
child: Row(
children: [

View File

@ -98,9 +98,7 @@ class _EnergyClampDialogState extends State<EnergyClampDialog> {
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
DialogHeader(widget.dialogType == 'THEN'
? 'Energy Clamp Functions'
: 'Energy Clamp Conditions'),
const DialogHeader('Energy Clamp Conditions'),
Expanded(
child: Visibility(
visible: _functions.isNotEmpty,

View File

@ -58,9 +58,7 @@ class ThreeGangSwitchHelper {
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
DialogHeader(dialogType == 'THEN'
? '3 Gangs Light Switch Functions'
: '3 Gangs Light Switch Condition'),
const DialogHeader('3 Gangs Light Switch Condition'),
Expanded(
child: Row(
children: [

View File

@ -59,9 +59,7 @@ class TwoGangSwitchHelper {
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
DialogHeader(dialogType == 'THEN'
? '2 Gangs Light Switch Functions'
: '2 Gangs Light Switch Condition'),
const DialogHeader('2 Gangs Light Switch Condition'),
Expanded(
child: Row(
children: [

View File

@ -63,8 +63,7 @@ class _WallPresenceSensorState extends State<WallPresenceSensor> {
@override
void initState() {
super.initState();
_wpsFunctions =
widget.functions.whereType<WpsFunctions>().where((function) {
_wpsFunctions = widget.functions.whereType<WpsFunctions>().where((function) {
if (widget.dialogType == 'THEN') {
return function.type == 'THEN' || function.type == 'BOTH';
}
@ -98,9 +97,7 @@ class _WallPresenceSensorState extends State<WallPresenceSensor> {
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
DialogHeader(widget.dialogType == 'THEN'
? 'Presence Sensor Functions'
: 'Presence Sensor Condition'),
const DialogHeader('Presence Sensor Condition'),
Expanded(child: _buildMainContent(context, state)),
_buildDialogFooter(context, state),
],

View File

@ -93,9 +93,7 @@ class _WaterHeaterDialogRoutinesState extends State<WaterHeaterDialogRoutines> {
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
DialogHeader(widget.dialogType == 'THEN'
? 'Water Heater Funtions'
: 'Water Heater Condition'),
const DialogHeader('Water Heater Condition'),
Expanded(child: _buildMainContent(context, state)),
_buildDialogFooter(context, state),
],

View File

@ -0,0 +1,15 @@
//
// Generated file. Do not edit.
//
// clang-format off
#ifndef GENERATED_PLUGIN_REGISTRANT_
#define GENERATED_PLUGIN_REGISTRANT_
#include <flutter_linux/flutter_linux.h>
// Registers Flutter plugins.
void fl_register_plugins(FlPluginRegistry* registry);
#endif // GENERATED_PLUGIN_REGISTRANT_

View File

@ -0,0 +1,26 @@
//
// Generated file. Do not edit.
//
import FlutterMacOS
import Foundation
import firebase_analytics
import firebase_core
import firebase_crashlytics
import firebase_database
import flutter_secure_storage_macos
import path_provider_foundation
import shared_preferences_foundation
import url_launcher_macos
func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
FirebaseAnalyticsPlugin.register(with: registry.registrar(forPlugin: "FirebaseAnalyticsPlugin"))
FLTFirebaseCorePlugin.register(with: registry.registrar(forPlugin: "FLTFirebaseCorePlugin"))
FLTFirebaseCrashlyticsPlugin.register(with: registry.registrar(forPlugin: "FLTFirebaseCrashlyticsPlugin"))
FLTFirebaseDatabasePlugin.register(with: registry.registrar(forPlugin: "FLTFirebaseDatabasePlugin"))
FlutterSecureStoragePlugin.register(with: registry.registrar(forPlugin: "FlutterSecureStoragePlugin"))
PathProviderPlugin.register(with: registry.registrar(forPlugin: "PathProviderPlugin"))
SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin"))
UrlLauncherPlugin.register(with: registry.registrar(forPlugin: "UrlLauncherPlugin"))
}