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/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/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 f23abd7b..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,7 +21,6 @@ abstract final class FetchAirQualityDataHelper {
required String spaceUuid,
bool shouldFetchAnalyticsDevices = true,
}) {
- final date = context.read().state.monthlyDate;
final aqiType = context.read().state.selectedAqiType;
if (shouldFetchAnalyticsDevices) {
loadAnalyticsDevices(
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 e4aa5b6f..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,7 +18,11 @@ 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(
@@ -39,11 +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/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 e35a05e7..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';
@@ -149,6 +150,7 @@ class AqiDistributionChart extends StatelessWidget {
);
final bottomTitles = AxisTitles(
+ axisNameWidget: const ChartsXAxisTitle(),
sideTitles: SideTitles(
showTitles: chartData.isNotEmpty,
getTitlesWidget: (value, _) => FittedBox(
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/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 625c59c2..fb8237b7 100644
--- a/lib/pages/common/custom_table.dart
+++ b/lib/pages/common/custom_table.dart
@@ -179,31 +179,36 @@ 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 _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(
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 4c3e5b39..b7f04a58 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
@@ -44,18 +44,14 @@ class DeviceManagementBloc
_devices.clear();
var spaceBloc = event.context.read();
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));
}
}
@@ -273,6 +269,7 @@ class DeviceManagementBloc
return 'All';
}
}
+
void _onSearchDevices(
SearchDevices event, Emitter emit) {
if ((event.community == null || event.community!.isEmpty) &&
diff --git a/lib/pages/device_managment/all_devices/helper/route_controls_based_code.dart b/lib/pages/device_managment/all_devices/helper/route_controls_based_code.dart
index 5586a310..08bca73c 100644
--- a/lib/pages/device_managment/all_devices/helper/route_controls_based_code.dart
+++ b/lib/pages/device_managment/all_devices/helper/route_controls_based_code.dart
@@ -7,6 +7,8 @@ import 'package:syncrow_web/pages/device_managment/ceiling_sensor/view/ceiling_s
import 'package:syncrow_web/pages/device_managment/ceiling_sensor/view/ceiling_sensor_controls.dart';
import 'package:syncrow_web/pages/device_managment/curtain/view/curtain_batch_status_view.dart';
import 'package:syncrow_web/pages/device_managment/curtain/view/curtain_status_view.dart';
+import 'package:syncrow_web/pages/device_managment/curtain_module/view/curtain_module_batch.dart';
+import 'package:syncrow_web/pages/device_managment/curtain_module/view/curtain_module_items.dart';
import 'package:syncrow_web/pages/device_managment/door_lock/view/door_lock_batch_control_view.dart';
import 'package:syncrow_web/pages/device_managment/door_lock/view/door_lock_control_view.dart';
import 'package:syncrow_web/pages/device_managment/flush_mounted_presence_sensor/views/flush_mounted_presence_sensor_batch_control_view.dart';
@@ -18,6 +20,7 @@ import 'package:syncrow_web/pages/device_managment/gateway/view/gateway_view.dar
import 'package:syncrow_web/pages/device_managment/main_door_sensor/view/main_door_control_view.dart';
import 'package:syncrow_web/pages/device_managment/main_door_sensor/view/main_door_sensor_batch_view.dart';
import 'package:syncrow_web/pages/device_managment/one_g_glass_switch/view/one_gang_glass_batch_control_view.dart';
+import 'package:syncrow_web/pages/device_managment/one_g_glass_switch/view/one_gang_glass_switch_control_view.dart';
import 'package:syncrow_web/pages/device_managment/one_gang_switch/view/wall_light_batch_control.dart';
import 'package:syncrow_web/pages/device_managment/one_gang_switch/view/wall_light_device_control.dart';
import 'package:syncrow_web/pages/device_managment/power_clamp/view/power_clamp_batch_control_view.dart';
@@ -39,8 +42,6 @@ import 'package:syncrow_web/pages/device_managment/water_heater/view/water_heate
import 'package:syncrow_web/pages/device_managment/water_leak/view/water_leak_batch_control_view.dart';
import 'package:syncrow_web/pages/device_managment/water_leak/view/water_leak_control_view.dart';
-import '../../one_g_glass_switch/view/one_gang_glass_switch_control_view.dart';
-
mixin RouteControlsBasedCode {
Widget routeControlsWidgets({required AllDevicesModel device}) {
switch (device.productType) {
@@ -84,6 +85,10 @@ mixin RouteControlsBasedCode {
return CurtainStatusControlsView(
deviceId: device.uuid!,
);
+ case 'CUR_2':
+ return CurtainModuleItems(
+ deviceId: device.uuid!,
+ );
case 'AC':
return AcDeviceControlsView(device: device);
case 'WH':
@@ -107,7 +112,7 @@ mixin RouteControlsBasedCode {
case 'SOS':
return SosDeviceControlsView(device: device);
- case 'NCPS':
+ case 'NCPS':
return FlushMountedPresenceSensorControlView(device: device);
default:
return const SizedBox();
@@ -132,76 +137,140 @@ mixin RouteControlsBasedCode {
switch (devices.first.productType) {
case '1G':
return WallLightBatchControlView(
- deviceIds: devices.where((e) => (e.productType == '1G')).map((e) => e.uuid!).toList(),
+ deviceIds: devices
+ .where((e) => e.productType == '1G')
+ .map((e) => e.uuid!)
+ .toList(),
);
case '2G':
return TwoGangBatchControlView(
- deviceIds: devices.where((e) => (e.productType == '2G')).map((e) => e.uuid!).toList(),
+ deviceIds: devices
+ .where((e) => e.productType == '2G')
+ .map((e) => e.uuid!)
+ .toList(),
);
case '3G':
return LivingRoomBatchControlsView(
- deviceIds: devices.where((e) => (e.productType == '3G')).map((e) => e.uuid!).toList(),
+ deviceIds: devices
+ .where((e) => e.productType == '3G')
+ .map((e) => e.uuid!)
+ .toList(),
);
case '1GT':
return OneGangGlassSwitchBatchControlView(
- deviceIds: devices.where((e) => (e.productType == '1GT')).map((e) => e.uuid!).toList(),
+ deviceIds: devices
+ .where((e) => e.productType == '1GT')
+ .map((e) => e.uuid!)
+ .toList(),
);
case '2GT':
return TwoGangGlassSwitchBatchControlView(
- deviceIds: devices.where((e) => (e.productType == '2GT')).map((e) => e.uuid!).toList(),
+ deviceIds: devices
+ .where((e) => e.productType == '2GT')
+ .map((e) => e.uuid!)
+ .toList(),
);
case '3GT':
return ThreeGangGlassSwitchBatchControlView(
- deviceIds: devices.where((e) => (e.productType == '3GT')).map((e) => e.uuid!).toList(),
+ deviceIds: devices
+ .where((e) => e.productType == '3GT')
+ .map((e) => e.uuid!)
+ .toList(),
);
case 'GW':
return GatewayBatchControlView(
- gatewayIds: devices.where((e) => (e.productType == 'GW')).map((e) => e.uuid!).toList(),
+ gatewayIds: devices
+ .where((e) => e.productType == 'GW')
+ .map((e) => e.uuid!)
+ .toList(),
);
case 'DL':
return DoorLockBatchControlView(
- devicesIds: devices.where((e) => (e.productType == 'DL')).map((e) => e.uuid!).toList());
+ devicesIds: devices
+ .where((e) => e.productType == 'DL')
+ .map((e) => e.uuid!)
+ .toList());
case 'WPS':
return WallSensorBatchControlView(
- devicesIds: devices.where((e) => (e.productType == 'WPS')).map((e) => e.uuid!).toList());
+ devicesIds: devices
+ .where((e) => e.productType == 'WPS')
+ .map((e) => e.uuid!)
+ .toList());
case 'CPS':
return CeilingSensorBatchControlView(
- devicesIds: devices.where((e) => (e.productType == 'CPS')).map((e) => e.uuid!).toList(),
+ devicesIds: devices
+ .where((e) => e.productType == 'CPS')
+ .map((e) => e.uuid!)
+ .toList(),
);
case 'CUR':
return CurtainBatchStatusView(
- devicesIds: devices.where((e) => (e.productType == 'CUR')).map((e) => e.uuid!).toList(),
+ devicesIds: devices
+ .where((e) => e.productType == 'CUR')
+ .map((e) => e.uuid!)
+ .toList(),
+ );
+ case 'CUR_2':
+ return CurtainModuleBatchView(
+ devicesIds: devices
+ .where((e) => e.productType == 'CUR_2')
+ .map((e) => e.uuid!)
+ .toList(),
);
case 'AC':
return AcDeviceBatchControlView(
- devicesIds: devices.where((e) => (e.productType == 'AC')).map((e) => e.uuid!).toList());
+ devicesIds: devices
+ .where((e) => e.productType == 'AC')
+ .map((e) => e.uuid!)
+ .toList());
case 'WH':
return WaterHEaterBatchControlView(
- deviceIds: devices.where((e) => (e.productType == 'WH')).map((e) => e.uuid!).toList(),
+ deviceIds: devices
+ .where((e) => e.productType == 'WH')
+ .map((e) => e.uuid!)
+ .toList(),
);
case 'DS':
return MainDoorSensorBatchView(
- devicesIds: devices.where((e) => (e.productType == 'DS')).map((e) => e.uuid!).toList(),
+ devicesIds: devices
+ .where((e) => e.productType == 'DS')
+ .map((e) => e.uuid!)
+ .toList(),
);
case 'GD':
return GarageDoorBatchControlView(
- deviceIds: devices.where((e) => (e.productType == 'GD')).map((e) => e.uuid!).toList(),
+ deviceIds: devices
+ .where((e) => e.productType == 'GD')
+ .map((e) => e.uuid!)
+ .toList(),
);
case 'WL':
return WaterLeakBatchControlView(
- deviceIds: devices.where((e) => (e.productType == 'WL')).map((e) => e.uuid!).toList(),
+ deviceIds: devices
+ .where((e) => e.productType == 'WL')
+ .map((e) => e.uuid!)
+ .toList(),
);
case 'PC':
return PowerClampBatchControlView(
- deviceIds: devices.where((e) => (e.productType == 'PC')).map((e) => e.uuid!).toList(),
+ deviceIds: devices
+ .where((e) => e.productType == 'PC')
+ .map((e) => e.uuid!)
+ .toList(),
);
case 'SOS':
return SOSBatchControlView(
- deviceIds: devices.where((e) => (e.productType == 'SOS')).map((e) => e.uuid!).toList(),
+ deviceIds: devices
+ .where((e) => e.productType == 'SOS')
+ .map((e) => e.uuid!)
+ .toList(),
);
case 'NCPS':
return FlushMountedPresenceSensorBatchControlView(
- devicesIds: devices.where((e) => (e.productType == 'NCPS')).map((e) => e.uuid!).toList(),
+ devicesIds: devices
+ .where((e) => e.productType == 'NCPS')
+ .map((e) => e.uuid!)
+ .toList(),
);
default:
return const SizedBox();
diff --git a/lib/pages/device_managment/all_devices/widgets/device_managment_body.dart b/lib/pages/device_managment/all_devices/widgets/device_managment_body.dart
index 8d108671..b4eb60e6 100644
--- a/lib/pages/device_managment/all_devices/widgets/device_managment_body.dart
+++ b/lib/pages/device_managment/all_devices/widgets/device_managment_body.dart
@@ -61,7 +61,8 @@ class DeviceManagementBody extends StatelessWidget with HelperResponsiveLayout {
final buttonLabel =
(selectedDevices.length > 1) ? 'Batch Control' : 'Control';
-
+ final isAnyDeviceOffline =
+ selectedDevices.any((element) => !(element.online ?? false));
return Row(
children: [
Expanded(child: SpaceTreeView(
@@ -102,8 +103,28 @@ class DeviceManagementBody extends StatelessWidget with HelperResponsiveLayout {
decoration: containerDecoration,
child: Center(
child: DefaultButton(
+ backgroundColor: isAnyDeviceOffline
+ ? ColorsManager.primaryColor
+ .withValues(alpha: 0.1)
+ : null,
onPressed: isControlButtonEnabled
? () {
+ if (isAnyDeviceOffline) {
+ ScaffoldMessenger.of(context)
+ .clearSnackBars();
+ ScaffoldMessenger.of(context)
+ .showSnackBar(
+ const SnackBar(
+ content: Text(
+ 'This Device is Offline',
+ ),
+ duration:
+ Duration(seconds: 2),
+ ),
+ );
+ return;
+ }
+
if (selectedDevices.length == 1) {
showDialog(
context: context,
diff --git a/lib/pages/device_managment/curtain_module/bloc/curtain_module_bloc.dart b/lib/pages/device_managment/curtain_module/bloc/curtain_module_bloc.dart
new file mode 100644
index 00000000..b40d7ea6
--- /dev/null
+++ b/lib/pages/device_managment/curtain_module/bloc/curtain_module_bloc.dart
@@ -0,0 +1,379 @@
+import 'dart:async';
+import 'package:bloc/bloc.dart';
+import 'package:equatable/equatable.dart';
+import 'package:firebase_database/firebase_database.dart';
+import 'package:syncrow_web/pages/device_managment/all_devices/models/device_status.dart';
+import 'package:syncrow_web/pages/device_managment/all_devices/models/factory_reset_model.dart';
+import 'package:syncrow_web/pages/device_managment/curtain_module/models/curtain_module_model.dart';
+import 'package:syncrow_web/services/batch_control_devices_service.dart';
+import 'package:syncrow_web/services/control_device_service.dart';
+import 'package:syncrow_web/services/devices_mang_api.dart';
+
+part 'curtain_module_event.dart';
+part 'curtain_module_state.dart';
+
+class CurtainModuleBloc extends Bloc {
+ final ControlDeviceService controlDeviceService;
+ final BatchControlDevicesService batchControlDevicesService;
+ StreamSubscription? _firebaseSubscription;
+
+ CurtainModuleBloc({
+ required this.controlDeviceService,
+ required this.batchControlDevicesService,
+ }) : super(CurtainModuleInitial()) {
+ on(_onFetchCurtainModuleStatusEvent);
+ on(_onSendCurtainPercentToApiEvent);
+ on(_onOpenCurtainEvent);
+ on(_onCloseCurtainEvent);
+ on(_onStopCurtainEvent);
+ on(_onChangeTimerControlEvent);
+ on(_onChageCurCalibrationEvent);
+ on(_onChangeElecMachineryModeEvent);
+ on(_onChangeControlBackEvent);
+ on(_onChangeControlBackModeEvent);
+ on(_onChangeCurtainModuleStatusEvent);
+ //batch
+ on(_onFetchCurtainModuleBatchStatus);
+ on(_onSendCurtainBatchPercentToApiEvent);
+ on(_onOpenCurtainBatchEvent);
+ on(_onCloseCurtainBatchEvent);
+ on(_onStopCurtainBatchEvent);
+ on(_onFactoryReset);
+ }
+
+ Future _onFetchCurtainModuleStatusEvent(
+ FetchCurtainModuleStatusEvent event,
+ Emitter emit,
+ ) async {
+ emit(CurtainModuleLoading());
+ final status = await DevicesManagementApi().getDeviceStatus(event.deviceId);
+ final result = Map.fromEntries(
+ status.status.map((element) => MapEntry(element.code, element.value)),
+ );
+
+ emit(CurtainModuleStatusLoaded(
+ curtainModuleStatus: CurtainModuleStatusModel.fromJson(result),
+ ));
+ Map statusMap = {};
+ final ref =
+ FirebaseDatabase.instance.ref('device-status/${event.deviceId}');
+ final stream = ref.onValue;
+
+ stream.listen((DatabaseEvent DatabaseEvent) async {
+ if (DatabaseEvent.snapshot.value == null) return;
+
+ Map usersMap =
+ DatabaseEvent.snapshot.value as Map;
+
+ List statusList = [];
+
+ usersMap['status'].forEach((element) {
+ statusList.add(Status(code: element['code'], value: element['value']));
+ });
+
+ statusMap = {
+ for (final element in statusList) element.code: element.value,
+ };
+ if (!isClosed) {
+ add(
+ ChangeCurtainModuleStatusEvent(
+ deviceId: event.deviceId,
+ status: CurtainModuleStatusModel.fromJson(statusMap),
+ ),
+ );
+ }
+ });
+ }
+
+ Future _onChangeCurtainModuleStatusEvent(
+ ChangeCurtainModuleStatusEvent event,
+ Emitter emit,
+ ) async {
+ emit(CurtainModuleLoading());
+ emit(CurtainModuleStatusLoaded(curtainModuleStatus: event.status));
+ }
+
+ Future _onSendCurtainPercentToApiEvent(
+ SendCurtainPercentToApiEvent event,
+ Emitter emit,
+ ) async {
+ try {
+ await controlDeviceService.controlDevice(
+ deviceUuid: event.deviceId,
+ status: event.status,
+ );
+ } catch (e) {
+ emit(CurtainModuleError(message: 'Failed to send control command: $e'));
+ }
+ }
+
+ Future _onOpenCurtainEvent(
+ OpenCurtainEvent event,
+ Emitter emit,
+ ) async {
+ try {
+ await controlDeviceService.controlDevice(
+ deviceUuid: event.deviceId,
+ status: Status(code: 'control', value: 'open'),
+ );
+ } catch (e) {
+ emit(CurtainModuleError(message: 'Failed to open curtain: $e'));
+ }
+ }
+
+ Future _onCloseCurtainEvent(
+ CloseCurtainEvent event,
+ Emitter emit,
+ ) async {
+ try {
+ await controlDeviceService.controlDevice(
+ deviceUuid: event.deviceId,
+ status: Status(code: 'control', value: 'close'),
+ );
+ } catch (e) {
+ emit(CurtainModuleError(message: 'Failed to close curtain: $e'));
+ }
+ }
+
+ Future _onStopCurtainEvent(
+ StopCurtainEvent event,
+ Emitter emit,
+ ) async {
+ try {
+ await controlDeviceService.controlDevice(
+ deviceUuid: event.deviceId,
+ status: Status(code: 'control', value: 'stop'),
+ );
+ } catch (e) {
+ emit(CurtainModuleError(message: 'Failed to stop curtain: $e'));
+ }
+ }
+
+ Future _onChangeTimerControlEvent(
+ ChangeTimerControlEvent event,
+ Emitter emit,
+ ) async {
+ try {
+ if (event.timControl < 10 || event.timControl > 120) {
+ emit(const CurtainModuleError(
+ message: 'Timer control value must be between 10 and 120'));
+ return;
+ }
+ await controlDeviceService.controlDevice(
+ deviceUuid: event.deviceId,
+ status: Status(
+ code: 'tr_timecon',
+ value: event.timControl,
+ ),
+ );
+ } catch (e) {
+ emit(CurtainModuleError(message: 'Failed to change timer control: $e'));
+ }
+ }
+
+ Future _onChageCurCalibrationEvent(
+ CurCalibrationEvent event,
+ Emitter emit,
+ ) async {
+ try {
+ await controlDeviceService.controlDevice(
+ deviceUuid: event.deviceId,
+ status: Status(code: 'cur_calibration', value: 'start'),
+ );
+ } catch (e) {
+ emit(CurtainModuleError(message: 'Failed to start calibration: $e'));
+ }
+ }
+
+ Future _onChangeElecMachineryModeEvent(
+ ChangeElecMachineryModeEvent event,
+ Emitter emit,
+ ) async {
+ try {
+ await controlDeviceService.controlDevice(
+ deviceUuid: event.deviceId,
+ status: Status(
+ code: 'elec_machinery_mode',
+ value: event.elecMachineryMode,
+ ),
+ );
+ } catch (e) {
+ emit(CurtainModuleError(message: 'Failed to change mode: $e'));
+ }
+ }
+
+ Future _onChangeControlBackEvent(
+ ChangeControlBackEvent event,
+ Emitter emit,
+ ) async {
+ try {
+ await controlDeviceService.controlDevice(
+ deviceUuid: event.deviceId,
+ status: Status(
+ code: 'control_back',
+ value: event.controlBack,
+ ),
+ );
+ } catch (e) {
+ emit(CurtainModuleError(message: 'Failed to change control back: $e'));
+ }
+ }
+
+ Future _onChangeControlBackModeEvent(
+ ChangeControlBackModeEvent event,
+ Emitter emit,
+ ) async {
+ try {
+ await controlDeviceService.controlDevice(
+ deviceUuid: event.deviceId,
+ status: Status(
+ code: 'control_back_mode',
+ value: event.controlBackMode,
+ ),
+ );
+ } catch (e) {
+ emit(CurtainModuleError(
+ message: 'Failed to change control back mode: $e'));
+ }
+ }
+
+ FutureOr _onFetchCurtainModuleBatchStatus(
+ CurtainModuleFetchBatchStatusEvent event,
+ Emitter emit,
+ ) async {
+ emit(CurtainModuleLoading());
+ try {
+ final status =
+ await DevicesManagementApi().getBatchStatus(event.devicesIds);
+
+ final result = Map.fromEntries(
+ status.status.map((element) => MapEntry(element.code, element.value)),
+ );
+
+ emit(CurtainModuleStatusLoaded(
+ curtainModuleStatus: CurtainModuleStatusModel.fromJson(result),
+ ));
+
+ Map statusMap = {};
+ final ref = FirebaseDatabase.instance
+ .ref('device-status/${event.devicesIds.first}');
+ final stream = ref.onValue;
+
+ stream.listen((DatabaseEvent DatabaseEvent) async {
+ if (DatabaseEvent.snapshot.value == null) return;
+
+ Map usersMap =
+ DatabaseEvent.snapshot.value as Map;
+
+ List statusList = [];
+
+ usersMap['status'].forEach((element) {
+ statusList
+ .add(Status(code: element['code'], value: element['value']));
+ });
+
+ statusMap = {
+ for (final element in statusList) element.code: element.value,
+ };
+ if (!isClosed) {
+ add(
+ ChangeCurtainModuleStatusEvent(
+ deviceId: event.devicesIds.first,
+ status: CurtainModuleStatusModel.fromJson(statusMap),
+ ),
+ );
+ }
+ });
+ } catch (e) {
+ emit(CurtainModuleError(message: e.toString()));
+ }
+ }
+
+ Future _onSendCurtainBatchPercentToApiEvent(
+ SendCurtainBatchPercentToApiEvent event,
+ Emitter emit,
+ ) async {
+ try {
+ await batchControlDevicesService.batchControlDevices(
+ uuids: event.devicesId,
+ code: event.status.code,
+ value: event.status.value,
+ );
+ } catch (e) {
+ emit(CurtainModuleError(message: 'Failed to send control command: $e'));
+ }
+ }
+
+ Future _onOpenCurtainBatchEvent(
+ OpenCurtainBatchEvent event,
+ Emitter emit,
+ ) async {
+ try {
+ await batchControlDevicesService.batchControlDevices(
+ uuids: event.devicesId,
+ code: 'control',
+ value: 'open',
+ );
+ } catch (e) {
+ emit(CurtainModuleError(message: 'Failed to open curtain: $e'));
+ }
+ }
+
+ Future _onCloseCurtainBatchEvent(
+ CloseCurtainBatchEvent event,
+ Emitter emit,
+ ) async {
+ try {
+ await batchControlDevicesService.batchControlDevices(
+ uuids: event.devicesId,
+ code: 'control',
+ value: 'close',
+ );
+ } catch (e) {
+ emit(CurtainModuleError(message: 'Failed to close curtain: $e'));
+ }
+ }
+
+ Future _onStopCurtainBatchEvent(
+ StopCurtainBatchEvent event,
+ Emitter emit,
+ ) async {
+ try {
+ await batchControlDevicesService.batchControlDevices(
+ uuids: event.devicesId,
+ code: 'control',
+ value: 'stop',
+ );
+ } catch (e) {
+ emit(CurtainModuleError(message: 'Failed to stop curtain: $e'));
+ }
+ }
+
+ Future _onFactoryReset(
+ CurtainModuleFactoryReset event,
+ Emitter emit,
+ ) async {
+ emit(CurtainModuleLoading());
+ try {
+ final response = await DevicesManagementApi().factoryReset(
+ event.factoryReset,
+ event.deviceId,
+ );
+ if (!response) {
+ emit(const CurtainModuleError(message: 'Failed'));
+ } else {
+ add(
+ FetchCurtainModuleStatusEvent(deviceId: event.deviceId),
+ );
+ }
+ } catch (e) {
+ emit(CurtainModuleError(message: e.toString()));
+ }
+ }
+
+ @override
+ Future close() async {
+ await _firebaseSubscription?.cancel();
+ return super.close();
+ }
+}
diff --git a/lib/pages/device_managment/curtain_module/bloc/curtain_module_event.dart b/lib/pages/device_managment/curtain_module/bloc/curtain_module_event.dart
new file mode 100644
index 00000000..4eec030d
--- /dev/null
+++ b/lib/pages/device_managment/curtain_module/bloc/curtain_module_event.dart
@@ -0,0 +1,193 @@
+part of 'curtain_module_bloc.dart';
+
+sealed class CurtainModuleEvent extends Equatable {
+ const CurtainModuleEvent();
+
+ @override
+ List