diff --git a/assets/icons/close_curtain.svg b/assets/icons/close_curtain.svg
new file mode 100644
index 00000000..53f9e03b
--- /dev/null
+++ b/assets/icons/close_curtain.svg
@@ -0,0 +1,8 @@
+
diff --git a/assets/icons/open_curtain.svg b/assets/icons/open_curtain.svg
new file mode 100644
index 00000000..715773a5
--- /dev/null
+++ b/assets/icons/open_curtain.svg
@@ -0,0 +1,8 @@
+
diff --git a/assets/icons/pause_curtain.svg b/assets/icons/pause_curtain.svg
new file mode 100644
index 00000000..8f90ea4f
--- /dev/null
+++ b/assets/icons/pause_curtain.svg
@@ -0,0 +1,6 @@
+
diff --git a/assets/icons/reverse_arrows.svg b/assets/icons/reverse_arrows.svg
new file mode 100644
index 00000000..fe119c39
--- /dev/null
+++ b/assets/icons/reverse_arrows.svg
@@ -0,0 +1,10 @@
+
diff --git a/lib/common/widgets/app_loading_indicator.dart b/lib/common/widgets/app_loading_indicator.dart
new file mode 100644
index 00000000..bc811c56
--- /dev/null
+++ b/lib/common/widgets/app_loading_indicator.dart
@@ -0,0 +1,10 @@
+import 'package:flutter/material.dart';
+
+class AppLoadingIndicator extends StatelessWidget {
+ const AppLoadingIndicator({super.key});
+
+ @override
+ Widget build(BuildContext context) {
+ return const Center(child: CircularProgressIndicator());
+ }
+}
diff --git a/lib/pages/analytics/models/analytics_device.dart b/lib/pages/analytics/models/analytics_device.dart
index 3340a41d..869de23f 100644
--- a/lib/pages/analytics/models/analytics_device.dart
+++ b/lib/pages/analytics/models/analytics_device.dart
@@ -25,8 +25,8 @@ class AnalyticsDevice {
factory AnalyticsDevice.fromJson(Map json) {
return AnalyticsDevice(
- uuid: json['uuid'] as String,
- name: json['name'] as String,
+ uuid: json['uuid'] as String? ?? '',
+ name: json['name'] as String? ?? '',
createdAt: json['createdAt'] != null
? DateTime.parse(json['createdAt'] as String)
: null,
@@ -39,8 +39,8 @@ class AnalyticsDevice {
? ProductDevice.fromJson(json['productDevice'] as Map)
: null,
spaceUuid: json['spaceUuid'] as String?,
- latitude: json['lat'] != null ? double.parse(json['lat'] as String) : null,
- longitude: json['lon'] != null ? double.parse(json['lon'] as String) : null,
+ latitude: json['lat'] != null ? double.parse(json['lat'] as String? ?? '0.0') : null,
+ longitude: json['lon'] != null ? double.parse(json['lon'] as String? ?? '0.0') : null,
);
}
}
diff --git a/lib/pages/analytics/models/occupancy_heat_map_model.dart b/lib/pages/analytics/models/occupancy_heat_map_model.dart
index 73e7d5d7..4c7a37d4 100644
--- a/lib/pages/analytics/models/occupancy_heat_map_model.dart
+++ b/lib/pages/analytics/models/occupancy_heat_map_model.dart
@@ -14,12 +14,21 @@ class OccupancyHeatMapModel extends Equatable {
});
factory OccupancyHeatMapModel.fromJson(Map json) {
+ final eventDate = json['event_date'] as String?;
+ final year = eventDate?.split('-')[0];
+ final month = eventDate?.split('-')[1];
+ final day = eventDate?.split('-')[2];
+
return OccupancyHeatMapModel(
uuid: json['uuid'] as String? ?? '',
- eventDate: DateTime.parse(
- json['event_date'] as String? ?? '${DateTime.now()}',
+ eventDate: DateTime.utc(
+ int.parse(year ?? '2025'),
+ int.parse(month ?? '1'),
+ int.parse(day ?? '1'),
),
- countTotalPresenceDetected: json['count_total_presence_detected'] as int? ?? 0,
+ countTotalPresenceDetected: num.parse(
+ json['count_total_presence_detected']?.toString() ?? '0',
+ ).toInt(),
);
}
diff --git a/lib/pages/analytics/models/range_of_aqi.dart b/lib/pages/analytics/models/range_of_aqi.dart
index 0308d564..dfb48ecb 100644
--- a/lib/pages/analytics/models/range_of_aqi.dart
+++ b/lib/pages/analytics/models/range_of_aqi.dart
@@ -38,9 +38,9 @@ class RangeOfAqiValue extends Equatable {
factory RangeOfAqiValue.fromJson(Map 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(),
+ min: (json['min'] as num? ?? 0).toDouble(),
+ average: (json['average'] as num? ?? 0).toDouble(),
+ max: (json['max'] as num? ?? 0).toDouble(),
);
}
diff --git a/lib/pages/analytics/modules/air_quality/helpers/fetch_air_quality_data_helper.dart b/lib/pages/analytics/modules/air_quality/helpers/fetch_air_quality_data_helper.dart
index 223c0357..5c63e397 100644
--- a/lib/pages/analytics/modules/air_quality/helpers/fetch_air_quality_data_helper.dart
+++ b/lib/pages/analytics/modules/air_quality/helpers/fetch_air_quality_data_helper.dart
@@ -4,7 +4,6 @@ import 'package:syncrow_web/pages/analytics/modules/air_quality/blocs/air_qualit
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';
import 'package:syncrow_web/pages/analytics/params/get_air_quality_distribution_param.dart';
@@ -22,13 +21,14 @@ abstract final class FetchAirQualityDataHelper {
required String spaceUuid,
bool shouldFetchAnalyticsDevices = true,
}) {
- final date = context.read().state.monthlyDate;
final aqiType = context.read().state.selectedAqiType;
- loadAnalyticsDevices(
- context,
- communityUuid: communityUuid,
- spaceUuid: spaceUuid,
- );
+ if (shouldFetchAnalyticsDevices) {
+ loadAnalyticsDevices(
+ context,
+ communityUuid: communityUuid,
+ spaceUuid: spaceUuid,
+ );
+ }
loadRangeOfAqi(
context,
spaceUuid: spaceUuid,
diff --git a/lib/pages/analytics/modules/air_quality/helpers/range_of_aqi_charts_helper.dart b/lib/pages/analytics/modules/air_quality/helpers/range_of_aqi_charts_helper.dart
index 21cb2a9e..f46f2708 100644
--- a/lib/pages/analytics/modules/air_quality/helpers/range_of_aqi_charts_helper.dart
+++ b/lib/pages/analytics/modules/air_quality/helpers/range_of_aqi_charts_helper.dart
@@ -18,11 +18,16 @@ abstract final class RangeOfAqiChartsHelper {
(ColorsManager.hazardousPurple, 'Hazardous'),
];
- static FlTitlesData titlesData(BuildContext context, List data) {
+ static FlTitlesData titlesData(
+ BuildContext context,
+ List data, {
+ double leftSideInterval = 50,
+ }) {
final titlesData = EnergyManagementChartsHelper.titlesData(context);
return titlesData.copyWith(
bottomTitles: titlesData.bottomTitles.copyWith(
sideTitles: titlesData.bottomTitles.sideTitles.copyWith(
+ reservedSize: 36,
getTitlesWidget: (value, meta) => Padding(
padding: const EdgeInsetsDirectional.only(top: 20.0),
child: Text(
@@ -38,10 +43,11 @@ abstract final class RangeOfAqiChartsHelper {
leftTitles: titlesData.leftTitles.copyWith(
sideTitles: titlesData.leftTitles.sideTitles.copyWith(
reservedSize: 70,
- interval: 50,
+ interval: leftSideInterval,
maxIncluded: false,
+ minIncluded: true,
getTitlesWidget: (value, meta) {
- final text = value >= 300 ? '301+' : value.toInt().toString();
+ final text = value.toInt().toString();
return Padding(
padding: const EdgeInsetsDirectional.only(end: 12),
child: FittedBox(
diff --git a/lib/pages/analytics/modules/air_quality/views/air_quality_view.dart b/lib/pages/analytics/modules/air_quality/views/air_quality_view.dart
index b6d403eb..61179d15 100644
--- a/lib/pages/analytics/modules/air_quality/views/air_quality_view.dart
+++ b/lib/pages/analytics/modules/air_quality/views/air_quality_view.dart
@@ -1,6 +1,7 @@
import 'package:flutter/material.dart';
import 'package:syncrow_web/pages/analytics/modules/air_quality/widgets/air_quality_end_side_widget.dart';
import 'package:syncrow_web/pages/analytics/modules/air_quality/widgets/aqi_distribution_chart_box.dart';
+import 'package:syncrow_web/pages/analytics/modules/air_quality/widgets/aqi_legend.dart';
import 'package:syncrow_web/pages/analytics/modules/air_quality/widgets/range_of_aqi_chart_box.dart';
class AirQualityView extends StatelessWidget {
@@ -20,6 +21,10 @@ class AirQualityView extends StatelessWidget {
child: Column(
spacing: 32,
children: [
+ SizedBox(
+ height: height * 0.1,
+ child: const AqiLegend(),
+ ),
SizedBox(
height: height * 1.2,
child: const AirQualityEndSideWidget(),
@@ -40,7 +45,7 @@ class AirQualityView extends StatelessWidget {
return SingleChildScrollView(
child: Container(
padding: _padding,
- height: height * 1.1,
+ height: height * 1.2,
child: const Column(
children: [
Expanded(
@@ -52,8 +57,9 @@ class AirQualityView extends StatelessWidget {
child: Column(
spacing: 20,
children: [
- Expanded(child: RangeOfAqiChartBox()),
- Expanded(child: AqiDistributionChartBox()),
+ Expanded(flex: 2, child: AqiLegend()),
+ Expanded(flex: 12, child: RangeOfAqiChartBox()),
+ Expanded(flex: 12, child: AqiDistributionChartBox()),
],
),
),
diff --git a/lib/pages/analytics/modules/air_quality/widgets/aqi_device_info.dart b/lib/pages/analytics/modules/air_quality/widgets/aqi_device_info.dart
index ebe88614..23ae874e 100644
--- a/lib/pages/analytics/modules/air_quality/widgets/aqi_device_info.dart
+++ b/lib/pages/analytics/modules/air_quality/widgets/aqi_device_info.dart
@@ -65,7 +65,7 @@ class AqiDeviceInfo extends StatelessWidget {
);
final tvocValue = _getValueForStatus(
status,
- 'tvoc_value',
+ 'voc_value',
formatter: (value) => (value / 100).toStringAsFixed(2),
);
diff --git a/lib/pages/analytics/modules/air_quality/widgets/aqi_distribution_chart.dart b/lib/pages/analytics/modules/air_quality/widgets/aqi_distribution_chart.dart
index 2f3d7ff0..4fdd8a2a 100644
--- a/lib/pages/analytics/modules/air_quality/widgets/aqi_distribution_chart.dart
+++ b/lib/pages/analytics/modules/air_quality/widgets/aqi_distribution_chart.dart
@@ -3,6 +3,7 @@ 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/pages/analytics/widgets/charts_x_axis_title.dart';
import 'package:syncrow_web/utils/color_manager.dart';
import 'package:syncrow_web/utils/extension/build_context_x.dart';
@@ -32,8 +33,13 @@ class AqiDistributionChart extends StatelessWidget {
}
List _buildBarGroups() {
- return List.generate(chartData.length, (index) {
- final data = chartData[index];
+ final groups = [];
+ for (var i = 0; i < chartData.length; i++) {
+ final data = chartData[i];
+ final isAllZero = data.data.every((d) => d.percentage == 0);
+ if (isAllZero) {
+ continue;
+ }
final stackItems = [];
double currentY = 0;
var isFirstElement = true;
@@ -56,13 +62,15 @@ class AqiDistributionChart extends StatelessWidget {
currentY += percentageData.percentage + _rodStackItemsSpacing;
isFirstElement = false;
}
-
- return BarChartGroupData(
- x: index,
- barRods: stackItems,
- groupVertically: true,
+ groups.add(
+ BarChartGroupData(
+ x: i,
+ barRods: stackItems,
+ groupVertically: true,
+ ),
);
- });
+ }
+ return groups;
}
BarTouchData _barTouchData(BuildContext context) {
@@ -73,6 +81,7 @@ class AqiDistributionChart extends StatelessWidget {
color: ColorsManager.semiTransparentBlack,
),
tooltipRoundedRadius: 16,
+ maxContentWidth: 500,
tooltipPadding: const EdgeInsets.all(8),
getTooltipItem: (group, groupIndex, rod, rodIndex) {
final data = chartData[group.x];
@@ -81,10 +90,13 @@ class AqiDistributionChart extends StatelessWidget {
final textStyle = context.textTheme.bodySmall?.copyWith(
color: ColorsManager.blackColor,
- fontSize: 8,
+ fontSize: 11,
);
for (final percentageData in data.data) {
+ if (percentageData.percentage == 0) {
+ continue;
+ }
final percentage = percentageData.percentage.toStringAsFixed(1);
final type = percentageData.type[0].toUpperCase() +
percentageData.type.substring(1).replaceAll('_', ' ');
@@ -98,7 +110,7 @@ class AqiDistributionChart extends StatelessWidget {
DateFormat('dd/MM/yyyy').format(data.date),
context.textTheme.bodyMedium!.copyWith(
color: ColorsManager.blackColor,
- fontSize: 9,
+ fontSize: 12,
fontWeight: FontWeight.w600,
),
textAlign: TextAlign.start,
@@ -118,7 +130,6 @@ class AqiDistributionChart extends StatelessWidget {
final leftTitles = titlesData.leftTitles.copyWith(
sideTitles: titlesData.leftTitles.sideTitles.copyWith(
reservedSize: 70,
- interval: 20,
maxIncluded: false,
minIncluded: true,
getTitlesWidget: (value, meta) => Padding(
@@ -139,8 +150,9 @@ class AqiDistributionChart extends StatelessWidget {
);
final bottomTitles = AxisTitles(
+ axisNameWidget: const ChartsXAxisTitle(),
sideTitles: SideTitles(
- showTitles: true,
+ showTitles: chartData.isNotEmpty,
getTitlesWidget: (value, _) => FittedBox(
alignment: AlignmentDirectional.bottomCenter,
fit: BoxFit.scaleDown,
@@ -148,7 +160,7 @@ class AqiDistributionChart extends StatelessWidget {
chartData[value.toInt()].date.day.toString(),
style: context.textTheme.bodySmall?.copyWith(
color: ColorsManager.lightGreyColor,
- fontSize: 8,
+ fontSize: 12,
),
),
),
diff --git a/lib/pages/analytics/modules/air_quality/widgets/aqi_distribution_chart_title.dart b/lib/pages/analytics/modules/air_quality/widgets/aqi_distribution_chart_title.dart
index 926d28e1..f7be6ee3 100644
--- a/lib/pages/analytics/modules/air_quality/widgets/aqi_distribution_chart_title.dart
+++ b/lib/pages/analytics/modules/air_quality/widgets/aqi_distribution_chart_title.dart
@@ -19,7 +19,7 @@ class AqiDistributionChartTitle extends StatelessWidget {
children: [
ChartsLoadingWidget(isLoading: isLoading),
const Expanded(
- flex: 3,
+ flex: 4,
child: FittedBox(
fit: BoxFit.scaleDown,
alignment: AlignmentDirectional.centerStart,
@@ -28,23 +28,26 @@ class AqiDistributionChartTitle extends StatelessWidget {
),
),
),
- FittedBox(
- alignment: AlignmentDirectional.centerEnd,
- fit: BoxFit.scaleDown,
- child: AqiTypeDropdown(
- onChanged: (value) {
- if (value != null) {
- final bloc = context.read();
- try {
- final param = _makeLoadAqiDistributionParam(context, value);
- bloc.add(LoadAirQualityDistribution(param));
- } catch (_) {
- return;
- } finally {
- bloc.add(UpdateAqiTypeEvent(value));
+ Expanded(
+ flex: 2,
+ child: FittedBox(
+ alignment: AlignmentDirectional.centerEnd,
+ fit: BoxFit.scaleDown,
+ child: AqiTypeDropdown(
+ onChanged: (value) {
+ if (value != null) {
+ final bloc = context.read();
+ try {
+ final param = _makeLoadAqiDistributionParam(context, value);
+ bloc.add(LoadAirQualityDistribution(param));
+ } catch (_) {
+ return;
+ } finally {
+ bloc.add(UpdateAqiTypeEvent(value));
+ }
}
- }
- },
+ },
+ ),
),
),
],
diff --git a/lib/pages/analytics/modules/air_quality/widgets/aqi_legend.dart b/lib/pages/analytics/modules/air_quality/widgets/aqi_legend.dart
new file mode 100644
index 00000000..3a00925d
--- /dev/null
+++ b/lib/pages/analytics/modules/air_quality/widgets/aqi_legend.dart
@@ -0,0 +1,38 @@
+import 'package:flutter/material.dart';
+import 'package:syncrow_web/pages/analytics/modules/air_quality/helpers/range_of_aqi_charts_helper.dart';
+import 'package:syncrow_web/pages/analytics/modules/analytics/widgets/chart_informative_cell.dart';
+import 'package:syncrow_web/utils/style.dart';
+
+class AqiLegend extends StatelessWidget {
+ const AqiLegend({super.key});
+
+ @override
+ Widget build(BuildContext context) {
+ return Container(
+ padding: const EdgeInsetsDirectional.all(20),
+ decoration: subSectionContainerDecoration.copyWith(
+ borderRadius: BorderRadius.circular(20),
+ ),
+ child: Row(
+ mainAxisAlignment: MainAxisAlignment.center,
+ spacing: 16,
+ children: RangeOfAqiChartsHelper.gradientData.map((e) {
+ return Flexible(
+ flex: 4,
+ child: FittedBox(
+ fit: BoxFit.fill,
+ child: ChartInformativeCell(
+ color: e.$1,
+ title: FittedBox(
+ fit: BoxFit.fill,
+ child: Text(e.$2),
+ ),
+ height: null,
+ ),
+ ),
+ );
+ }).toList(),
+ ),
+ );
+ }
+}
diff --git a/lib/pages/analytics/modules/air_quality/widgets/aqi_location_info_cell.dart b/lib/pages/analytics/modules/air_quality/widgets/aqi_location_info_cell.dart
index fa0216a1..00233ad3 100644
--- a/lib/pages/analytics/modules/air_quality/widgets/aqi_location_info_cell.dart
+++ b/lib/pages/analytics/modules/air_quality/widgets/aqi_location_info_cell.dart
@@ -47,36 +47,37 @@ class AqiLocationInfoCell extends StatelessWidget {
),
),
Align(
- alignment: AlignmentDirectional.bottomEnd,
- child: Padding(
- padding: const EdgeInsetsDirectional.all(10),
- child: SizedBox(
- height: 40,
- width: 120,
- child: FittedBox(
- fit: BoxFit.scaleDown,
- alignment: AlignmentDirectional.bottomEnd,
- child: Text(
- value,
- style: context.textTheme.bodySmall?.copyWith(
- color: ColorsManager.vividBlue.withValues(alpha: 0.7),
- fontWeight: FontWeight.w700,
- fontSize: 24,
+ alignment: AlignmentDirectional.bottomCenter,
+ child: Row(
+ crossAxisAlignment: CrossAxisAlignment.end,
+ children: [
+ Expanded(
+ child: SvgPicture.asset(
+ svgPath,
+ fit: BoxFit.scaleDown,
+ alignment: AlignmentDirectional.bottomStart,
+ ),
+ ),
+ Expanded(
+ child: FittedBox(
+ fit: BoxFit.scaleDown,
+ alignment: AlignmentDirectional.bottomEnd,
+ child: Padding(
+ padding: const EdgeInsetsDirectional.all(10),
+ child: Text(
+ value,
+ style: context.textTheme.bodySmall?.copyWith(
+ color: ColorsManager.vividBlue.withValues(
+ alpha: 0.7,
+ ),
+ fontWeight: FontWeight.w700,
+ fontSize: 24,
+ ),
+ ),
),
),
),
- ),
- ),
- ),
- Align(
- alignment: AlignmentDirectional.bottomStart,
- child: SizedBox.square(
- dimension: MediaQuery.sizeOf(context).width * 0.45,
- child: FittedBox(
- fit: BoxFit.scaleDown,
- alignment: AlignmentDirectional.bottomStart,
- child: SvgPicture.asset(svgPath),
- ),
+ ],
),
),
],
diff --git a/lib/pages/analytics/modules/air_quality/widgets/aqi_type_dropdown.dart b/lib/pages/analytics/modules/air_quality/widgets/aqi_type_dropdown.dart
index 5d482d9c..6640c717 100644
--- a/lib/pages/analytics/modules/air_quality/widgets/aqi_type_dropdown.dart
+++ b/lib/pages/analytics/modules/air_quality/widgets/aqi_type_dropdown.dart
@@ -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³', 'ch2o'),
+ tvoc('TVOC', 'mg/m³', 'voc'),
co2('CO2', 'ppm', 'co2');
const AqiType(this.value, this.unit, this.code);
diff --git a/lib/pages/analytics/modules/air_quality/widgets/range_of_aqi_chart.dart b/lib/pages/analytics/modules/air_quality/widgets/range_of_aqi_chart.dart
index 5e731d90..0914eab3 100644
--- a/lib/pages/analytics/modules/air_quality/widgets/range_of_aqi_chart.dart
+++ b/lib/pages/analytics/modules/air_quality/widgets/range_of_aqi_chart.dart
@@ -2,15 +2,18 @@ import 'package:fl_chart/fl_chart.dart';
import 'package:flutter/material.dart';
import 'package:syncrow_web/pages/analytics/models/range_of_aqi.dart';
import 'package:syncrow_web/pages/analytics/modules/air_quality/helpers/range_of_aqi_charts_helper.dart';
+import 'package:syncrow_web/pages/analytics/modules/air_quality/widgets/aqi_type_dropdown.dart';
import 'package:syncrow_web/pages/analytics/modules/energy_management/helpers/energy_management_charts_helper.dart';
import 'package:syncrow_web/utils/color_manager.dart';
class RangeOfAqiChart extends StatelessWidget {
final List chartData;
+ final AqiType selectedAqiType;
const RangeOfAqiChart({
super.key,
required this.chartData,
+ required this.selectedAqiType,
});
List<(List values, Color color, Color? dotColor)> get _lines {
@@ -45,15 +48,34 @@ class RangeOfAqiChart extends StatelessWidget {
];
}
+ (double maxY, double interval) get _maxYForAqiType {
+ const aqiMaxValues = {
+ AqiType.aqi: (401, 100),
+ AqiType.pm25: (351, 50),
+ AqiType.pm10: (501, 100),
+ AqiType.hcho: (301, 50),
+ AqiType.tvoc: (501, 50),
+ AqiType.co2: (1251, 250),
+ };
+
+ return aqiMaxValues[selectedAqiType]!;
+ }
+
@override
Widget build(BuildContext context) {
return LineChart(
LineChartData(
minY: 0,
- maxY: 301,
+ maxY: _maxYForAqiType.$1,
clipData: const FlClipData.vertical(),
- gridData: EnergyManagementChartsHelper.gridData(horizontalInterval: 50),
- titlesData: RangeOfAqiChartsHelper.titlesData(context, chartData),
+ gridData: EnergyManagementChartsHelper.gridData(
+ horizontalInterval: _maxYForAqiType.$2,
+ ),
+ titlesData: RangeOfAqiChartsHelper.titlesData(
+ context,
+ chartData,
+ leftSideInterval: _maxYForAqiType.$2,
+ ),
borderData: EnergyManagementChartsHelper.borderData(),
lineTouchData: RangeOfAqiChartsHelper.lineTouchData(chartData),
betweenBarsData: [
diff --git a/lib/pages/analytics/modules/air_quality/widgets/range_of_aqi_chart_box.dart b/lib/pages/analytics/modules/air_quality/widgets/range_of_aqi_chart_box.dart
index 6548c696..cb189dce 100644
--- a/lib/pages/analytics/modules/air_quality/widgets/range_of_aqi_chart_box.dart
+++ b/lib/pages/analytics/modules/air_quality/widgets/range_of_aqi_chart_box.dart
@@ -32,7 +32,12 @@ class RangeOfAqiChartBox extends StatelessWidget {
const SizedBox(height: 10),
const Divider(),
const SizedBox(height: 20),
- Expanded(child: RangeOfAqiChart(chartData: state.filteredRangeOfAqi)),
+ Expanded(
+ child: RangeOfAqiChart(
+ chartData: state.filteredRangeOfAqi,
+ selectedAqiType: state.selectedAqiType,
+ ),
+ ),
],
),
);
diff --git a/lib/pages/analytics/modules/analytics/widgets/chart_informative_cell.dart b/lib/pages/analytics/modules/analytics/widgets/chart_informative_cell.dart
index eec31998..f79ecb44 100644
--- a/lib/pages/analytics/modules/analytics/widgets/chart_informative_cell.dart
+++ b/lib/pages/analytics/modules/analytics/widgets/chart_informative_cell.dart
@@ -7,16 +7,18 @@ class ChartInformativeCell extends StatelessWidget {
required this.title,
required this.color,
this.hasBorder = false,
+ this.height,
});
final Widget title;
final Color color;
final bool hasBorder;
+ final double? height;
@override
Widget build(BuildContext context) {
return Container(
- height: MediaQuery.sizeOf(context).height * 0.0385,
+ height: height ?? MediaQuery.sizeOf(context).height * 0.0385,
padding: const EdgeInsetsDirectional.symmetric(
vertical: 8,
horizontal: 12,
diff --git a/lib/pages/analytics/modules/energy_management/helpers/energy_management_charts_helper.dart b/lib/pages/analytics/modules/energy_management/helpers/energy_management_charts_helper.dart
index b1af85c8..6b44e125 100644
--- a/lib/pages/analytics/modules/energy_management/helpers/energy_management_charts_helper.dart
+++ b/lib/pages/analytics/modules/energy_management/helpers/energy_management_charts_helper.dart
@@ -1,6 +1,7 @@
import 'package:fl_chart/fl_chart.dart';
import 'package:flutter/material.dart';
import 'package:syncrow_web/pages/analytics/helpers/format_number_to_kwh.dart';
+import 'package:syncrow_web/pages/analytics/widgets/charts_x_axis_title.dart';
import 'package:syncrow_web/utils/color_manager.dart';
import 'package:syncrow_web/utils/extension/build_context_x.dart';
@@ -15,6 +16,7 @@ abstract final class EnergyManagementChartsHelper {
return FlTitlesData(
show: true,
bottomTitles: AxisTitles(
+ axisNameWidget: const ChartsXAxisTitle(),
drawBelowEverything: true,
sideTitles: SideTitles(
interval: 1,
@@ -62,17 +64,12 @@ abstract final class EnergyManagementChartsHelper {
);
}
- static String getToolTipLabel(num month, double value) {
- final monthLabel = month.toString();
- final valueLabel = value.formatNumberToKwh;
- final labels = [monthLabel, valueLabel];
- return labels.where((element) => element.isNotEmpty).join(', ');
- }
+ static String getToolTipLabel(double value) => value.formatNumberToKwh;
static List getTooltipItems(List touchedSpots) {
return touchedSpots.map((spot) {
return LineTooltipItem(
- getToolTipLabel(spot.x, spot.y),
+ getToolTipLabel(spot.y),
const TextStyle(
color: ColorsManager.textPrimaryColor,
fontWeight: FontWeight.w600,
diff --git a/lib/pages/analytics/modules/energy_management/widgets/analytics_device_dropdown.dart b/lib/pages/analytics/modules/energy_management/widgets/analytics_device_dropdown.dart
index f7b33309..055e9675 100644
--- a/lib/pages/analytics/modules/energy_management/widgets/analytics_device_dropdown.dart
+++ b/lib/pages/analytics/modules/energy_management/widgets/analytics_device_dropdown.dart
@@ -28,15 +28,29 @@ class AnalyticsDeviceDropdown extends StatelessWidget {
),
),
child: Visibility(
- visible: state.devices.isNotEmpty,
- replacement: _buildNoDevicesFound(context),
- child: _buildDevicesDropdown(context, state),
+ visible: state.status != AnalyticsDevicesStatus.loading,
+ replacement: _buildLoadingIndicator(),
+ child: Visibility(
+ visible: state.devices.isNotEmpty,
+ replacement: _buildNoDevicesFound(context),
+ child: _buildDevicesDropdown(context, state),
+ ),
),
);
},
);
}
+ Widget _buildLoadingIndicator() {
+ return const Center(
+ child: SizedBox(
+ width: 24,
+ height: 24,
+ child: CircularProgressIndicator(strokeWidth: 3),
+ ),
+ );
+ }
+
static const _defaultPadding = EdgeInsetsDirectional.symmetric(
horizontal: 20,
vertical: 2,
diff --git a/lib/pages/analytics/modules/energy_management/widgets/energy_consumption_per_device_chart_box.dart b/lib/pages/analytics/modules/energy_management/widgets/energy_consumption_per_device_chart_box.dart
index be5faf57..06b6c529 100644
--- a/lib/pages/analytics/modules/energy_management/widgets/energy_consumption_per_device_chart_box.dart
+++ b/lib/pages/analytics/modules/energy_management/widgets/energy_consumption_per_device_chart_box.dart
@@ -37,7 +37,7 @@ class EnergyConsumptionPerDeviceChartBox extends StatelessWidget {
fit: BoxFit.scaleDown,
alignment: AlignmentDirectional.centerStart,
child: ChartTitle(
- title: Text('Energy Consumption per Device'),
+ title: Text('Device energy consumed'),
),
),
),
diff --git a/lib/pages/analytics/modules/energy_management/widgets/total_energy_consumption_chart.dart b/lib/pages/analytics/modules/energy_management/widgets/total_energy_consumption_chart.dart
index 85b95c29..bba1fa14 100644
--- a/lib/pages/analytics/modules/energy_management/widgets/total_energy_consumption_chart.dart
+++ b/lib/pages/analytics/modules/energy_management/widgets/total_energy_consumption_chart.dart
@@ -14,14 +14,17 @@ class TotalEnergyConsumptionChart extends StatelessWidget {
return Expanded(
child: LineChart(
LineChartData(
+ maxY: chartData.isEmpty
+ ? null
+ : chartData.map((e) => e.value).reduce((a, b) => a > b ? a : b) + 250,
clipData: const FlClipData.vertical(),
titlesData: EnergyManagementChartsHelper.titlesData(
context,
- leftTitlesInterval: 250,
+ leftTitlesInterval: 500,
),
gridData: EnergyManagementChartsHelper.gridData().copyWith(
checkToShowHorizontalLine: (value) => true,
- horizontalInterval: 250,
+ horizontalInterval: 500,
),
borderData: EnergyManagementChartsHelper.borderData(),
lineTouchData: EnergyManagementChartsHelper.lineTouchData(),
@@ -29,7 +32,6 @@ class TotalEnergyConsumptionChart extends StatelessWidget {
),
duration: Duration.zero,
curve: Curves.easeIn,
-
),
);
}
diff --git a/lib/pages/analytics/modules/energy_management/widgets/total_energy_consumption_chart_box.dart b/lib/pages/analytics/modules/energy_management/widgets/total_energy_consumption_chart_box.dart
index e197c297..4d88471d 100644
--- a/lib/pages/analytics/modules/energy_management/widgets/total_energy_consumption_chart_box.dart
+++ b/lib/pages/analytics/modules/energy_management/widgets/total_energy_consumption_chart_box.dart
@@ -32,7 +32,7 @@ class TotalEnergyConsumptionChartBox extends StatelessWidget {
child: FittedBox(
alignment: AlignmentDirectional.centerStart,
fit: BoxFit.scaleDown,
- child: ChartTitle(title: Text('Total Energy Consumption')),
+ child: ChartTitle(title: Text('Space energy consumed')),
),
),
const Spacer(flex: 4),
diff --git a/lib/pages/analytics/modules/occupancy/helpers/fetch_occupancy_data_helper.dart b/lib/pages/analytics/modules/occupancy/helpers/fetch_occupancy_data_helper.dart
index 0b01fda2..3bd96bce 100644
--- a/lib/pages/analytics/modules/occupancy/helpers/fetch_occupancy_data_helper.dart
+++ b/lib/pages/analytics/modules/occupancy/helpers/fetch_occupancy_data_helper.dart
@@ -81,7 +81,7 @@ abstract final class FetchOccupancyDataHelper {
param: GetAnalyticsDevicesParam(
communityUuid: communityUuid,
spaceUuid: spaceUuid,
- deviceTypes: ['WPS', 'CPS'],
+ deviceTypes: ['WPS', 'CPS', 'NCPS'],
requestType: AnalyticsDeviceRequestType.occupancy,
),
onSuccess: (device) {
diff --git a/lib/pages/analytics/modules/occupancy/views/analytics_occupancy_view.dart b/lib/pages/analytics/modules/occupancy/views/analytics_occupancy_view.dart
index 3a025254..56f8ce08 100644
--- a/lib/pages/analytics/modules/occupancy/views/analytics_occupancy_view.dart
+++ b/lib/pages/analytics/modules/occupancy/views/analytics_occupancy_view.dart
@@ -20,7 +20,7 @@ class AnalyticsOccupancyView extends StatelessWidget {
child: Column(
spacing: 32,
children: [
- SizedBox(height: height * 0.46, child: const OccupancyEndSideBar()),
+ SizedBox(height: height * 0.8, child: const OccupancyEndSideBar()),
SizedBox(height: height * 0.5, child: const OccupancyChartBox()),
SizedBox(height: height * 0.5, child: const OccupancyHeatMapBox()),
],
diff --git a/lib/pages/analytics/modules/occupancy/widgets/heat_map_tooltip.dart b/lib/pages/analytics/modules/occupancy/widgets/heat_map_tooltip.dart
index c7695064..66612a3e 100644
--- a/lib/pages/analytics/modules/occupancy/widgets/heat_map_tooltip.dart
+++ b/lib/pages/analytics/modules/occupancy/widgets/heat_map_tooltip.dart
@@ -39,7 +39,7 @@ class HeatMapTooltip extends StatelessWidget {
),
const Divider(height: 2, thickness: 1),
Text(
- '$value Occupants',
+ 'Occupancy detected: $value',
style: context.textTheme.bodySmall?.copyWith(
fontSize: 10,
fontWeight: FontWeight.w500,
diff --git a/lib/pages/analytics/modules/occupancy/widgets/interactive_heat_map.dart b/lib/pages/analytics/modules/occupancy/widgets/interactive_heat_map.dart
index a652ae73..514ebb65 100644
--- a/lib/pages/analytics/modules/occupancy/widgets/interactive_heat_map.dart
+++ b/lib/pages/analytics/modules/occupancy/widgets/interactive_heat_map.dart
@@ -52,7 +52,7 @@ class _InteractiveHeatMapState extends State {
color: Colors.transparent,
child: Transform.translate(
offset: Offset(-(widget.cellSize * 2.5), -50),
- child: HeatMapTooltip(date: item.date, value: item.value),
+ child: HeatMapTooltip(date: item.date.toUtc(), value: item.value),
),
),
),
diff --git a/lib/pages/analytics/modules/occupancy/widgets/occupancy_chart.dart b/lib/pages/analytics/modules/occupancy/widgets/occupancy_chart.dart
index 70087c46..1205a66e 100644
--- a/lib/pages/analytics/modules/occupancy/widgets/occupancy_chart.dart
+++ b/lib/pages/analytics/modules/occupancy/widgets/occupancy_chart.dart
@@ -2,6 +2,7 @@ import 'package:fl_chart/fl_chart.dart';
import 'package:flutter/material.dart';
import 'package:syncrow_web/pages/analytics/models/occupacy.dart';
import 'package:syncrow_web/pages/analytics/modules/energy_management/helpers/energy_management_charts_helper.dart';
+import 'package:syncrow_web/pages/analytics/widgets/charts_x_axis_title.dart';
import 'package:syncrow_web/utils/color_manager.dart';
import 'package:syncrow_web/utils/extension/build_context_x.dart';
@@ -88,8 +89,8 @@ class OccupancyChart extends StatelessWidget {
}) {
final data = chartData;
- final occupancyValue = double.parse(data[group.x.toInt()].occupancy);
- final percentage = '${(occupancyValue).toStringAsFixed(0)}%';
+ final occupancyValue = double.parse(data[group.x].occupancy);
+ final percentage = '${occupancyValue.toStringAsFixed(0)}%';
return BarTooltipItem(
percentage,
@@ -116,7 +117,7 @@ class OccupancyChart extends StatelessWidget {
alignment: AlignmentDirectional.centerStart,
fit: BoxFit.scaleDown,
child: Text(
- '${(value).toStringAsFixed(0)}%',
+ '${value.toStringAsFixed(0)}%',
style: context.textTheme.bodySmall?.copyWith(
fontSize: 12,
color: ColorsManager.greyColor,
@@ -128,6 +129,7 @@ class OccupancyChart extends StatelessWidget {
);
final bottomTitles = AxisTitles(
+ axisNameWidget: const ChartsXAxisTitle(),
sideTitles: SideTitles(
showTitles: true,
getTitlesWidget: (value, _) => FittedBox(
diff --git a/lib/pages/analytics/modules/occupancy/widgets/occupancy_end_side_bar.dart b/lib/pages/analytics/modules/occupancy/widgets/occupancy_end_side_bar.dart
index 3dd01bee..7c9ed548 100644
--- a/lib/pages/analytics/modules/occupancy/widgets/occupancy_end_side_bar.dart
+++ b/lib/pages/analytics/modules/occupancy/widgets/occupancy_end_side_bar.dart
@@ -23,10 +23,9 @@ class OccupancyEndSideBar extends StatelessWidget {
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
- const AnalyticsSidebarHeader(title: 'Presnce Sensor'),
+ const AnalyticsSidebarHeader(title: 'Presence Sensor'),
Expanded(
child: SizedBox(
- // height: MediaQuery.sizeOf(context).height * 0.2,
child: PowerClampEnergyStatusWidget(
status: [
PowerClampEnergyStatus(
diff --git a/lib/pages/analytics/modules/occupancy/widgets/occupancy_heat_map.dart b/lib/pages/analytics/modules/occupancy/widgets/occupancy_heat_map.dart
index 05415421..482f0029 100644
--- a/lib/pages/analytics/modules/occupancy/widgets/occupancy_heat_map.dart
+++ b/lib/pages/analytics/modules/occupancy/widgets/occupancy_heat_map.dart
@@ -9,8 +9,13 @@ import 'package:syncrow_web/pages/analytics/modules/occupancy/widgets/occupancy_
import 'package:syncrow_web/utils/color_manager.dart';
class OccupancyHeatMap extends StatelessWidget {
- const OccupancyHeatMap({required this.heatMapData, super.key});
+ const OccupancyHeatMap({
+ required this.heatMapData,
+ required this.selectedDate,
+ super.key,
+ });
final Map heatMapData;
+ final DateTime selectedDate;
static const _cellSize = 16.0;
static const _totalWeeks = 53;
@@ -20,7 +25,7 @@ class OccupancyHeatMap extends StatelessWidget {
: 0;
DateTime _getStartingDate() {
- final jan1 = DateTime(DateTime.now().year, 1, 1);
+ final jan1 = DateTime.utc(selectedDate.year, 1, 1);
final startOfWeek = jan1.subtract(Duration(days: jan1.weekday - 1));
return startOfWeek;
}
diff --git a/lib/pages/analytics/modules/occupancy/widgets/occupancy_heat_map_box.dart b/lib/pages/analytics/modules/occupancy/widgets/occupancy_heat_map_box.dart
index c3b537e0..a5f56aa4 100644
--- a/lib/pages/analytics/modules/occupancy/widgets/occupancy_heat_map_box.dart
+++ b/lib/pages/analytics/modules/occupancy/widgets/occupancy_heat_map_box.dart
@@ -70,6 +70,8 @@ class OccupancyHeatMapBox extends StatelessWidget {
const SizedBox(height: 20),
Expanded(
child: OccupancyHeatMap(
+ selectedDate:
+ context.watch().state.yearlyDate,
heatMapData: state.heatMapData.asMap().map(
(_, value) => MapEntry(
value.eventDate,
diff --git a/lib/pages/analytics/widgets/charts_x_axis_title.dart b/lib/pages/analytics/widgets/charts_x_axis_title.dart
new file mode 100644
index 00000000..746a8cbb
--- /dev/null
+++ b/lib/pages/analytics/widgets/charts_x_axis_title.dart
@@ -0,0 +1,23 @@
+import 'package:flutter/material.dart';
+import 'package:syncrow_web/utils/color_manager.dart';
+import 'package:syncrow_web/utils/extension/build_context_x.dart';
+
+class ChartsXAxisTitle extends StatelessWidget {
+ const ChartsXAxisTitle({
+ this.label = 'Day of month',
+ super.key,
+ });
+
+ final String label;
+
+ @override
+ Widget build(BuildContext context) {
+ return Text(
+ label,
+ style: context.textTheme.bodySmall?.copyWith(
+ color: ColorsManager.lightGreyColor,
+ fontSize: 8,
+ ),
+ );
+ }
+}
diff --git a/lib/pages/auth/bloc/auth_bloc.dart b/lib/pages/auth/bloc/auth_bloc.dart
index 58950089..bfe0b3eb 100644
--- a/lib/pages/auth/bloc/auth_bloc.dart
+++ b/lib/pages/auth/bloc/auth_bloc.dart
@@ -36,7 +36,8 @@ class AuthBloc extends Bloc {
////////////////////////////// forget password //////////////////////////////////
final TextEditingController forgetEmailController = TextEditingController();
- final TextEditingController forgetPasswordController = TextEditingController();
+ final TextEditingController forgetPasswordController =
+ TextEditingController();
final TextEditingController forgetOtp = TextEditingController();
final forgetFormKey = GlobalKey();
final forgetEmailKey = GlobalKey();
@@ -53,7 +54,8 @@ class AuthBloc extends Bloc {
return;
}
_remainingTime = 1;
- add(UpdateTimerEvent(remainingTime: _remainingTime, isButtonEnabled: false));
+ add(UpdateTimerEvent(
+ remainingTime: _remainingTime, isButtonEnabled: false));
try {
forgetEmailValidate = '';
_remainingTime = (await AuthenticationAPI.sendOtp(
@@ -90,7 +92,8 @@ class AuthBloc extends Bloc {
_timer?.cancel();
add(const UpdateTimerEvent(remainingTime: 0, isButtonEnabled: true));
} else {
- add(UpdateTimerEvent(remainingTime: _remainingTime, isButtonEnabled: false));
+ add(UpdateTimerEvent(
+ remainingTime: _remainingTime, isButtonEnabled: false));
}
});
}
@@ -100,7 +103,7 @@ class AuthBloc extends Bloc {
emit(const TimerState(isButtonEnabled: true, remainingTime: 0));
}
-Future changePassword(
+ Future changePassword(
ChangePasswordEvent event, Emitter emit) async {
emit(LoadingForgetState());
try {
@@ -122,7 +125,6 @@ Future changePassword(
}
}
-
String? validateCode(String? value) {
if (value == null || value.isEmpty) {
return 'Code is required';
@@ -131,7 +133,9 @@ Future changePassword(
}
void _onUpdateTimer(UpdateTimerEvent event, Emitter emit) {
- emit(TimerState(isButtonEnabled: event.isButtonEnabled, remainingTime: event.remainingTime));
+ emit(TimerState(
+ isButtonEnabled: event.isButtonEnabled,
+ remainingTime: event.remainingTime));
}
///////////////////////////////////// login /////////////////////////////////////
@@ -151,7 +155,6 @@ Future changePassword(
static UserModel? user;
bool showValidationMessage = false;
-
void _login(LoginButtonPressed event, Emitter emit) async {
emit(AuthLoading());
if (isChecked) {
@@ -170,11 +173,11 @@ Future changePassword(
);
} on APIException catch (e) {
validate = e.message;
- emit(LoginInitial());
+ emit(LoginFailure(error: validate));
return;
} catch (e) {
validate = 'Something went wrong';
- emit(LoginInitial());
+ emit(LoginFailure(error: validate));
return;
}
@@ -197,7 +200,6 @@ Future changePassword(
}
}
-
checkBoxToggle(
CheckBoxEvent event,
Emitter emit,
@@ -339,12 +341,14 @@ Future changePassword(
static Future getTokenAndValidate() async {
try {
const storage = FlutterSecureStorage();
- final firstLaunch =
- await SharedPreferencesHelper.readBoolFromSP(StringsManager.firstLaunch) ?? true;
+ final firstLaunch = await SharedPreferencesHelper.readBoolFromSP(
+ StringsManager.firstLaunch) ??
+ true;
if (firstLaunch) {
storage.deleteAll();
}
- await SharedPreferencesHelper.saveBoolToSP(StringsManager.firstLaunch, false);
+ await SharedPreferencesHelper.saveBoolToSP(
+ StringsManager.firstLaunch, false);
final value = await storage.read(key: Token.loginAccessTokenKey) ?? '';
if (value.isEmpty) {
return 'Token not found';
@@ -397,7 +401,9 @@ Future changePassword(
final String formattedTime = [
if (days > 0) '${days}d', // Append 'd' for days
if (days > 0 || hours > 0)
- hours.toString().padLeft(2, '0'), // Show hours if there are days or hours
+ hours
+ .toString()
+ .padLeft(2, '0'), // Show hours if there are days or hours
minutes.toString().padLeft(2, '0'),
seconds.toString().padLeft(2, '0'),
].join(':');
diff --git a/lib/pages/common/custom_table.dart b/lib/pages/common/custom_table.dart
index f23daa45..fb8237b7 100644
--- a/lib/pages/common/custom_table.dart
+++ b/lib/pages/common/custom_table.dart
@@ -50,20 +50,11 @@ class _DynamicTableState extends State {
bool _selectAll = false;
final ScrollController _verticalScrollController = ScrollController();
final ScrollController _horizontalScrollController = ScrollController();
- late ScrollController _horizontalHeaderScrollController;
- late ScrollController _horizontalBodyScrollController;
+
@override
void initState() {
super.initState();
_initializeSelection();
- _horizontalHeaderScrollController = ScrollController();
- _horizontalBodyScrollController = ScrollController();
-
- // Synchronize horizontal scrolling
- _horizontalBodyScrollController.addListener(() {
- _horizontalHeaderScrollController
- .jumpTo(_horizontalBodyScrollController.offset);
- });
}
@override
@@ -113,94 +104,112 @@ class _DynamicTableState extends State {
context.read().add(UpdateSelection(_selectedRows));
}
- @override
- void dispose() {
- _horizontalHeaderScrollController.dispose();
- _horizontalBodyScrollController.dispose();
- super.dispose();
- }
-
@override
Widget build(BuildContext context) {
return Container(
decoration: widget.cellDecoration,
- child: Column(
- children: [
- Container(
- decoration: widget.headerDecoration ??
- const BoxDecoration(color: ColorsManager.boxColor),
+ child: Scrollbar(
+ controller: _verticalScrollController,
+ thumbVisibility: true,
+ trackVisibility: true,
+ child: Scrollbar(
+ //fixed the horizontal scrollbar issue
+ controller: _horizontalScrollController,
+ thumbVisibility: true,
+ trackVisibility: true,
+ notificationPredicate: (notif) => notif.depth == 1,
+ child: SingleChildScrollView(
+ controller: _verticalScrollController,
child: SingleChildScrollView(
+ controller: _horizontalScrollController,
scrollDirection: Axis.horizontal,
- physics: const NeverScrollableScrollPhysics(),
- controller: _horizontalHeaderScrollController,
child: SizedBox(
width: widget.size.width,
- child: Row(
+ child: Column(
children: [
- if (widget.withCheckBox) _buildSelectAllCheckbox(),
- ...List.generate(widget.headers.length, (index) {
- return _buildTableHeaderCell(
- widget.headers[index], index);
- }),
+ Container(
+ decoration: widget.headerDecoration ??
+ const BoxDecoration(
+ color: ColorsManager.boxColor,
+ ),
+ child: Row(
+ children: [
+ if (widget.withCheckBox) _buildSelectAllCheckbox(),
+ ...List.generate(widget.headers.length, (index) {
+ return _buildTableHeaderCell(
+ widget.headers[index], index);
+ })
+ //...widget.headers.map((header) => _buildTableHeaderCell(header)),
+ ],
+ ),
+ ),
+ SizedBox(
+ width: widget.size.width,
+ child: widget.isEmpty
+ ? _buildEmptyState()
+ : Column(
+ children:
+ List.generate(widget.data.length, (rowIndex) {
+ final row = widget.data[rowIndex];
+ return Row(
+ children: [
+ if (widget.withCheckBox)
+ _buildRowCheckbox(
+ rowIndex, widget.size.height * 0.08),
+ ...row.asMap().entries.map((entry) {
+ return _buildTableCell(
+ entry.value.toString(),
+ widget.size.height * 0.08,
+ rowIndex: rowIndex,
+ columnIndex: entry.key,
+ );
+ }).toList(),
+ ],
+ );
+ }),
+ ),
+ ),
],
),
),
),
),
- Expanded(
- child: Scrollbar(
- controller: _verticalScrollController,
- thumbVisibility: true,
- trackVisibility: true,
- child: SingleChildScrollView(
- controller: _verticalScrollController,
- child: Scrollbar(
- controller: _horizontalBodyScrollController,
- thumbVisibility: false,
- trackVisibility: false,
- notificationPredicate: (notif) => notif.depth == 1,
- child: SingleChildScrollView(
- scrollDirection: Axis.horizontal,
- controller: _horizontalBodyScrollController,
- child: Container(
- color: ColorsManager.whiteColors,
- child: SizedBox(
- width: widget.size.width,
- child: widget.isEmpty
- ? _buildEmptyState()
- : Column(
- children: List.generate(widget.data.length,
- (rowIndex) {
- final row = widget.data[rowIndex];
- return Row(
- children: [
- if (widget.withCheckBox)
- _buildRowCheckbox(rowIndex,
- widget.size.height * 0.08),
- ...row.asMap().entries.map((entry) {
- return _buildTableCell(
- entry.value.toString(),
- widget.size.height * 0.08,
- rowIndex: rowIndex,
- columnIndex: entry.key,
- );
- }).toList(),
- ],
- );
- }),
- ),
- ),
- ),
- ),
- ),
- ),
- ),
- ),
- ],
+ ),
),
);
}
+ Widget _buildEmptyState() => Container(
+ height: widget.size.height,
+ color: ColorsManager.whiteColors,
+ child: Column(
+ mainAxisAlignment: MainAxisAlignment.center,
+ children: [
+ Row(
+ crossAxisAlignment: CrossAxisAlignment.center,
+ mainAxisAlignment: MainAxisAlignment.center,
+ children: [
+ Column(
+ children: [
+ SvgPicture.asset(Assets.emptyTable),
+ const SizedBox(height: 15),
+ Text(
+ widget.tableName == 'AccessManagement'
+ ? 'No Password '
+ : 'No Devices',
+ style: Theme.of(context)
+ .textTheme
+ .bodySmall!
+ .copyWith(color: ColorsManager.grayColor),
+ )
+ ],
+ ),
+ ],
+ ),
+ SizedBox(height: widget.size.height * 0.5),
+ ],
+ ),
+ );
Widget _buildSelectAllCheckbox() {
return Container(
width: 50,
@@ -218,32 +227,6 @@ class _DynamicTableState extends State {
);
}
- Widget _buildEmptyState() => Column(
- mainAxisAlignment: MainAxisAlignment.center,
- children: [
- Row(
- crossAxisAlignment: CrossAxisAlignment.center,
- mainAxisAlignment: MainAxisAlignment.center,
- children: [
- Column(
- children: [
- SvgPicture.asset(Assets.emptyTable),
- const SizedBox(height: 15),
- Text(
- widget.tableName == 'AccessManagement'
- ? 'No Password '
- : 'No Devices',
- style: Theme.of(context)
- .textTheme
- .bodySmall!
- .copyWith(color: ColorsManager.grayColor),
- )
- ],
- ),
- ],
- ),
- ],
- );
Widget _buildRowCheckbox(int index, double size) {
return Container(
width: 50,
@@ -298,12 +281,8 @@ class _DynamicTableState extends State {
);
}
- Widget _buildTableCell(
- String content,
- double size, {
- required int rowIndex,
- required int columnIndex,
- }) {
+ Widget _buildTableCell(String content, double size,
+ {required int rowIndex, required int columnIndex}) {
bool isBatteryLevel = content.endsWith('%');
double? batteryLevel;
@@ -311,7 +290,6 @@ class _DynamicTableState extends State {
batteryLevel = double.tryParse(content.replaceAll('%', '').trim());
}
bool isSettingsColumn = widget.headers[columnIndex] == 'Settings';
-
if (isSettingsColumn) {
return buildSettingsIcon(
width: 120,
@@ -416,11 +394,10 @@ class _DynamicTableState extends State {
padding: const EdgeInsets.all(8.0),
child: Center(
child: SvgPicture.asset(
- Assets.settings, // ضع المسار الصحيح هنا
+ Assets.settings,
width: 40,
height: 22,
- color: ColorsManager
- .primaryColor, // نفس لون الأيقونة في الصورة
+ color: ColorsManager.primaryColor,
),
),
),
diff --git a/lib/pages/device_managment/ac/bloc/ac_bloc.dart b/lib/pages/device_managment/ac/bloc/ac_bloc.dart
index af5a7b0a..85e119f1 100644
--- a/lib/pages/device_managment/ac/bloc/ac_bloc.dart
+++ b/lib/pages/device_managment/ac/bloc/ac_bloc.dart
@@ -68,24 +68,30 @@ class AcBloc extends Bloc {
}
}
- void _listenToChanges(deviceId) {
+ StreamSubscription? _deviceStatusSubscription;
+
+ void _listenToChanges(String deviceId) {
try {
final ref = FirebaseDatabase.instance.ref('device-status/$deviceId');
- final stream = ref.onValue;
-
- stream.listen((DatabaseEvent event) async {
+ _deviceStatusSubscription = ref.onValue.listen((DatabaseEvent event) async {
if (event.snapshot.value == null) return;
- Map usersMap =
- event.snapshot.value as Map;
+ final usersMap = event.snapshot.value! as Map;
- List statusList = [];
+ final statusList = [];
usersMap['status'].forEach((element) {
statusList.add(Status(code: element['code'], value: element['value']));
});
+
+ deviceStatus =
+ AcStatusModel.fromJson(usersMap['productUuid'], statusList);
+
deviceStatus = AcStatusModel.fromJson(usersMap['productUuid'], statusList);
+ print('Device status updated: ${deviceStatus.acSwitch}');
+
+
if (!isClosed) {
add(AcStatusUpdated(deviceStatus));
}
@@ -105,22 +111,14 @@ class AcBloc extends Bloc {
AcControlEvent event,
Emitter emit,
) async {
- emit(AcsLoadingState());
- _updateDeviceFunctionFromCode(event.code, event.value);
- emit(ACStatusLoaded(status: deviceStatus));
-
try {
- final success = await controlDeviceService.controlDevice(
+ _updateDeviceFunctionFromCode(event.code, event.value);
+ emit(ACStatusLoaded(status: deviceStatus));
+ await controlDeviceService.controlDevice(
deviceUuid: event.deviceId,
status: Status(code: event.code, value: event.value),
);
-
- if (!success) {
- emit(const AcsFailedState(error: 'Failed to control device'));
- }
- } catch (e) {
- emit(AcsFailedState(error: e.toString()));
- }
+ } catch (e) {}
}
FutureOr _onFetchAcBatchStatus(
@@ -141,23 +139,16 @@ class AcBloc extends Bloc {
AcBatchControlEvent event,
Emitter emit,
) async {
- emit(AcsLoadingState());
_updateDeviceFunctionFromCode(event.code, event.value);
emit(ACStatusLoaded(status: deviceStatus));
try {
- final success = await batchControlDevicesService.batchControlDevices(
+ await batchControlDevicesService.batchControlDevices(
uuids: event.devicesIds,
code: event.code,
value: event.value,
);
-
- if (!success) {
- emit(const AcsFailedState(error: 'Failed to control devices'));
- }
- } catch (e) {
- emit(AcsFailedState(error: e.toString()));
- }
+ } catch (e) {}
}
Future _onFactoryReset(
@@ -190,8 +181,8 @@ class AcBloc extends Bloc {
void _handleIncreaseTime(IncreaseTimeEvent event, Emitter emit) {
if (state is! ACStatusLoaded) return;
final currentState = state as ACStatusLoaded;
- int newHours = scheduledHours;
- int newMinutes = scheduledMinutes + 30;
+ var newHours = scheduledHours;
+ var newMinutes = scheduledMinutes + 30;
newHours += newMinutes ~/ 60;
newMinutes = newMinutes % 60;
if (newHours > 23) {
@@ -213,7 +204,7 @@ class AcBloc extends Bloc {
) {
if (state is! ACStatusLoaded) return;
final currentState = state as ACStatusLoaded;
- int totalMinutes = (scheduledHours * 60) + scheduledMinutes;
+ var totalMinutes = (scheduledHours * 60) + scheduledMinutes;
totalMinutes = (totalMinutes - 30).clamp(0, 1440);
scheduledHours = totalMinutes ~/ 60;
scheduledMinutes = totalMinutes % 60;
@@ -286,20 +277,24 @@ class AcBloc extends Bloc {
void _startCountdownTimer(Emitter emit) {
_countdownTimer?.cancel();
- int totalSeconds = (scheduledHours * 3600) + (scheduledMinutes * 60);
+ var totalSeconds = (scheduledHours * 3600) + (scheduledMinutes * 60);
_countdownTimer = Timer.periodic(const Duration(seconds: 1), (timer) {
if (totalSeconds > 0) {
totalSeconds--;
scheduledHours = totalSeconds ~/ 3600;
scheduledMinutes = (totalSeconds % 3600) ~/ 60;
- add(UpdateTimerEvent());
+ if (!isClosed) {
+ add(UpdateTimerEvent());
+ }
} else {
_countdownTimer?.cancel();
timerActive = false;
scheduledHours = 0;
scheduledMinutes = 0;
- add(TimerCompletedEvent());
+ if (!isClosed) {
+ add(TimerCompletedEvent());
+ }
}
});
}
@@ -326,7 +321,9 @@ class AcBloc extends Bloc {
_startCountdownTimer(
emit,
);
- add(UpdateTimerEvent());
+ if (!isClosed) {
+ add(UpdateTimerEvent());
+ }
}
}
@@ -370,6 +367,8 @@ class AcBloc extends Bloc {
@override
Future close() {
add(OnClose());
+ _countdownTimer?.cancel();
+ _deviceStatusSubscription?.cancel();
return super.close();
}
}
diff --git a/lib/pages/device_managment/all_devices/bloc/device_mgmt_bloc/device_managment_bloc.dart b/lib/pages/device_managment/all_devices/bloc/device_mgmt_bloc/device_managment_bloc.dart
index 05e82f1f..4063692e 100644
--- a/lib/pages/device_managment/all_devices/bloc/device_mgmt_bloc/device_managment_bloc.dart
+++ b/lib/pages/device_managment/all_devices/bloc/device_mgmt_bloc/device_managment_bloc.dart
@@ -40,18 +40,15 @@ class DeviceManagementBloc
List devices = [];
_devices.clear();
var spaceBloc = event.context.read();
- final projectUuid = await ProjectManager.getProjectUUID() ?? '';
-
+ final projectUuid = await ProjectManager.getProjectUUID() ?? '';
if (spaceBloc.state.selectedCommunities.isEmpty) {
- devices = await DevicesManagementApi().fetchDevices('', '', projectUuid);
+ devices = await DevicesManagementApi().fetchDevices(projectUuid);
} else {
for (var community in spaceBloc.state.selectedCommunities) {
List spacesList =
spaceBloc.state.selectedCommunityAndSpaces[community] ?? [];
- for (var space in spacesList) {
- devices.addAll(await DevicesManagementApi().fetchDevices(
- community, space, projectUuid));
- }
+ devices.addAll(await DevicesManagementApi()
+ .fetchDevices(projectUuid, spacesId: spacesList));
}
}
@@ -100,7 +97,7 @@ class DeviceManagementBloc
));
if (currentProductName.isNotEmpty) {
- add(SearchDevices(productName: currentProductName));
+ add(SearchDevices(deviceNameOrProductName: currentProductName));
}
}
}
@@ -274,29 +271,37 @@ class DeviceManagementBloc
SearchDevices event, Emitter emit) {
if ((event.community == null || event.community!.isEmpty) &&
(event.unitName == null || event.unitName!.isEmpty) &&
- (event.productName == null || event.productName!.isEmpty)) {
+ (event.deviceNameOrProductName == null ||
+ event.deviceNameOrProductName!.isEmpty)) {
currentProductName = '';
- if (state is DeviceManagementFiltered) {
- add(FilterDevices(_getFilterFromIndex(_selectedIndex)));
- } else {
- return;
- }
+ _filteredDevices = List.from(_devices);
+ emit(DeviceManagementLoaded(
+ devices: _devices,
+ selectedIndex: _selectedIndex,
+ onlineCount: _onlineCount,
+ offlineCount: _offlineCount,
+ lowBatteryCount: _lowBatteryCount,
+ selectedDevice: null,
+ isControlButtonEnabled: false,
+ ));
+ return;
}
-
- if (event.productName == currentProductName &&
+ if (event.deviceNameOrProductName == currentProductName &&
event.community == currentCommunity &&
event.unitName == currentUnitName &&
event.searchField) {
return;
}
- currentProductName = event.productName ?? '';
+ currentProductName = event.deviceNameOrProductName ?? '';
currentCommunity = event.community;
currentUnitName = event.unitName;
- List devicesToSearch = _filteredDevices;
+ List devicesToSearch = _devices;
if (devicesToSearch.isNotEmpty) {
+ final searchText = event.deviceNameOrProductName?.toLowerCase() ?? '';
+
final filteredDevices = devicesToSearch.where((device) {
final matchesCommunity = event.community == null ||
event.community!.isEmpty ||
@@ -304,31 +309,25 @@ class DeviceManagementBloc
?.toLowerCase()
.contains(event.community!.toLowerCase()) ??
false);
+
final matchesUnit = event.unitName == null ||
event.unitName!.isEmpty ||
(device.spaces != null &&
- device.spaces!.isNotEmpty &&
- device.spaces![0].spaceName!
- .toLowerCase()
- .contains(event.unitName!.toLowerCase()));
- final matchesProductName = event.productName == null ||
- event.productName!.isEmpty ||
- (device.name
- ?.toLowerCase()
- .contains(event.productName!.toLowerCase()) ??
- false);
- final matchesDeviceName = event.productName == null ||
- event.productName!.isEmpty ||
- (device.categoryName
- ?.toLowerCase()
- .contains(event.productName!.toLowerCase()) ??
- false);
+ device.spaces!.any((space) =>
+ space.spaceName != null &&
+ space.spaceName!
+ .toLowerCase()
+ .contains(event.unitName!.toLowerCase())));
- return matchesCommunity &&
- matchesUnit &&
- (matchesProductName || matchesDeviceName);
+ final matchesSearchText = searchText.isEmpty ||
+ (device.name?.toLowerCase().contains(searchText) ?? false) ||
+ (device.productName?.toLowerCase().contains(searchText) ?? false);
+
+ return matchesCommunity && matchesUnit && matchesSearchText;
}).toList();
+ _filteredDevices = filteredDevices;
+
emit(DeviceManagementFiltered(
filteredDevices: filteredDevices,
selectedIndex: _selectedIndex,
diff --git a/lib/pages/device_managment/all_devices/bloc/device_mgmt_bloc/device_managment_event.dart b/lib/pages/device_managment/all_devices/bloc/device_mgmt_bloc/device_managment_event.dart
index 9928c50e..5292de0e 100644
--- a/lib/pages/device_managment/all_devices/bloc/device_mgmt_bloc/device_managment_event.dart
+++ b/lib/pages/device_managment/all_devices/bloc/device_mgmt_bloc/device_managment_event.dart
@@ -38,18 +38,18 @@ class SelectedFilterChanged extends DeviceManagementEvent {
class SearchDevices extends DeviceManagementEvent {
final String? community;
final String? unitName;
- final String? productName;
+ final String? deviceNameOrProductName;
final bool searchField;
const SearchDevices({
this.community,
this.unitName,
- this.productName,
+ this.deviceNameOrProductName,
this.searchField = false,
});
@override
- List