Compare commits

..
Author SHA1 Message Date
mohammad 78f42dacf6 Adjust ConditionToggle widget dimensions and colors for improved UI consistency 2025-06-01 14:37:42 +03:00
mohammad 0a9d53e5bd Refactor ConditionToggle widget to display icons with corresponding conditions 2025-05-29 10:48:12 +03:00
mohammadnemer1 010960c89b Merge pull request #208 from SyncrowIOT/SP-1603-FE-Freeze-First-Row-in-All-Table-Views-Across-the-Platform
Refactor table layout to accommodate dynamic table size
2025-05-28 16:57:56 +03:00
mohammad fccf395c38 Update function names to follow consistent naming convention in name_filter.dart and users_page.dart 2025-05-28 16:56:51 +03:00
mohammad 7c65b874eb Refactor table layout to accommodate dynamic table size 2025-05-28 16:40:44 +03:00
Faris Armoush 25db6ec687 Created pull_request_template.md . 2025-05-28 14:24:03 +03:00
Faris Armoush 7f5d2ca6ea Merge pull request #206 from SyncrowIOT/SP-1592-FE-Build-AQI-Breakdown-Percentage-Chart-with-Standard-Color-Codes
SP-1592-FE-Build-AQI-Breakdown-Percentage-Chart-with-Standard-Color-Codes
2025-05-27 15:17:10 +03:00
Faris Armoush 5a5173c19b Merge branch 'dev' of https://github.com/SyncrowIOT/web into SP-1592-FE-Build-AQI-Breakdown-Percentage-Chart-with-Standard-Color-Codes 2025-05-27 15:16:37 +03:00
Faris Armoush 83363b4c50 Made RangeOfAqiChart._lines colors use ColorsManager colors instead of statically defining them in the widget itself using Hex codes. 2025-05-27 15:15:29 +03:00
mohammadnemer1 6ebdc59966 Merge pull request #207 from SyncrowIOT/Fix-Bugs-Related-TextForm-Routine
Add 'PC' device to routine
2025-05-27 14:56:11 +03:00
Faris Armoush 5f3a0c74ac Merge branch 'dev' of https://github.com/SyncrowIOT/web into SP-1592-FE-Build-AQI-Breakdown-Percentage-Chart-with-Standard-Color-Codes 2025-05-27 14:06:48 +03:00
Faris Armoush 03009ed276 made a RangeOfAqiChart._lines a getter. 2025-05-27 13:21:42 +03:00
Faris Armoush a1142eb38c gave range of aqi chart a tooltip that shows the necessary data. 2025-05-27 13:17:05 +03:00
Faris Armoush 1aa7bf2162 fixed charts clipping overflow in chart. 2025-05-27 12:37:36 +03:00
Faris Armoush 043820f84f does not emit an entirely new state when we already have chart data on loading. 2025-05-27 12:33:48 +03:00
Faris Armoush d90d3d4026 added loading state to range of aqi chart. 2025-05-27 12:29:06 +03:00
Faris Armoush 3ac5254abf fixed bug in total energy consumption chart. 2025-05-27 12:26:21 +03:00
Faris Armoush f5d926f5a2 modify left side titles. 2025-05-27 12:21:59 +03:00
mohammadnemer1 c1d6db8bba Merge branch 'dev' into Fix-Bugs-Related-TextForm-Routine 2025-05-27 09:56:07 +03:00
mohammad 50fc5f9562 Add 'PC' device to routine 2025-05-27 09:54:21 +03:00
Faris Armoush 1b0d8d446c modified flex's values. 2025-05-27 09:47:06 +03:00
Faris Armoush 8a5173f429 made font size of AqiTypeDropdown slightly smaller. 2025-05-27 09:36:35 +03:00
Faris Armoush bee8652d03 responsivness 2025-05-26 16:59:44 +03:00
Faris Armoush 9546d7bdd1 fixed titles widget for bottom title. 2025-05-26 16:56:38 +03:00
Faris Armoush cb4956f915 made range of aqi fake data random and not linear. 2025-05-26 16:56:25 +03:00
Faris Armoush ec7b0aa078 shows AnalyticsErrorWidget and spacing under it only when there is an error. 2025-05-26 15:58:34 +03:00
Faris Armoush 296b03e1aa shows month data instead of index on bottom titles of RangeOfAqiChart. 2025-05-26 15:54:56 +03:00
Faris Armoush 177c7f1030 Responsiveness of RangeOfAqiChartTitle. 2025-05-26 15:49:38 +03:00
Faris Armoush 3746c36a71 Merge pull request #205 from SyncrowIOT/sp-1493-data-foramtting-2.0
sp-1493-data-formatting-2.0.
2025-05-26 15:19:18 +03:00
Faris Armoush 0b4337fb6c sp-1493-data-formatting-2.0. 2025-05-26 15:17:29 +03:00
Faris Armoush 171dc52e28 Created AqiTypeDropdown. 2025-05-26 15:10:30 +03:00
Faris Armoush 642d8e9591 Merge pull request #204 from SyncrowIOT/SP-1493-data-formatting
SP-1493-data-formatting
2025-05-26 14:24:46 +03:00
Faris Armoush 5a8ef578c3 SP-1493-data-formatting 2025-05-26 14:16:43 +03:00
Faris Armoush 63ca98895f moved RangeOfAqiChartTitle. 2025-05-26 13:27:12 +03:00
Faris Armoush 7e54cfdccd Implemented min, max, average informative cells to RangeOfAqiChartBox. 2025-05-26 13:25:14 +03:00
Faris Armoush fb4d44450f Disabled animation in RangeOfAqiChart. 2025-05-26 11:25:12 +03:00
Faris Armoush 12e4285b14 removed unnecessary Stack widget from RangeOfAqiChart. 2025-05-26 11:24:53 +03:00
Faris Armoush 82adbcf4df loads and clears aqi range data in FetchAirQualityDataHelper. 2025-05-26 11:24:00 +03:00
Faris Armoush 7305d511bc Added spaceUuid to GetRangeOfAqiParam model. 2025-05-26 11:23:33 +03:00
Faris Armoush 61acaa17c5 fixed typo. 2025-05-26 11:22:11 +03:00
Faris Armoush 4af81bcc10 make the aqi range chart read its data from RangeOfAqiBloc. 2025-05-26 11:22:05 +03:00
Faris Armoush d4dd7a19ba make the generated fake aqi range data, look better on the chart. 2025-05-26 11:21:42 +03:00
Faris Armoush 9ab906d24c Injected RangeOfAqiBloc into AnalyticsPage. 2025-05-26 11:10:23 +03:00
Faris Armoush 5c57143ea5 Created RangeOfAqiBloc along with its events, and state. 2025-05-26 11:09:45 +03:00
Faris Armoush 4a3085e1b4 Created RangeOfAqiService along with its fake implementation until the API is ready. 2025-05-26 11:00:57 +03:00
Faris Armoush eb8ba1806c Created GetRangeOfAqiParam model. 2025-05-26 10:59:07 +03:00
Faris Armoush 902419f9c4 Created RangeOfAqi model. 2025-05-26 10:58:05 +03:00
Faris Armoush 926bcd9a5d Extracted lines data into a helper method for ease of readability. 2025-05-26 10:48:04 +03:00
Faris Armoush 33f9add78a Extracted some logic of RangeOfAqiChart into a helper class. 2025-05-26 10:41:36 +03:00
Faris Armoush 563a3e1cf5 Refactored RangeOfAqiChart to consolidate line chart creation into a reusable method, improving code maintainability and reducing duplication. 2025-05-26 10:31:21 +03:00
Faris Armoush 791b71276a populated linear data for RangeOfAqiChart, for a more pleasant dev experience and debugging. 2025-05-26 10:29:38 +03:00
Faris Armoush 24e3eb2311 extracted titlesData into a private factory method to enahnce readability. 2025-05-26 10:18:15 +03:00
Faris Armoush 82006e9aaf Implemented the side titles of RangeOfAqiChart. 2025-05-26 10:12:52 +03:00
Faris Armoush cedef666f6 Merge pull request #202 from SyncrowIOT/SP-1493-rework
SP-1493 rework
2025-05-26 10:03:10 +03:00
Faris Armoush a10d998ec6 Merge pull request #203 from SyncrowIOT/SP-1513-rework
SP-1513-rework
2025-05-26 10:02:49 +03:00
Faris Armoush ed50ac03d3 Merge pull request #201 from SyncrowIOT/SP-1492-landing_page_analytics_button_design
SP-1492-landing_page_analytics_button_design
2025-05-26 09:57:23 +03:00
Faris Armoush cd2eb46f49 Implemented the overall design of RangeOfAqiChart, whats left is 100% matching it with the figma design. 2025-05-26 09:50:53 +03:00
Faris Armoush 39351a710d Added aqi info colors to ColorsManager. 2025-05-25 12:18:09 +03:00
Faris Armoush c8fe4e3baa Created an initial version of RangeOfAqiChart. 2025-05-25 12:01:45 +03:00
Faris Armoush 12deceb7d3 SP-1513-rework 2025-05-25 11:35:01 +03:00
Faris Armoush 9d27ed2dc5 SP-1506 rework, coloring and padding. 2025-05-25 11:13:24 +03:00
Faris Armoush a878b9328a SP-1493 rework, can select a subspace in sidebar even when the space has no child-spaces. 2025-05-25 11:06:36 +03:00
Faris Armoush 0f9227a6f5 Merge pull request #200 from SyncrowIOT/SP-1591-FE-Implement-Space-Level-Structure-Selection-and-Air-Quality-Device-Dropdown
Sp 1591 fe implement space level structure selection and air quality device dropdown
2025-05-22 15:59:19 +03:00
Faris Armoush 5b13962d41 removed unnecessary * 1 calculation of height. 2025-05-22 15:57:03 +03:00
Faris Armoush 8c53d5322a SP-1591 2025-05-22 15:53:18 +03:00
Faris Armoush af4d37939b Merge branch 'dev' of https://github.com/SyncrowIOT/web into SP-1591-FE-Implement-Space-Level-Structure-Selection-and-Air-Quality-Device-Dropdown 2025-05-22 15:48:47 +03:00
Faris Armoush d43c1847ff SP-1591 2025-05-22 15:44:19 +03:00
Faris Armoush 4c5b390887 Fixed typos. 2025-05-22 15:42:49 +03:00
Faris Armoush 5eeac01666 cannot select a community in AirQualityDataLoadingStrategy. 2025-05-22 15:35:04 +03:00
Faris Armoush 717d698378 can select child spaces with children in AirQualityDataLoadingStrategy. 2025-05-22 15:23:42 +03:00
Faris Armoush 9adbbb9a2d Integrated and implemented devices dropdown into the newly created widget AirQualityEndSideWidget. 2025-05-22 15:19:50 +03:00
Faris Armoush e792dbd72f SP-1591/ Implement business logic in AirQualityDataLoadingStrategy for community structure loading strategy. 2025-05-22 14:58:42 +03:00
Faris Armoush d2eea33714 Prepared AirQualityView layout and structure with PlaceHolder widgets. 2025-05-22 12:24:13 +03:00
Faris Armoush 5a61647fe4 Prepared and created the necessary component for the air quality loading strategy for the side bar selection, and for loading data in different parts of the UI. 2025-05-21 16:49:30 +03:00
Faris Armoush 568b6be354 Created AirQualityView widget for the new Air Quality analytics module. 2025-05-21 16:46:38 +03:00
56 changed files with 2633 additions and 526 deletions
+30
View File
@@ -0,0 +1,30 @@
<!--
Thanks for contributing!
Provide a description of your changes below and a general summary in the title
Please look at the following checklist to ensure that your PR can be accepted quickly:
-->
## Jira Ticket
<!-- Add your Jira ticket number as a link (e.g., [PROJ-123](https://jira.company.com/browse/PROJ-123)) -->
## Status
**READY/IN DEVELOPMENT/HOLD**
## Description
<!--- Describe your changes in detail -->
## Type of Change
<!--- Put an `x` in all the boxes that apply: -->
- [ ] ✨ New feature (non-breaking change which adds functionality)
- [ ] 🛠️ Bug fix (non-breaking change which fixes an issue)
- [ ] ❌ Breaking change (fix or feature that would cause existing functionality to change)
- [ ] 🧹 Code refactor
- [ ] ✅ Build configuration change
- [ ] 📝 Documentation
- [ ] 🗑️ Chore
+10
View File
@@ -0,0 +1,10 @@
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M20 6.90237L13.6328 13.1837L6.60156 6.1524L2.32422 10.418L1.50391 9.59769L6.60156 4.48828L13.6328 11.5195L19.1797 6.08206L20 6.90237Z" fill="#64E1DC"/>
<path d="M20 13.1133L19.1797 13.9336L13.6328 8.49615L7.77344 14.3555L5.42969 12.0118L2.32422 15.1055L1.50391 14.2852L5.42969 10.3477L7.77344 12.6914L13.6328 6.83203L20 13.1133Z" fill="#FDBF00"/>
<path d="M20 6.90234L13.6328 13.1836L10.1172 9.668V8.00388L13.6328 11.5195L19.1797 6.08203L20 6.90234Z" fill="#00C8C8"/>
<path d="M20 13.1133L19.1797 13.9336L13.6328 8.49615L10.1172 12.0118V10.3477L13.6328 6.83203L20 13.1133Z" fill="#FF9100"/>
<path d="M19.1714 17.625V18.7813L17.7184 18.7821L10.1172 18.7851L1.32812 18.7891V0.75H2.5V17.625H19.1714Z" fill="#676E74"/>
<path d="M3.0127 2.37976L1.91406 1.50024L0.732422 2.37976L0 1.46423L1.91406 0L3.74512 1.46423L3.0127 2.37976Z" fill="#676E74"/>
<path d="M19.1714 17.625V18.7813L17.7176 18.7824L17.7184 18.7821L10.1172 18.7851V17.625H19.1714Z" fill="#474F54"/>
<path d="M19.9998 18.2108L18.1205 19.9999L17.292 19.1714L17.7174 18.7823L17.7182 18.782L18.3427 18.2108L17.7565 17.6249L17.292 17.1604L18.1205 16.332L19.9998 18.2108Z" fill="#474F54"/>
</svg>

After

Width:  |  Height:  |  Size: 1.2 KiB

@@ -0,0 +1,18 @@
import 'package:equatable/equatable.dart';
class RangeOfAqi extends Equatable {
final double min;
final double avg;
final double max;
final DateTime date;
const RangeOfAqi({
required this.min,
required this.avg,
required this.max,
required this.date,
});
@override
List<Object?> get props => [min, avg, max, date];
}
@@ -0,0 +1,42 @@
import 'package:bloc/bloc.dart';
import 'package:equatable/equatable.dart';
import 'package:syncrow_web/pages/analytics/models/range_of_aqi.dart';
import 'package:syncrow_web/pages/analytics/params/get_range_of_aqi_param.dart';
import 'package:syncrow_web/pages/analytics/services/range_of_aqi/range_of_aqi_service.dart';
part 'range_of_aqi_event.dart';
part 'range_of_aqi_state.dart';
class RangeOfAqiBloc extends Bloc<RangeOfAqiEvent, RangeOfAqiState> {
RangeOfAqiBloc(this._rangeOfAqiService) : super(const RangeOfAqiState()) {
on<LoadRangeOfAqiEvent>(_onLoadRangeOfAqiEvent);
on<ClearRangeOfAqiEvent>(_onClearRangeOfAqiEvent);
}
final RangeOfAqiService _rangeOfAqiService;
Future<void> _onLoadRangeOfAqiEvent(
LoadRangeOfAqiEvent event,
Emitter<RangeOfAqiState> emit,
) async {
emit(
RangeOfAqiState(
status: RangeOfAqiStatus.loading,
rangeOfAqi: state.rangeOfAqi,
),
);
try {
final rangeOfAqi = await _rangeOfAqiService.load(event.param);
emit(RangeOfAqiState(status: RangeOfAqiStatus.loaded, rangeOfAqi: rangeOfAqi));
} catch (e) {
emit(RangeOfAqiState(status: RangeOfAqiStatus.failure, errorMessage: '$e'));
}
}
void _onClearRangeOfAqiEvent(
ClearRangeOfAqiEvent event,
Emitter<RangeOfAqiState> emit,
) {
emit(const RangeOfAqiState());
}
}
@@ -0,0 +1,21 @@
part of 'range_of_aqi_bloc.dart';
sealed class RangeOfAqiEvent extends Equatable {
const RangeOfAqiEvent();
@override
List<Object> get props => [];
}
class LoadRangeOfAqiEvent extends RangeOfAqiEvent {
const LoadRangeOfAqiEvent(this.param);
final GetRangeOfAqiParam param;
@override
List<Object> get props => [param];
}
class ClearRangeOfAqiEvent extends RangeOfAqiEvent {
const ClearRangeOfAqiEvent();
}
@@ -0,0 +1,18 @@
part of 'range_of_aqi_bloc.dart';
enum RangeOfAqiStatus { initial, loading, loaded, failure }
final class RangeOfAqiState extends Equatable {
const RangeOfAqiState({
this.rangeOfAqi = const [],
this.status = RangeOfAqiStatus.initial,
this.errorMessage,
});
final RangeOfAqiStatus status;
final List<RangeOfAqi> rangeOfAqi;
final String? errorMessage;
@override
List<Object?> get props => [status, rangeOfAqi, errorMessage];
}
@@ -0,0 +1,82 @@
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:syncrow_web/pages/analytics/modules/air_quality/blocs/range_of_aqi/range_of_aqi_bloc.dart';
import 'package:syncrow_web/pages/analytics/modules/air_quality/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_analytics_devices_param.dart';
import 'package:syncrow_web/pages/analytics/params/get_range_of_aqi_param.dart';
abstract final class FetchAirQualityDataHelper {
const FetchAirQualityDataHelper._();
static void loadAirQualityData(
BuildContext context, {
required String communityUuid,
required String spaceUuid,
}) {
final date = context.read<AnalyticsDatePickerBloc>().state.monthlyDate;
loadAnalyticsDevices(
context,
communityUuid: communityUuid,
spaceUuid: spaceUuid,
);
loadRangeOfAqi(
context,
spaceUuid: spaceUuid,
date: date,
aqiType: AqiType.aqi,
);
}
static void clearAllData(BuildContext context) {
context.read<AnalyticsDevicesBloc>().add(
const ClearAnalyticsDeviceEvent(),
);
context.read<RealtimeDeviceChangesBloc>().add(
const RealtimeDeviceChangesClosed(),
);
context.read<RangeOfAqiBloc>().add(const ClearRangeOfAqiEvent());
}
static void loadAnalyticsDevices(
BuildContext context, {
required String communityUuid,
required String spaceUuid,
}) {
context.read<AnalyticsDevicesBloc>().add(
LoadAnalyticsDevicesEvent(
param: GetAnalyticsDevicesParam(
communityUuid: communityUuid,
spaceUuid: spaceUuid,
deviceTypes: ['AQI'],
requestType: AnalyticsDeviceRequestType.energyManagement,
),
onSuccess: (device) {
context.read<RealtimeDeviceChangesBloc>()
..add(const RealtimeDeviceChangesClosed())
..add(RealtimeDeviceChangesStarted(device.uuid));
},
),
);
}
static void loadRangeOfAqi(
BuildContext context, {
required String spaceUuid,
required DateTime date,
required AqiType aqiType,
}) {
context.read<RangeOfAqiBloc>().add(
LoadRangeOfAqiEvent(
GetRangeOfAqiParam(
date: date,
spaceUuid: spaceUuid,
aqiType: aqiType,
),
),
);
}
}
@@ -0,0 +1,115 @@
import 'package:fl_chart/fl_chart.dart';
import 'package:flutter/material.dart';
import 'package:intl/intl.dart';
import 'package:syncrow_web/pages/analytics/models/range_of_aqi.dart';
import 'package:syncrow_web/pages/analytics/modules/energy_management/helpers/energy_management_charts_helper.dart';
import 'package:syncrow_web/utils/color_manager.dart';
import 'package:syncrow_web/utils/extension/build_context_x.dart';
abstract final class RangeOfAqiChartsHelper {
const RangeOfAqiChartsHelper._();
static const gradientData = <(Color color, String label)>[
(ColorsManager.goodGreen, 'Good'),
(ColorsManager.moderateYellow, 'Moderate'),
(ColorsManager.poorOrange, 'Poor'),
(ColorsManager.unhealthyRed, 'Unhealthy'),
(ColorsManager.severePink, 'Severe'),
(ColorsManager.hazardousPurple, 'Hazardous'),
];
static FlTitlesData titlesData(BuildContext context, List<RangeOfAqi> data) {
final titlesData = EnergyManagementChartsHelper.titlesData(context);
return titlesData.copyWith(
bottomTitles: titlesData.bottomTitles.copyWith(
sideTitles: titlesData.bottomTitles.sideTitles.copyWith(
getTitlesWidget: (value, meta) => Padding(
padding: const EdgeInsetsDirectional.only(top: 20.0),
child: Text(
data.isNotEmpty ? data[value.toInt()].date.day.toString() : '',
style: context.textTheme.bodySmall?.copyWith(
fontSize: 12,
color: ColorsManager.lightGreyColor,
),
),
),
),
),
leftTitles: titlesData.leftTitles.copyWith(
sideTitles: titlesData.leftTitles.sideTitles.copyWith(
reservedSize: 70,
interval: 50,
maxIncluded: false,
getTitlesWidget: (value, meta) {
final text = value >= 300 ? '301+' : value.toInt().toString();
return Padding(
padding: const EdgeInsetsDirectional.only(end: 12),
child: FittedBox(
alignment: AlignmentDirectional.centerStart,
fit: BoxFit.scaleDown,
child: Text(
text,
style: context.textTheme.bodySmall?.copyWith(
fontSize: 12,
color: ColorsManager.lightGreyColor,
),
),
),
);
},
),
),
);
}
static List<LineTooltipItem?> getTooltipItems(
List<LineBarSpot> touchedSpots,
List<RangeOfAqi> chartData,
) {
return touchedSpots.asMap().entries.map((entry) {
final index = entry.key;
final spot = entry.value;
final label = switch (spot.barIndex) {
0 => 'Max',
1 => 'Avg',
2 => 'Min',
_ => '',
};
final date = DateFormat('dd/MM').format(chartData[spot.x.toInt()].date);
return LineTooltipItem(
index == 0 ? '$date\n' : '',
const TextStyle(
color: ColorsManager.textPrimaryColor,
fontWeight: FontWeight.w600,
fontSize: 12,
),
children: [
TextSpan(text: '$label: ${spot.y.toStringAsFixed(0)}'),
],
);
}).toList();
}
static LineTouchData lineTouchData(
List<RangeOfAqi> chartData,
) {
return LineTouchData(
touchTooltipData: LineTouchTooltipData(
getTooltipColor: (touchTooltipItem) => ColorsManager.whiteColors,
tooltipBorder: const BorderSide(
color: ColorsManager.semiTransparentBlack,
),
tooltipRoundedRadius: 16,
showOnTopOfTheChartBoxArea: false,
tooltipPadding: const EdgeInsets.all(8),
getTooltipItems: (touchedSpots) => RangeOfAqiChartsHelper.getTooltipItems(
touchedSpots,
chartData,
),
),
);
}
}
@@ -0,0 +1,64 @@
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/range_of_aqi_chart_box.dart';
class AirQualityView extends StatelessWidget {
const AirQualityView({super.key});
static const _padding = EdgeInsetsDirectional.all(32);
@override
Widget build(BuildContext context) {
return LayoutBuilder(
builder: (context, constraints) {
final isMediumOrLess = constraints.maxWidth <= 900;
final height = MediaQuery.sizeOf(context).height;
if (isMediumOrLess) {
return SingleChildScrollView(
padding: _padding,
child: Column(
spacing: 32,
children: [
SizedBox(
height: height * 1.2,
child: const AirQualityEndSideWidget(),
),
SizedBox(height: height * 0.5, child: const RangeOfAqiChartBox()),
SizedBox(height: height * 0.5, child: const Placeholder()),
],
),
);
}
return SingleChildScrollView(
child: Container(
padding: _padding,
height: height,
child: const Column(
children: [
Expanded(
child: Row(
spacing: 32,
children: [
Expanded(
flex: 5,
child: Column(
spacing: 20,
children: [
Expanded(child: RangeOfAqiChartBox()),
Expanded(child: Placeholder()),
],
),
),
Expanded(flex: 2, child: AirQualityEndSideWidget()),
],
),
),
],
),
),
);
},
);
}
}
@@ -0,0 +1,86 @@
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_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/helpers/fetch_energy_management_data_helper.dart';
import 'package:syncrow_web/pages/analytics/modules/energy_management/widgets/analytics_device_dropdown.dart';
import 'package:syncrow_web/utils/color_manager.dart';
import 'package:syncrow_web/utils/extension/build_context_x.dart';
import 'package:syncrow_web/utils/style.dart';
class AirQualityEndSideWidget extends StatelessWidget {
const AirQualityEndSideWidget({super.key});
@override
Widget build(BuildContext context) {
return Container(
decoration: subSectionContainerDecoration.copyWith(
borderRadius: BorderRadius.circular(30),
),
padding: const EdgeInsetsDirectional.all(32),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_buildHeader(context),
Text(
'Device ID:',
style: context.textTheme.bodySmall?.copyWith(
color: ColorsManager.textPrimaryColor,
fontWeight: FontWeight.w400,
fontSize: 12,
),
),
const SizedBox(height: 6),
SelectableText(
context.watch<AnalyticsDevicesBloc>().state.selectedDevice?.uuid ??
'N/A',
style: context.textTheme.bodySmall?.copyWith(
color: ColorsManager.blackColor,
fontWeight: FontWeight.w400,
fontSize: 12,
),
),
],
),
);
}
Widget _buildHeader(BuildContext context) {
return Row(
mainAxisSize: MainAxisSize.min,
children: [
Expanded(
flex: 3,
child: FittedBox(
alignment: AlignmentDirectional.centerStart,
child: SelectableText(
'AQI Sensor',
style: context.textTheme.headlineSmall?.copyWith(
fontWeight: FontWeight.w700,
color: ColorsManager.vividBlue.withValues(alpha: 0.6),
fontSize: 18,
),
),
),
),
const Spacer(),
Expanded(
flex: 4,
child: FittedBox(
alignment: AlignmentDirectional.centerEnd,
child: AnalyticsDeviceDropdown(
onChanged: (value) {
context.read<AnalyticsDevicesBloc>().add(
SelectAnalyticsDeviceEvent(value),
);
FetchEnergyManagementDataHelper.loadRealtimeDeviceChanges(
context,
deviceUuid: value.uuid,
);
},
),
),
),
],
);
}
}
@@ -0,0 +1,75 @@
import 'package:flutter/material.dart';
import 'package:syncrow_web/utils/color_manager.dart';
import 'package:syncrow_web/utils/extension/build_context_x.dart';
enum AqiType {
aqi('AQI'),
pm25('PM2.5'),
pm10('PM10'),
hcho('HCHO'),
tvoc('TVOC'),
co2('CO2'),
c6h6('C6H6');
final String value;
const AqiType(this.value);
}
class AqiTypeDropdown extends StatefulWidget {
const AqiTypeDropdown({super.key, required this.onChanged});
final ValueChanged<AqiType?> onChanged;
@override
State<AqiTypeDropdown> createState() => _AqiTypeDropdownState();
}
class _AqiTypeDropdownState extends State<AqiTypeDropdown> {
AqiType? _selectedItem = AqiType.aqi;
void _updateSelectedItem(AqiType? item) => setState(() => _selectedItem = item);
@override
Widget build(BuildContext context) {
return Container(
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(16),
border: Border.all(
color: ColorsManager.greyColor,
width: 1,
),
),
child: DropdownButton<AqiType?>(
value: _selectedItem,
isDense: true,
borderRadius: BorderRadius.circular(16),
dropdownColor: ColorsManager.whiteColors,
underline: const SizedBox.shrink(),
icon: const RotatedBox(
quarterTurns: 1,
child: Icon(Icons.chevron_right, size: 24),
),
style: _getTextStyle(context),
padding: const EdgeInsetsDirectional.symmetric(
horizontal: 12,
vertical: 2,
),
items: AqiType.values
.map((e) => DropdownMenuItem(value: e, child: Text(e.value)))
.toList(),
onChanged: (value) {
_updateSelectedItem(value);
widget.onChanged(value);
},
),
);
}
TextStyle? _getTextStyle(BuildContext context) {
return context.textTheme.labelSmall?.copyWith(
color: ColorsManager.textPrimaryColor,
fontWeight: FontWeight.w700,
fontSize: 12,
);
}
}
@@ -0,0 +1,97 @@
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/energy_management/helpers/energy_management_charts_helper.dart';
import 'package:syncrow_web/utils/color_manager.dart';
class RangeOfAqiChart extends StatelessWidget {
final List<RangeOfAqi> chartData;
const RangeOfAqiChart({
super.key,
required this.chartData,
});
List<(List<double> values, Color color, Color? dotColor)> get _lines => [
(
chartData.map((e) => e.max).toList(),
ColorsManager.maxPurple,
ColorsManager.maxPurpleDot,
),
(
chartData.map((e) => e.avg).toList(),
Colors.white,
null,
),
(
chartData.map((e) => e.min).toList(),
ColorsManager.minBlue,
ColorsManager.minBlueDot,
),
];
@override
Widget build(BuildContext context) {
return LineChart(
LineChartData(
minY: 0,
maxY: 301,
clipData: const FlClipData.vertical(),
gridData: EnergyManagementChartsHelper.gridData(horizontalInterval: 50),
titlesData: RangeOfAqiChartsHelper.titlesData(context, chartData),
borderData: EnergyManagementChartsHelper.borderData(),
lineTouchData: RangeOfAqiChartsHelper.lineTouchData(chartData),
betweenBarsData: [
BetweenBarsData(
fromIndex: 0,
toIndex: 2,
gradient: LinearGradient(
begin: Alignment.bottomCenter,
end: Alignment.topCenter,
stops: [0.0, 0.2, 0.4, 0.6, 0.8, 1.0],
colors: RangeOfAqiChartsHelper.gradientData.map((e) {
final (color, _) = e;
return color.withValues(alpha: 0.6);
}).toList(),
),
),
],
lineBarsData: _lines.map((e) {
final (values, color, dotColor) = e;
return _buildLine(values: values, color: color, dotColor: dotColor);
}).toList(),
),
duration: Duration.zero,
);
}
FlDotData _buildDotData(Color color) {
return FlDotData(
show: true,
getDotPainter: (_, __, ___, ____) => FlDotCirclePainter(
radius: 2,
color: ColorsManager.whiteColors,
strokeWidth: 2,
strokeColor: color,
),
);
}
LineChartBarData _buildLine({
required List<double> values,
required Color color,
Color? dotColor,
}) {
const invisibleDot = FlDotData(show: false);
return LineChartBarData(
spots: List.generate(values.length, (i) => FlSpot(i.toDouble(), values[i])),
isCurved: true,
color: color,
barWidth: 4,
isStrokeCapRound: true,
dotData: dotColor != null ? _buildDotData(dotColor) : invisibleDot,
belowBarData: BarAreaData(show: false),
);
}
}
@@ -0,0 +1,42 @@
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:syncrow_web/pages/analytics/modules/air_quality/blocs/range_of_aqi/range_of_aqi_bloc.dart';
import 'package:syncrow_web/pages/analytics/modules/air_quality/widgets/range_of_aqi_chart.dart';
import 'package:syncrow_web/pages/analytics/modules/air_quality/widgets/range_of_aqi_chart_title.dart';
import 'package:syncrow_web/pages/analytics/widgets/analytics_error_widget.dart';
import 'package:syncrow_web/utils/style.dart';
class RangeOfAqiChartBox extends StatelessWidget {
const RangeOfAqiChartBox({super.key});
@override
Widget build(BuildContext context) {
return BlocBuilder<RangeOfAqiBloc, RangeOfAqiState>(
builder: (context, state) {
return Container(
padding: const EdgeInsetsDirectional.all(30),
decoration: subSectionContainerDecoration.copyWith(
borderRadius: BorderRadius.circular(30),
),
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
if (state.errorMessage != null) ...[
AnalyticsErrorWidget(state.errorMessage),
const SizedBox(height: 10),
],
RangeOfAqiChartTitle(
isLoading: state.status == RangeOfAqiStatus.loading,
),
const SizedBox(height: 10),
const Divider(),
const SizedBox(height: 20),
Expanded(child: RangeOfAqiChart(chartData: state.rangeOfAqi)),
],
),
);
},
);
}
}
@@ -0,0 +1,82 @@
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:syncrow_web/pages/analytics/modules/air_quality/helpers/fetch_air_quality_data_helper.dart';
import 'package:syncrow_web/pages/analytics/modules/air_quality/widgets/aqi_type_dropdown.dart';
import 'package:syncrow_web/pages/analytics/modules/analytics/blocs/analytics_date_picker_bloc/analytics_date_picker_bloc.dart';
import 'package:syncrow_web/pages/analytics/modules/analytics/widgets/chart_informative_cell.dart';
import 'package:syncrow_web/pages/analytics/modules/energy_management/widgets/chart_title.dart';
import 'package:syncrow_web/pages/analytics/widgets/charts_loading_widget.dart';
import 'package:syncrow_web/pages/space_tree/bloc/space_tree_bloc.dart';
class RangeOfAqiChartTitle extends StatelessWidget {
const RangeOfAqiChartTitle({required this.isLoading, super.key});
final bool isLoading;
static const List<(Color color, String title, bool hasBorder)> _colors = [
(Color(0xFF962DFF), 'Max', false),
(Color(0xFF93AAFD), 'Min', false),
(Colors.transparent, 'Avg', true),
];
@override
Widget build(BuildContext context) {
return Row(
children: [
ChartsLoadingWidget(isLoading: isLoading),
const Expanded(
flex: 3,
child: FittedBox(
fit: BoxFit.scaleDown,
alignment: AlignmentDirectional.centerStart,
child: ChartTitle(title: Text('Range of AQI')),
),
),
const Spacer(flex: 3),
..._colors.map(
(e) {
final (color, title, hasBorder) = e;
return Expanded(
child: IntrinsicHeight(
child: FittedBox(
fit: BoxFit.fitWidth,
alignment: AlignmentDirectional.centerStart,
child: Padding(
padding: const EdgeInsetsDirectional.only(end: 16),
child: ChartInformativeCell(
title: Text(title),
color: color,
hasBorder: hasBorder,
),
),
),
),
);
},
),
const Spacer(),
Expanded(
flex: 2,
child: FittedBox(
fit: BoxFit.scaleDown,
alignment: AlignmentDirectional.centerEnd,
child: AqiTypeDropdown(
onChanged: (value) {
final spaceTreeState = context.read<SpaceTreeBloc>().state;
final spaceUuid = spaceTreeState.selectedSpaces.firstOrNull;
if (spaceUuid == null) return;
FetchAirQualityDataHelper.loadRangeOfAqi(
context,
spaceUuid: spaceUuid,
date: context.read<AnalyticsDatePickerBloc>().state.monthlyDate,
aqiType: value ?? AqiType.aqi,
);
},
),
),
),
],
);
}
}
@@ -1,4 +1,5 @@
import 'package:flutter/material.dart';
import 'package:syncrow_web/pages/analytics/modules/air_quality/views/air_quality_view.dart';
import 'package:syncrow_web/pages/analytics/modules/energy_management/views/analytics_energy_management_view.dart';
import 'package:syncrow_web/pages/analytics/modules/occupancy/views/analytics_occupancy_view.dart';
@@ -10,6 +11,10 @@ enum AnalyticsPageTab {
occupancy(
title: 'Occupancy',
child: AnalyticsOccupancyView(),
),
airQuality(
title: 'Air Quality',
child: AirQualityView(),
);
const AnalyticsPageTab({
@@ -0,0 +1,59 @@
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:syncrow_web/pages/analytics/modules/air_quality/helpers/fetch_air_quality_data_helper.dart';
import 'package:syncrow_web/pages/analytics/modules/analytics/strategies/analytics_data_loading_strategy.dart';
import 'package:syncrow_web/pages/space_tree/bloc/space_tree_bloc.dart';
import 'package:syncrow_web/pages/space_tree/bloc/space_tree_event.dart';
import 'package:syncrow_web/pages/spaces_management/all_spaces/model/community_model.dart';
import 'package:syncrow_web/pages/spaces_management/all_spaces/model/space_model.dart';
final class AirQualityDataLoadingStrategy implements AnalyticsDataLoadingStrategy {
@override
void onCommunitySelected(
BuildContext context,
CommunityModel community,
List<SpaceModel> spaces,
) {
// Do nothing
}
@override
void onSpaceSelected(
BuildContext context,
CommunityModel community,
SpaceModel space,
) {
final spaceTreeBloc = context.read<SpaceTreeBloc>();
final isSpaceSelected = spaceTreeBloc.state.selectedSpaces.contains(space.uuid);
if (isSpaceSelected) {
clearData(context);
return;
}
spaceTreeBloc
..add(const SpaceTreeClearSelectionEvent())
..add(OnSpaceSelected(community, space.uuid ?? '', []));
FetchAirQualityDataHelper.loadAirQualityData(
context,
communityUuid: community.uuid,
spaceUuid: space.uuid ?? '',
);
}
@override
void onChildSpaceSelected(
BuildContext context,
CommunityModel community,
SpaceModel child,
) {
return onSpaceSelected(context, community, child);
}
@override
void clearData(BuildContext context) {
context.read<SpaceTreeBloc>().add(const SpaceTreeClearSelectionEvent());
FetchAirQualityDataHelper.clearAllData(context);
}
}
@@ -1,4 +1,5 @@
import 'package:syncrow_web/pages/analytics/modules/analytics/enums/analytics_page_tab.dart';
import 'package:syncrow_web/pages/analytics/modules/analytics/strategies/air_quality_data_loading_strategy.dart';
import 'package:syncrow_web/pages/analytics/modules/analytics/strategies/analytics_data_loading_strategy.dart';
import 'package:syncrow_web/pages/analytics/modules/analytics/strategies/energy_management_data_loading_strategy.dart';
import 'package:syncrow_web/pages/analytics/modules/analytics/strategies/occupancy_data_loading_strategy.dart';
@@ -9,6 +10,7 @@ abstract final class AnalyticsDataLoadingStrategyFactory {
return switch (tab) {
AnalyticsPageTab.energyManagement => EnergyManagementDataLoadingStrategy(),
AnalyticsPageTab.occupancy => OccupancyDataLoadingStrategy(),
AnalyticsPageTab.airQuality => AirQualityDataLoadingStrategy(),
};
}
}
@@ -68,9 +68,7 @@ class EnergyManagementDataLoadingStrategy implements AnalyticsDataLoadingStrateg
CommunityModel community,
SpaceModel child,
) {
if (child.children.isNotEmpty) {
return onSpaceSelected(context, community, child);
}
return onSpaceSelected(context, community, child);
}
@override
@@ -48,7 +48,7 @@ class OccupancyDataLoadingStrategy implements AnalyticsDataLoadingStrategy {
CommunityModel community,
SpaceModel child,
) {
onSpaceSelected(context, community, child);
return onSpaceSelected(context, community, child);
}
@override
@@ -1,5 +1,6 @@
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:syncrow_web/pages/analytics/modules/air_quality/blocs/range_of_aqi/range_of_aqi_bloc.dart';
import 'package:syncrow_web/pages/analytics/modules/analytics/blocs/analytics_date_picker_bloc/analytics_date_picker_bloc.dart';
import 'package:syncrow_web/pages/analytics/modules/analytics/blocs/analytics_devices/analytics_devices_bloc.dart';
import 'package:syncrow_web/pages/analytics/modules/analytics/blocs/analytics_tab/analytics_tab_bloc.dart';
@@ -20,6 +21,7 @@ import 'package:syncrow_web/pages/analytics/services/energy_consumption_per_devi
import 'package:syncrow_web/pages/analytics/services/occupacy/remote_occupancy_service.dart';
import 'package:syncrow_web/pages/analytics/services/occupancy_heat_map/remote_occupancy_heat_map_service.dart';
import 'package:syncrow_web/pages/analytics/services/power_clamp_info/remote_power_clamp_info_service.dart';
import 'package:syncrow_web/pages/analytics/services/range_of_aqi/fake_range_of_aqi_service.dart';
import 'package:syncrow_web/pages/analytics/services/realtime_device_service/firebase_realtime_device_service.dart';
import 'package:syncrow_web/pages/analytics/services/total_energy_consumption/remote_total_energy_consumption_service.dart';
import 'package:syncrow_web/pages/device_managment/shared/navigate_home_grid_view.dart';
@@ -94,6 +96,11 @@ class _AnalyticsPageState extends State<AnalyticsPage> {
),
),
),
BlocProvider(
create: (context) => RangeOfAqiBloc(
FakeRangeOfAqiService(),
),
),
],
child: const AnalyticsPageForm(),
);
@@ -0,0 +1,59 @@
import 'package:flutter/material.dart';
import 'package:syncrow_web/utils/color_manager.dart';
class ChartInformativeCell extends StatelessWidget {
const ChartInformativeCell({
super.key,
required this.title,
required this.color,
this.hasBorder = false,
});
final Widget title;
final Color color;
final bool hasBorder;
@override
Widget build(BuildContext context) {
return Container(
height: MediaQuery.sizeOf(context).height * 0.0385,
padding: const EdgeInsetsDirectional.symmetric(
vertical: 8,
horizontal: 12,
),
decoration: BoxDecoration(
borderRadius: BorderRadiusDirectional.circular(8),
border: Border.all(
color: ColorsManager.greyColor,
width: 1,
),
),
child: FittedBox(
fit: BoxFit.scaleDown,
alignment: Alignment.center,
child: Row(
spacing: 6,
children: [
Container(
height: 8,
width: 8,
decoration: BoxDecoration(
color: color,
border: Border.all(color: ColorsManager.grayBorder),
shape: BoxShape.circle,
),
),
DefaultTextStyle(
style: const TextStyle(
color: ColorsManager.blackColor,
fontWeight: FontWeight.w400,
fontSize: 14,
),
child: title,
),
],
),
),
);
}
}
@@ -38,7 +38,7 @@ abstract final class EnergyManagementChartsHelper {
sideTitles: SideTitles(
showTitles: true,
maxIncluded: false,
minIncluded: true,
minIncluded: false,
interval: leftTitlesInterval,
reservedSize: 110,
getTitlesWidget: (value, meta) => Padding(
@@ -50,7 +50,7 @@ abstract final class EnergyManagementChartsHelper {
value.formatNumberToKwh,
style: context.textTheme.bodySmall?.copyWith(
fontSize: 12,
color: ColorsManager.greyColor,
color: ColorsManager.lightGreyColor,
),
),
),
@@ -93,12 +93,14 @@ abstract final class EnergyManagementChartsHelper {
);
}
static FlGridData gridData() {
static FlGridData gridData({
double horizontalInterval = 250,
}) {
return FlGridData(
show: true,
drawVerticalLine: false,
drawHorizontalLine: true,
horizontalInterval: 250,
horizontalInterval: horizontalInterval,
getDrawingHorizontalLine: (value) {
return FlLine(
color: ColorsManager.greyColor,
@@ -18,6 +18,7 @@ class EnergyConsumptionByPhasesChart extends StatelessWidget {
Widget build(BuildContext context) {
return BarChart(
BarChartData(
gridData: EnergyManagementChartsHelper.gridData().copyWith(
checkToShowHorizontalLine: (value) => true,
horizontalInterval: 250,
@@ -170,7 +171,7 @@ class EnergyConsumptionByPhasesChart extends StatelessWidget {
child: Text(
month,
style: context.textTheme.bodySmall?.copyWith(
color: ColorsManager.greyColor,
color: ColorsManager.lightGreyColor,
fontSize: 11,
),
),
@@ -19,10 +19,12 @@ class EnergyConsumptionByPhasesChartBox extends StatelessWidget {
decoration: secondarySection,
child: Column(
mainAxisSize: MainAxisSize.min,
spacing: 20,
children: [
AnalyticsErrorWidget(state.errorMessage),
EnergyConsumptionByPhasesTitle(isLoading: state.status == EnergyConsumptionByPhasesStatus.loading,),
EnergyConsumptionByPhasesTitle(
isLoading: state.status == EnergyConsumptionByPhasesStatus.loading,
),
const SizedBox(height: 20),
Expanded(
child: EnergyConsumptionByPhasesChart(
energyData: state.chartData,
@@ -12,6 +12,7 @@ class EnergyConsumptionPerDeviceChart extends StatelessWidget {
Widget build(BuildContext context) {
return LineChart(
LineChartData(
clipData: const FlClipData.vertical(),
titlesData: EnergyManagementChartsHelper.titlesData(
context,
leftTitlesInterval: 250,
@@ -1,7 +1,7 @@
import 'package:flutter/material.dart';
import 'package:syncrow_web/pages/analytics/models/analytics_device.dart';
import 'package:syncrow_web/pages/analytics/models/device_energy_data_model.dart';
import 'package:syncrow_web/utils/color_manager.dart';
import 'package:syncrow_web/pages/analytics/modules/analytics/widgets/chart_informative_cell.dart';
class EnergyConsumptionPerDeviceDevicesList extends StatelessWidget {
const EnergyConsumptionPerDeviceDevicesList({
@@ -42,42 +42,7 @@ class EnergyConsumptionPerDeviceDevicesList extends StatelessWidget {
return Tooltip(
message: '${device.name}\n${device.productDevice?.uuid ?? ''}',
child: Container(
height: MediaQuery.sizeOf(context).height * 0.0365,
padding: const EdgeInsetsDirectional.symmetric(
vertical: 8,
horizontal: 12,
),
decoration: BoxDecoration(
borderRadius: BorderRadiusDirectional.circular(8),
border: Border.all(
color: ColorsManager.greyColor,
width: 1,
),
),
child: FittedBox(
fit: BoxFit.scaleDown,
alignment: Alignment.center,
child: Row(
spacing: 6,
children: [
CircleAvatar(
radius: 4,
backgroundColor: deviceColor,
),
Text(
device.name,
textAlign: TextAlign.center,
style: const TextStyle(
color: ColorsManager.blackColor,
fontWeight: FontWeight.w400,
fontSize: 14,
),
),
],
),
),
),
child: ChartInformativeCell(title: Text(device.name), color: deviceColor),
);
}
}
@@ -69,13 +69,18 @@ class PowerClampEnergyDataWidget extends StatelessWidget {
PowerClampEnergyStatus(
iconPath: Assets.powerActiveIcon,
title: 'Active',
value: _valueFromCode('EnergyConsumed', generalDataPoints),
value: _valueFromCode('ActivePower', generalDataPoints),
unit: 'W',
),
PowerClampEnergyStatus(
iconPath: Assets.voltMeterIcon,
title: 'Current',
value: _valueFromCode('Current', generalDataPoints),
value: _valueFromCode('Current', generalDataPoints)
.replaceAllMapped(
RegExp(r'(\d{1,3})(?=(\d{3})+(?!\d))'),
(Match m) => '${m[1]}.',
)
.replaceAll('.0', ''),
unit: 'A',
),
PowerClampEnergyStatus(
@@ -55,7 +55,7 @@ class PowerClampPhasesDataWidget extends StatelessWidget {
iconPath: Assets.powerActiveIcon,
title: 'Active Power',
value: _valueFromCode(
code: 'ReactivePower$phaseSuffix',
code: 'ActivePower$phaseSuffix',
points: phase?.dataPoints,
),
unit: 'W',
@@ -125,7 +125,48 @@ class PowerClampPhasesDataWidget extends StatelessWidget {
(e) => e.code == code,
orElse: () => DataPoint(value: '--'),
);
final value = element?.value;
if (code.contains('Current')) {
return _formatCurrentValue(value?.toString());
}
if (code.contains('PowerFactor')) {
return _formatPowerFactor(value?.toString());
}
if (code.contains('Voltage')) {
return _formatVoltage(value?.toString());
}
return value?.toString() ?? '--';
}
return element?.value.toString() ?? '--';
String _formatCurrentValue(String? value) {
if (value == null) return '--';
String str = value;
if (str.isEmpty || str == '--') return '--';
str = str.replaceAll(RegExp(r'[^0-9]'), '');
if (str.isEmpty) return '--';
if (str.length == 1) return '${str[0]}.0';
return '${str[0]}.${str.substring(1)}';
}
String _formatPowerFactor(String? value) {
if (value == null) return '--';
String str = value;
if (str.isEmpty || str == '--') return '--';
str = str.replaceAll(RegExp(r'[^0-9]'), '');
if (str.isEmpty) return '--';
final intValue = int.tryParse(str);
if (intValue == null) return '--';
final doubleValue = intValue / 100;
return doubleValue.toStringAsFixed(2);
}
String _formatVoltage(String? value) {
if (value == null) return '--';
String str = value;
if (str.isEmpty || str == '--') return '--';
str = str.replaceAll(RegExp(r'[^0-9]'), '');
if (str.isEmpty) return '--';
if (str.length == 1) return '0.${str[0]}';
return '${str.substring(0, str.length - 1)}.${str.substring(str.length - 1)}';
}
}
@@ -14,6 +14,7 @@ class TotalEnergyConsumptionChart extends StatelessWidget {
return Expanded(
child: LineChart(
LineChartData(
clipData: const FlClipData.vertical(),
titlesData: EnergyManagementChartsHelper.titlesData(
context,
leftTitlesInterval: 250,
@@ -28,6 +29,7 @@ class TotalEnergyConsumptionChart extends StatelessWidget {
),
duration: Duration.zero,
curve: Curves.easeIn,
),
);
}
@@ -35,9 +37,6 @@ class TotalEnergyConsumptionChart extends StatelessWidget {
List<LineChartBarData> get _lineBarsData {
return [
LineChartBarData(
preventCurveOvershootingThreshold: 0.1,
curveSmoothness: 0.55,
preventCurveOverShooting: true,
spots: chartData
.asMap()
.entries
@@ -0,0 +1,18 @@
import 'package:equatable/equatable.dart';
import 'package:syncrow_web/pages/analytics/modules/air_quality/widgets/aqi_type_dropdown.dart';
class GetRangeOfAqiParam extends Equatable {
final DateTime date;
final String spaceUuid;
final AqiType aqiType;
const GetRangeOfAqiParam(
{
required this.date,
required this.spaceUuid,
required this.aqiType,
});
@override
List<Object?> get props => [date, spaceUuid];
}
@@ -0,0 +1,30 @@
import 'package:syncrow_web/pages/analytics/models/range_of_aqi.dart';
import 'package:syncrow_web/pages/analytics/params/get_range_of_aqi_param.dart';
import 'package:syncrow_web/pages/analytics/services/range_of_aqi/range_of_aqi_service.dart';
class FakeRangeOfAqiService implements RangeOfAqiService {
@override
Future<List<RangeOfAqi>> load(GetRangeOfAqiParam param) async {
return await Future.delayed(const Duration(milliseconds: 800), () {
final random = DateTime.now().millisecondsSinceEpoch;
return List.generate(30, (index) {
final date = DateTime(2025, 5, 1).add(Duration(days: index));
final min = ((random + index * 17) % 200).toDouble();
final avgDelta = ((random + index * 23) % 50).toDouble() + 20;
final maxDelta = ((random + index * 31) % 50).toDouble() + 30;
final avg = (min + avgDelta).clamp(0.0, 301.0);
final max = (avg + maxDelta).clamp(0.0, 301.0);
return RangeOfAqi(
min: min,
avg: avg,
max: max,
date: date,
);
});
});
}
}
@@ -0,0 +1,6 @@
import 'package:syncrow_web/pages/analytics/models/range_of_aqi.dart';
import 'package:syncrow_web/pages/analytics/params/get_range_of_aqi_param.dart';
abstract interface class RangeOfAqiService {
Future<List<RangeOfAqi>> load(GetRangeOfAqiParam param);
}
+135 -83
View File
@@ -21,6 +21,7 @@ class DynamicTable extends StatefulWidget {
final List<String>? initialSelectedIds;
final int uuidIndex;
final Function(dynamic selectedRows)? onSelectionChanged;
final Function(int rowIndex)? onSettingsPressed;
const DynamicTable({
super.key,
required this.headers,
@@ -37,6 +38,7 @@ class DynamicTable extends StatefulWidget {
this.initialSelectedIds,
required this.uuidIndex,
this.onSelectionChanged,
this.onSettingsPressed,
});
@override
@@ -48,11 +50,20 @@ class _DynamicTableState extends State<DynamicTable> {
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
@@ -102,101 +113,87 @@ class _DynamicTableState extends State<DynamicTable> {
context.read<DeviceManagementBloc>().add(UpdateSelection(_selectedRows));
}
@override
void dispose() {
_horizontalHeaderScrollController.dispose();
_horizontalBodyScrollController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Container(
decoration: widget.cellDecoration,
child: Scrollbar(
controller: _verticalScrollController,
thumbVisibility: true,
trackVisibility: true,
child: Scrollbar(
controller: _horizontalScrollController,
thumbVisibility: true,
trackVisibility: true,
notificationPredicate: (notif) => notif.depth == 1,
child: SingleChildScrollView(
controller: _verticalScrollController,
child: Column(
children: [
Container(
decoration: widget.headerDecoration ??
const BoxDecoration(color: ColorsManager.boxColor),
child: SingleChildScrollView(
controller: _horizontalScrollController,
scrollDirection: Axis.horizontal,
physics: const NeverScrollableScrollPhysics(),
controller: _horizontalHeaderScrollController,
child: SizedBox(
width: widget.size.width,
child: Column(
child: Row(
children: [
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)),
],
),
),
widget.isEmpty
? SizedBox(
height: widget.size.height * 0.5,
width: widget.size.width,
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),
)
],
),
],
),
],
),
)
: Column(
children:
List.generate(widget.data.length, (index) {
final row = widget.data[index];
return Row(
children: [
if (widget.withCheckBox)
_buildRowCheckbox(
index, widget.size.height * 0.08),
...row.map((cell) => _buildTableCell(
cell.toString(),
widget.size.height * 0.08)),
],
);
}),
),
if (widget.withCheckBox) _buildSelectAllCheckbox(),
...List.generate(widget.headers.length, (index) {
return _buildTableHeaderCell(
widget.headers[index], index);
}),
],
),
),
),
),
),
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: 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(),
],
);
}),
),
),
),
),
),
),
),
],
),
);
}
@@ -218,6 +215,32 @@ class _DynamicTableState extends State<DynamicTable> {
);
}
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,
@@ -272,13 +295,23 @@ class _DynamicTableState extends State<DynamicTable> {
);
}
Widget _buildTableCell(String content, double size) {
Widget _buildTableCell(
String content,
double size, {
required int rowIndex,
required int columnIndex,
}) {
bool isBatteryLevel = content.endsWith('%');
double? batteryLevel;
if (isBatteryLevel) {
batteryLevel = double.tryParse(content.replaceAll('%', '').trim());
}
bool isSettingsColumn = widget.headers[columnIndex] == 'Settings';
if (isSettingsColumn) {
return _buildSettingsIcon(rowIndex, size);
}
Color? statusColor;
switch (content) {
@@ -330,4 +363,23 @@ class _DynamicTableState extends State<DynamicTable> {
),
);
}
Widget _buildSettingsIcon(int rowIndex, double size) {
return Container(
height: size,
width: 120,
padding: const EdgeInsets.all(5.0),
decoration: const BoxDecoration(
border: Border(
bottom: BorderSide(color: ColorsManager.boxDivider, width: 1.0),
),
color: Colors.white,
),
alignment: Alignment.center,
child: IconButton(
icon: SvgPicture.asset(Assets.settings),
onPressed: () => widget.onSettingsPressed?.call(rowIndex),
),
);
}
}
@@ -12,6 +12,7 @@ import 'package:syncrow_web/pages/routines/models/gang_switches/one_gang_switch/
import 'package:syncrow_web/pages/routines/models/gang_switches/three_gang_switch/three_gang_switch.dart';
import 'package:syncrow_web/pages/routines/models/gang_switches/two_gang_switch/two_gang_switch.dart';
import 'package:syncrow_web/pages/routines/models/gateway.dart';
import 'package:syncrow_web/pages/routines/models/pc/energy_clamp_functions.dart';
import 'package:syncrow_web/pages/routines/models/water_heater/water_heater_functions.dart';
import 'package:syncrow_web/pages/routines/models/wps/wps_functions.dart';
import 'package:syncrow_web/pages/routines/widgets/routine_dialogs/ceiling_sensor/ceiling_sensor_helper.dart';
@@ -248,6 +249,8 @@ SOS
tempIcon = Assets.waterLeakNormal;
} else if (type == DeviceType.NCPS) {
tempIcon = Assets.sensors;
} else if (type == DeviceType.PC) {
tempIcon = Assets.powerClamp;
} else {
tempIcon = Assets.logoHorizontal;
}
@@ -393,6 +396,59 @@ SOS
BacklightFunction(
deviceId: uuid ?? '', deviceName: name ?? '', type: 'IF'),
];
case 'PC':
return [
TotalEnergyConsumedStatusFunction(
deviceId: uuid ?? '', deviceName: name ?? '', type: 'IF'),
TotalActivePowerConsumedStatusFunction(
deviceId: uuid ?? '', deviceName: name ?? '', type: 'IF'),
VoltagePhaseSequenceDetectionFunction(
deviceId: uuid ?? '', deviceName: name ?? '', type: 'IF'),
TotalCurrentStatusFunction(
deviceId: uuid ?? '', deviceName: name ?? '', type: 'IF'),
FrequencyStatusFunction(
deviceId: uuid ?? '', deviceName: name ?? '', type: 'IF'),
// Phase A
EnergyConsumedAStatusFunction(
deviceId: uuid ?? '', deviceName: name ?? '', type: 'IF'),
ActivePowerAStatusFunction(
deviceId: uuid ?? '', deviceName: name ?? '', type: 'IF'),
VoltageAStatusFunction(
deviceId: uuid ?? '', deviceName: name ?? '', type: 'IF'),
PowerFactorAStatusFunction(
deviceId: uuid ?? '', deviceName: name ?? '', type: 'IF'),
CurrentAStatusFunction(
deviceId: uuid ?? '', deviceName: name ?? '', type: 'IF'),
// Phase B
EnergyConsumedBStatusFunction(
deviceId: uuid ?? '', deviceName: name ?? '', type: 'IF'),
ActivePowerBStatusFunction(
deviceId: uuid ?? '', deviceName: name ?? '', type: 'IF'),
VoltageBStatusFunction(
deviceId: uuid ?? '', deviceName: name ?? '', type: 'IF'),
CurrentBStatusFunction(
deviceId: uuid ?? '', deviceName: name ?? '', type: 'IF'),
PowerFactorBStatusFunction(
deviceId: uuid ?? '', deviceName: name ?? '', type: 'IF'),
// Phase C
EnergyConsumedCStatusFunction(
deviceId: uuid ?? '', deviceName: name ?? '', type: 'IF'),
ActivePowerCStatusFunction(
deviceId: uuid ?? '', deviceName: name ?? '', type: 'IF'),
VoltageCStatusFunction(
deviceId: uuid ?? '', deviceName: name ?? '', type: 'IF'),
CurrentCStatusFunction(
deviceId: uuid ?? '',
deviceName: name ?? '',
type: 'IF'),
PowerFactorCStatusFunction(
deviceId: uuid ?? '',
deviceName: name ?? '',
type: 'IF'),
];
default:
return [];
@@ -526,5 +582,6 @@ SOS
"GD": DeviceType.GarageDoor,
"WL": DeviceType.WaterLeak,
"NCPS": DeviceType.NCPS,
"PC": DeviceType.PC,
};
}
@@ -5,7 +5,7 @@ import 'package:syncrow_web/utils/constants/assets.dart';
Future<void> showNameMenu({
required BuildContext context,
Function()? aToZTap,
Function()? zToaTap,
Function()? zToATap,
String? isSelected,
}) async {
final RenderBox overlay =
@@ -46,7 +46,7 @@ Future<void> showNameMenu({
),
),
PopupMenuItem(
onTap: zToaTap,
onTap: zToATap,
child: ListTile(
leading: Image.asset(
Assets.ZtoAIcon,
@@ -95,7 +95,7 @@ class _TableRow extends StatelessWidget {
],
),
if (!isLast)
Divider(
const Divider(
height: 1,
thickness: 1,
color: ColorsManager.boxDivider,
@@ -110,12 +110,14 @@ class DynamicTableScreen extends StatefulWidget {
final List<String> titles;
final List<List<Widget>> rows;
final void Function(int columnIndex)? onFilter;
final double tableSize;
const DynamicTableScreen({
required this.titles,
required this.rows,
required this.onFilter,
Key? key,
required this.tableSize,
}) : super(key: key);
@override
@@ -205,7 +207,8 @@ class _DynamicTableScreenState extends State<DynamicTableScreen> {
bottomRight: Radius.circular(15),
),
),
child: Column(
child: ListView(
shrinkWrap: true,
children: [
for (int rowIndex = 0; rowIndex < widget.rows.length; rowIndex++)
_TableRow(
@@ -253,7 +256,7 @@ class _DynamicTableScreenState extends State<DynamicTableScreen> {
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_buildHeader(),
_buildBody(),
Container(height: widget.tableSize - 37, child: _buildBody()),
],
),
),
@@ -27,7 +27,8 @@ class UsersPage extends StatelessWidget {
Widget build(BuildContext context) {
final TextEditingController searchController = TextEditingController();
Widget actionButton({bool isActive = false, required String title, Function()? onTap}) {
Widget actionButton(
{bool isActive = false, required String title, Function()? onTap}) {
return InkWell(
onTap: onTap,
child: Padding(
@@ -60,7 +61,8 @@ class UsersPage extends StatelessWidget {
: ColorsManager.disabledPink.withOpacity(0.5),
),
child: Padding(
padding: const EdgeInsets.only(left: 10, right: 10, bottom: 5, top: 5),
padding:
const EdgeInsets.only(left: 10, right: 10, bottom: 5, top: 5),
child: Row(
crossAxisAlignment: CrossAxisAlignment.center,
mainAxisAlignment: MainAxisAlignment.center,
@@ -84,12 +86,15 @@ class UsersPage extends StatelessWidget {
}
Widget changeIconStatus(
{required String userId, required String status, required Function()? onTap}) {
{required String userId,
required String status,
required Function()? onTap}) {
return Center(
child: InkWell(
onTap: onTap,
child: Padding(
padding: const EdgeInsets.only(left: 5, right: 5, bottom: 5, top: 5),
padding:
const EdgeInsets.only(left: 5, right: 5, bottom: 5, top: 5),
child: SvgPicture.asset(
status == "invited"
? Assets.invitedIcon
@@ -114,8 +119,7 @@ class UsersPage extends StatelessWidget {
padding: const EdgeInsets.all(20),
child: Align(
alignment: Alignment.topCenter,
child: ListView(
shrinkWrap: true,
child: Column(
children: [
Row(
children: [
@@ -188,292 +192,325 @@ class UsersPage extends StatelessWidget {
),
],
),
const SizedBox(height: 25),
DynamicTableScreen(
onFilter: (columnIndex) {
if (columnIndex == 0) {
showNameMenu(
context: context,
isSelected: _blocRole.currentSortOrder,
aToZTap: () {
context.read<UserTableBloc>().add(const SortUsersByNameAsc());
},
zToaTap: () {
context.read<UserTableBloc>().add(const SortUsersByNameDesc());
},
);
}
if (columnIndex == 2) {
final Map<String, bool> checkboxStates = {
for (var item in _blocRole.jobTitle)
item: _blocRole.selectedJobTitles.contains(item),
};
final RenderBox overlay =
Overlay.of(context).context.findRenderObject() as RenderBox;
const SizedBox(height: 20),
Container(
height: screenSize.height * 0.65,
child: DynamicTableScreen(
tableSize: screenSize.height * 0.65,
onFilter: (columnIndex) {
if (columnIndex == 0) {
showNameMenu(
context: context,
isSelected: _blocRole.currentSortOrder,
aToZTap: () {
context
.read<UserTableBloc>()
.add(const SortUsersByNameAsc());
},
zToATap: () {
context
.read<UserTableBloc>()
.add(const SortUsersByNameDesc());
},
);
}
if (columnIndex == 2) {
final Map<String, bool> checkboxStates = {
for (var item in _blocRole.jobTitle)
item: _blocRole.selectedJobTitles.contains(item),
};
final RenderBox overlay = Overlay.of(context)
.context
.findRenderObject() as RenderBox;
showPopUpFilterMenu(
position: RelativeRect.fromLTRB(
overlay.size.width / 5.3,
240,
overlay.size.width / 4,
0,
),
list: _blocRole.jobTitle,
context: context,
checkboxStates: checkboxStates,
isSelected: _blocRole.currentSortJopTitle,
onOkPressed: () {
searchController.clear();
_blocRole.add(FilterClearEvent());
final selectedItems = checkboxStates.entries
.where((entry) => entry.value)
.map((entry) => entry.key)
.toList();
Navigator.of(context).pop();
_blocRole.add(FilterUsersByJobEvent(
selectedJob: selectedItems,
sortOrder: _blocRole.currentSortJopTitle,
));
},
onSortAtoZ: (v) {
_blocRole.currentSortJopTitle = v;
},
onSortZtoA: (v) {
_blocRole.currentSortJopTitle = v;
},
);
}
showPopUpFilterMenu(
position: RelativeRect.fromLTRB(
overlay.size.width / 5.3,
240,
overlay.size.width / 4,
0,
),
list: _blocRole.jobTitle,
context: context,
checkboxStates: checkboxStates,
isSelected: _blocRole.currentSortJopTitle,
onOkPressed: () {
searchController.clear();
_blocRole.add(FilterClearEvent());
final selectedItems = checkboxStates.entries
.where((entry) => entry.value)
.map((entry) => entry.key)
.toList();
Navigator.of(context).pop();
_blocRole.add(FilterUsersByJobEvent(
selectedJob: selectedItems,
sortOrder: _blocRole.currentSortJopTitle,
));
},
onSortAtoZ: (v) {
_blocRole.currentSortJopTitle = v;
},
onSortZtoA: (v) {
_blocRole.currentSortJopTitle = v;
},
);
}
if (columnIndex == 3) {
final Map<String, bool> checkboxStates = {
for (var item in _blocRole.roleTypes)
item: _blocRole.selectedRoles.contains(item),
};
final RenderBox overlay =
Overlay.of(context).context.findRenderObject() as RenderBox;
showPopUpFilterMenu(
position: RelativeRect.fromLTRB(
overlay.size.width / 4,
240,
overlay.size.width / 4,
0,
),
list: _blocRole.roleTypes,
context: context,
checkboxStates: checkboxStates,
isSelected: _blocRole.currentSortRole,
onOkPressed: () {
searchController.clear();
_blocRole.add(FilterClearEvent());
final selectedItems = checkboxStates.entries
.where((entry) => entry.value)
.map((entry) => entry.key)
.toList();
Navigator.of(context).pop();
context.read<UserTableBloc>().add(FilterUsersByRoleEvent(
selectedRoles: selectedItems,
sortOrder: _blocRole.currentSortRole));
},
onSortAtoZ: (v) {
_blocRole.currentSortRole = v;
},
onSortZtoA: (v) {
_blocRole.currentSortRole = v;
},
);
}
if (columnIndex == 4) {
showDateFilterMenu(
context: context,
isSelected: _blocRole.currentSortOrder,
aToZTap: () {
context.read<UserTableBloc>().add(const DateNewestToOldestEvent());
},
zToaTap: () {
context.read<UserTableBloc>().add(const DateOldestToNewestEvent());
},
);
}
if (columnIndex == 6) {
final Map<String, bool> checkboxStates = {
for (var item in _blocRole.createdBy)
item: _blocRole.selectedCreatedBy.contains(item),
};
final RenderBox overlay =
Overlay.of(context).context.findRenderObject() as RenderBox;
showPopUpFilterMenu(
position: RelativeRect.fromLTRB(
overlay.size.width / 1,
240,
overlay.size.width / 4,
0,
),
list: _blocRole.createdBy,
context: context,
checkboxStates: checkboxStates,
isSelected: _blocRole.currentSortCreatedBy,
onOkPressed: () {
searchController.clear();
_blocRole.add(FilterClearEvent());
final selectedItems = checkboxStates.entries
.where((entry) => entry.value)
.map((entry) => entry.key)
.toList();
Navigator.of(context).pop();
_blocRole.add(FilterUsersByCreatedEvent(
selectedCreatedBy: selectedItems,
sortOrder: _blocRole.currentSortCreatedBy));
},
onSortAtoZ: (v) {
_blocRole.currentSortCreatedBy = v;
},
onSortZtoA: (v) {
_blocRole.currentSortCreatedBy = v;
},
);
}
if (columnIndex == 7) {
final Map<String, bool> checkboxStates = {
for (var item in _blocRole.status)
item: _blocRole.selectedStatuses.contains(item),
};
if (columnIndex == 3) {
final Map<String, bool> checkboxStates = {
for (var item in _blocRole.roleTypes)
item: _blocRole.selectedRoles.contains(item),
};
final RenderBox overlay = Overlay.of(context)
.context
.findRenderObject() as RenderBox;
showPopUpFilterMenu(
position: RelativeRect.fromLTRB(
overlay.size.width / 4,
240,
overlay.size.width / 4,
0,
),
list: _blocRole.roleTypes,
context: context,
checkboxStates: checkboxStates,
isSelected: _blocRole.currentSortRole,
onOkPressed: () {
searchController.clear();
_blocRole.add(FilterClearEvent());
final selectedItems = checkboxStates.entries
.where((entry) => entry.value)
.map((entry) => entry.key)
.toList();
Navigator.of(context).pop();
context.read<UserTableBloc>().add(
FilterUsersByRoleEvent(
selectedRoles: selectedItems,
sortOrder: _blocRole.currentSortRole));
},
onSortAtoZ: (v) {
_blocRole.currentSortRole = v;
},
onSortZtoA: (v) {
_blocRole.currentSortRole = v;
},
);
}
if (columnIndex == 4) {
showDateFilterMenu(
context: context,
isSelected: _blocRole.currentSortOrder,
aToZTap: () {
context
.read<UserTableBloc>()
.add(const DateNewestToOldestEvent());
},
zToaTap: () {
context
.read<UserTableBloc>()
.add(const DateOldestToNewestEvent());
},
);
}
if (columnIndex == 6) {
final Map<String, bool> checkboxStates = {
for (var item in _blocRole.createdBy)
item: _blocRole.selectedCreatedBy.contains(item),
};
final RenderBox overlay = Overlay.of(context)
.context
.findRenderObject() as RenderBox;
showPopUpFilterMenu(
position: RelativeRect.fromLTRB(
overlay.size.width / 1,
240,
overlay.size.width / 4,
0,
),
list: _blocRole.createdBy,
context: context,
checkboxStates: checkboxStates,
isSelected: _blocRole.currentSortCreatedBy,
onOkPressed: () {
searchController.clear();
_blocRole.add(FilterClearEvent());
final selectedItems = checkboxStates.entries
.where((entry) => entry.value)
.map((entry) => entry.key)
.toList();
Navigator.of(context).pop();
_blocRole.add(FilterUsersByCreatedEvent(
selectedCreatedBy: selectedItems,
sortOrder: _blocRole.currentSortCreatedBy));
},
onSortAtoZ: (v) {
_blocRole.currentSortCreatedBy = v;
},
onSortZtoA: (v) {
_blocRole.currentSortCreatedBy = v;
},
);
}
if (columnIndex == 7) {
final Map<String, bool> checkboxStates = {
for (var item in _blocRole.status)
item: _blocRole.selectedStatuses.contains(item),
};
final RenderBox overlay =
Overlay.of(context).context.findRenderObject() as RenderBox;
showPopUpFilterMenu(
position: RelativeRect.fromLTRB(
overlay.size.width / 0,
240,
overlay.size.width / 5,
0,
final RenderBox overlay = Overlay.of(context)
.context
.findRenderObject() as RenderBox;
showPopUpFilterMenu(
position: RelativeRect.fromLTRB(
overlay.size.width / 0,
240,
overlay.size.width / 5,
0,
),
list: _blocRole.status,
context: context,
checkboxStates: checkboxStates,
isSelected: _blocRole.currentSortStatus,
onOkPressed: () {
searchController.clear();
_blocRole.add(FilterClearEvent());
final selectedItems = checkboxStates.entries
.where((entry) => entry.value)
.map((entry) => entry.key)
.toList();
Navigator.of(context).pop();
_blocRole.add(FilterUsersByDeActevateEvent(
selectedActivate: selectedItems,
sortOrder: _blocRole.currentSortStatus));
},
onSortAtoZ: (v) {
_blocRole.currentSortStatus = v;
},
onSortZtoA: (v) {
_blocRole.currentSortStatus = v;
},
);
}
if (columnIndex == 8) {
showDeActivateFilterMenu(
context: context,
isSelected: _blocRole.currentSortOrderDate,
aToZTap: () {
context
.read<UserTableBloc>()
.add(const DateNewestToOldestEvent());
},
zToaTap: () {
context
.read<UserTableBloc>()
.add(const DateOldestToNewestEvent());
},
);
}
},
titles: const [
"Full Name",
"Email Address",
"Job Title",
"Role",
"Creation Date",
"Creation Time",
"Created By",
"Status",
"De/Activate",
"Action"
],
rows: state.users.map((user) {
return [
Text('${user.firstName} ${user.lastName}'),
Text(user.email),
Text(user.jobTitle),
Text(user.roleType ?? ''),
Text(user.createdDate ?? ''),
Text(user.createdTime ?? ''),
Text(user.invitedBy),
status(
status: user.isEnabled == false
? 'disabled'
: user.status,
),
list: _blocRole.status,
context: context,
checkboxStates: checkboxStates,
isSelected: _blocRole.currentSortStatus,
onOkPressed: () {
searchController.clear();
_blocRole.add(FilterClearEvent());
final selectedItems = checkboxStates.entries
.where((entry) => entry.value)
.map((entry) => entry.key)
.toList();
Navigator.of(context).pop();
_blocRole.add(FilterUsersByDeActevateEvent(
selectedActivate: selectedItems,
sortOrder: _blocRole.currentSortStatus));
},
onSortAtoZ: (v) {
_blocRole.currentSortStatus = v;
},
onSortZtoA: (v) {
_blocRole.currentSortStatus = v;
},
);
}
if (columnIndex == 8) {
showDeActivateFilterMenu(
context: context,
isSelected: _blocRole.currentSortOrderDate,
aToZTap: () {
context.read<UserTableBloc>().add(const DateNewestToOldestEvent());
},
zToaTap: () {
context.read<UserTableBloc>().add(const DateOldestToNewestEvent());
},
);
}
},
titles: const [
"Full Name",
"Email Address",
"Job Title",
"Role",
"Creation Date",
"Creation Time",
"Created By",
"Status",
"De/Activate",
"Action"
],
rows: state.users.map((user) {
return [
Text('${user.firstName} ${user.lastName}'),
Text(user.email),
Text(user.jobTitle),
Text(user.roleType ?? ''),
Text(user.createdDate ?? ''),
Text(user.createdTime ?? ''),
Text(user.invitedBy),
status(
status: user.isEnabled == false ? 'disabled' : user.status,
),
changeIconStatus(
status: user.isEnabled == false ? 'disabled' : user.status,
userId: user.uuid,
onTap: user.status != "invited"
? () {
context.read<UserTableBloc>().add(ChangeUserStatus(
userId: user.uuid,
newStatus:
user.isEnabled == false ? 'disabled' : user.status));
}
: null,
),
Row(
children: [
user.isEnabled != false
? actionButton(
isActive: true,
title: "Edit",
onTap: () {
context.read<SpaceTreeBloc>().add(ClearCachedData());
showDialog(
context: context,
barrierDismissible: false,
builder: (BuildContext context) {
return EditUserDialog(userId: user.uuid);
},
).then((v) {
if (v != null) {
changeIconStatus(
status: user.isEnabled == false
? 'disabled'
: user.status,
userId: user.uuid,
onTap: user.status != "invited"
? () {
context.read<UserTableBloc>().add(
ChangeUserStatus(
userId: user.uuid,
newStatus: user.isEnabled == false
? 'disabled'
: user.status));
}
: null,
),
Row(
children: [
user.isEnabled != false
? actionButton(
isActive: true,
title: "Edit",
onTap: () {
context
.read<SpaceTreeBloc>()
.add(ClearCachedData());
showDialog(
context: context,
barrierDismissible: false,
builder: (BuildContext context) {
return EditUserDialog(
userId: user.uuid);
},
).then((v) {
if (v != null) {
_blocRole.add(const GetUsers());
if (v != null) {
_blocRole.add(const GetUsers());
}
}
});
},
)
: actionButton(
title: "Edit",
),
actionButton(
title: "Delete",
onTap: () {
showDialog(
context: context,
barrierDismissible: false,
builder: (BuildContext context) {
return DeleteUserDialog(
onTapDelete: () async {
try {
_blocRole.add(DeleteUserEvent(
user.uuid, context));
await Future.delayed(
const Duration(seconds: 2));
return true;
} catch (e) {
return false;
}
});
},
)
: actionButton(
title: "Edit",
),
actionButton(
title: "Delete",
onTap: () {
showDialog(
context: context,
barrierDismissible: false,
builder: (BuildContext context) {
return DeleteUserDialog(onTapDelete: () async {
try {
_blocRole.add(DeleteUserEvent(user.uuid, context));
await Future.delayed(const Duration(seconds: 2));
return true;
} catch (e) {
return false;
}
});
},
).then((v) {
if (v != null) {
_blocRole.add(const GetUsers());
}
});
},
),
],
),
];
}).toList(),
).then((v) {
if (v != null) {
_blocRole.add(const GetUsers());
}
});
},
),
],
),
];
}).toList(),
),
),
Padding(
padding: const EdgeInsets.all(8.0),
@@ -486,14 +523,20 @@ class UsersPage extends StatelessWidget {
visiblePagesCount: 4,
buttonRadius: 10,
selectedButtonColor: ColorsManager.secondaryColor,
buttonUnSelectedBorderColor: ColorsManager.grayBorder,
lastPageIcon: const Icon(Icons.keyboard_double_arrow_right),
firstPageIcon: const Icon(Icons.keyboard_double_arrow_left),
totalPages:
(_blocRole.totalUsersCount.length / _blocRole.itemsPerPage).ceil(),
buttonUnSelectedBorderColor:
ColorsManager.grayBorder,
lastPageIcon:
const Icon(Icons.keyboard_double_arrow_right),
firstPageIcon:
const Icon(Icons.keyboard_double_arrow_left),
totalPages: (_blocRole.totalUsersCount.length /
_blocRole.itemsPerPage)
.ceil(),
currentPage: _blocRole.currentPage,
onPageChanged: (int pageNumber) {
context.read<UserTableBloc>().add(ChangePage(pageNumber));
context
.read<UserTableBloc>()
.add(ChangePage(pageNumber));
},
),
),
@@ -7,6 +7,7 @@ import 'package:syncrow_web/pages/routines/widgets/routine_dialogs/ceiling_senso
import 'package:syncrow_web/pages/routines/widgets/routine_dialogs/flush_presence_sensor/flush_presence_sensor.dart';
import 'package:syncrow_web/pages/routines/widgets/routine_dialogs/gateway/gateway_helper.dart';
import 'package:syncrow_web/pages/routines/widgets/routine_dialogs/one_gang_switch_dialog.dart';
import 'package:syncrow_web/pages/routines/widgets/routine_dialogs/power_clamp_enargy/energy_clamp_dialog.dart';
import 'package:syncrow_web/pages/routines/widgets/routine_dialogs/three_gang_switch_dialog.dart';
import 'package:syncrow_web/pages/routines/widgets/routine_dialogs/two_gang_switch_dialog.dart';
import 'package:syncrow_web/pages/routines/widgets/routine_dialogs/wall_sensor/wall_presence_sensor.dart';
@@ -137,6 +138,16 @@ class DeviceDialogHelper {
device: data['device'],
);
case 'PC':
return EnergyClampDialog.showEnergyClampFunctionsDialog(
context: context,
functions: functions,
uniqueCustomId: data['uniqueCustomId'],
deviceSelectedFunctions: deviceSelectedFunctions,
dialogType: dialogType,
device: data['device'],
);
default:
return null;
}
@@ -9,7 +9,6 @@ abstract class DeviceFunction<T> {
final double? max;
final double? min;
DeviceFunction({
required this.deviceId,
required this.deviceName,
@@ -114,4 +113,28 @@ class DeviceFunctionData {
max.hashCode ^
min.hashCode;
}
DeviceFunctionData copyWith({
String? entityId,
String? functionCode,
String? operationName,
String? condition,
dynamic value,
double? step,
String? unit,
double? max,
double? min,
}) {
return DeviceFunctionData(
entityId: entityId ?? this.entityId,
functionCode: functionCode ?? this.functionCode,
operationName: operationName ?? this.operationName,
condition: condition ?? this.condition,
value: value ?? this.value,
step: step ?? this.step,
unit: unit ?? this.unit,
max: max ?? this.max,
min: min ?? this.min,
);
}
}
@@ -0,0 +1,416 @@
import 'package:syncrow_web/pages/device_managment/power_clamp/models/power_clamp_batch_model.dart';
import 'package:syncrow_web/pages/routines/models/device_functions.dart';
import 'package:syncrow_web/pages/routines/models/pc/enrgy_clamp_operational_value.dart';
import 'package:syncrow_web/utils/constants/assets.dart';
abstract class EnergyClampFunctions extends DeviceFunction<PowerClampModel1> {
final String type;
EnergyClampFunctions({
required super.deviceId,
required super.deviceName,
required super.code,
required super.operationName,
required super.icon,
required this.type,
super.step,
super.unit,
super.max,
super.min,
});
List<EnergyClampOperationalValue> getOperationalValues();
}
// General & shared
class TotalEnergyConsumedStatusFunction extends EnergyClampFunctions {
TotalEnergyConsumedStatusFunction({
required super.deviceId,
required super.deviceName,
required super.type,
}) : super(
code: 'EnergyConsumed',
operationName: 'Total Energy Consumed',
icon: Assets.energyConsumedIcon,
min: 0.00,
max: 20000000.00,
step: 1,
unit: "kWh",
);
@override
List<EnergyClampOperationalValue> getOperationalValues() => [];
}
class TotalActivePowerConsumedStatusFunction extends EnergyClampFunctions {
TotalActivePowerConsumedStatusFunction({
required super.deviceId,
required super.deviceName,
required super.type,
}) : super(
code: 'ActivePower',
operationName: 'Total Active Power',
icon: Assets.powerActiveIcon,
min: -19800000,
max: 19800000,
step: 0.1,
unit: "kW",
);
@override
List<EnergyClampOperationalValue> getOperationalValues() => [];
}
class VoltagePhaseSequenceDetectionFunction extends EnergyClampFunctions {
VoltagePhaseSequenceDetectionFunction({
required super.deviceId,
required super.deviceName,
required super.type,
}) : super(
code: 'voltage_phase_seq',
operationName: 'Voltage phase sequence detection',
icon: Assets.voltageIcon,
);
@override
List<EnergyClampOperationalValue> getOperationalValues() => [
EnergyClampOperationalValue(
icon: Assets.voltageIcon, description: '0', value: '0'),
EnergyClampOperationalValue(
icon: Assets.voltageIcon, description: '1', value: '1'),
EnergyClampOperationalValue(
icon: Assets.voltageIcon, description: '2', value: '2'),
EnergyClampOperationalValue(
icon: Assets.voltageIcon, description: '3', value: '3'),
EnergyClampOperationalValue(
icon: Assets.voltageIcon, description: '4', value: '4'),
EnergyClampOperationalValue(
icon: Assets.voltageIcon, description: '5', value: '5'),
];
}
class TotalCurrentStatusFunction extends EnergyClampFunctions {
TotalCurrentStatusFunction({
required super.deviceId,
required super.deviceName,
required super.type,
}) : super(
code: 'Current',
operationName: 'Total Current',
icon: Assets.voltMeterIcon,
min: 0.000,
max: 9000.000,
step: 1,
unit: "A",
);
@override
List<EnergyClampOperationalValue> getOperationalValues() => [];
}
class FrequencyStatusFunction extends EnergyClampFunctions {
FrequencyStatusFunction({
required super.deviceId,
required super.deviceName,
required super.type,
}) : super(
code: 'Frequency',
operationName: 'Frequency',
icon: Assets.frequencyIcon,
min: 0,
max: 80,
step: 1,
unit: "Hz",
);
@override
List<EnergyClampOperationalValue> getOperationalValues() => [];
}
// Phase A
class EnergyConsumedAStatusFunction extends EnergyClampFunctions {
EnergyConsumedAStatusFunction({
required super.deviceId,
required super.deviceName,
required super.type,
}) : super(
code: 'EnergyConsumedA',
operationName: 'Energy Consumed A',
icon: Assets.energyConsumedIcon,
min: 0.00,
max: 20000000.00,
step: 1,
unit: "kWh",
);
@override
List<EnergyClampOperationalValue> getOperationalValues() => [];
}
class ActivePowerAStatusFunction extends EnergyClampFunctions {
ActivePowerAStatusFunction({
required super.deviceId,
required super.deviceName,
required super.type,
}) : super(
code: 'ActivePowerA',
operationName: 'Active Power A',
icon: Assets.powerActiveIcon,
min: 200,
max: 300,
step: 1,
unit: "kW",
);
@override
List<EnergyClampOperationalValue> getOperationalValues() => [];
}
class VoltageAStatusFunction extends EnergyClampFunctions {
VoltageAStatusFunction({
required super.deviceId,
required super.deviceName,
required super.type,
}) : super(
code: 'VoltageA',
operationName: 'Voltage A',
icon: Assets.voltageIcon,
min: 0.0,
max: 500,
step: 1,
unit: "V",
);
@override
List<EnergyClampOperationalValue> getOperationalValues() => [];
}
class PowerFactorAStatusFunction extends EnergyClampFunctions {
PowerFactorAStatusFunction({
required super.deviceId,
required super.deviceName,
required super.type,
}) : super(
code: 'PowerFactorA',
operationName: 'Power Factor A',
icon: Assets.speedoMeter,
min: 0.00,
max: 1.00,
step: 0.1,
unit: "",
);
@override
List<EnergyClampOperationalValue> getOperationalValues() => [];
}
class CurrentAStatusFunction extends EnergyClampFunctions {
CurrentAStatusFunction({
required super.deviceId,
required super.deviceName,
required super.type,
}) : super(
code: 'CurrentA',
operationName: 'Current A',
icon: Assets.voltMeterIcon,
min: 0.000,
max: 3000.000,
step: 1,
unit: "A",
);
@override
List<EnergyClampOperationalValue> getOperationalValues() => [];
}
// Phase B
class EnergyConsumedBStatusFunction extends EnergyClampFunctions {
EnergyConsumedBStatusFunction({
required super.deviceId,
required super.deviceName,
required super.type,
}) : super(
code: 'EnergyConsumedB',
operationName: 'Energy Consumed B',
icon: Assets.energyConsumedIcon,
min: 0.00,
max: 20000000.00,
step: 1,
unit: "kWh",
);
@override
List<EnergyClampOperationalValue> getOperationalValues() => [];
}
class ActivePowerBStatusFunction extends EnergyClampFunctions {
ActivePowerBStatusFunction({
required super.deviceId,
required super.deviceName,
required super.type,
}) : super(
code: 'ActivePowerB',
operationName: 'Active Power B',
icon: Assets.powerActiveIcon,
min: -6600000,
max: 6600000,
step: 1,
unit: "kW",
);
@override
List<EnergyClampOperationalValue> getOperationalValues() => [];
}
class VoltageBStatusFunction extends EnergyClampFunctions {
VoltageBStatusFunction({
required super.deviceId,
required super.deviceName,
required super.type,
}) : super(
code: 'VoltageB',
operationName: 'Voltage B',
icon: Assets.voltageIcon,
min: 0.0,
max: 500,
step: 1,
unit: "V",
);
@override
List<EnergyClampOperationalValue> getOperationalValues() => [];
}
class CurrentBStatusFunction extends EnergyClampFunctions {
CurrentBStatusFunction({
required super.deviceId,
required super.deviceName,
required super.type,
}) : super(
code: 'CurrentB',
operationName: 'Current B',
icon: Assets.voltMeterIcon,
min: 0.000,
max: 3000.000,
step: 1,
unit: "A",
);
@override
List<EnergyClampOperationalValue> getOperationalValues() => [];
}
class PowerFactorBStatusFunction extends EnergyClampFunctions {
PowerFactorBStatusFunction({
required super.deviceId,
required super.deviceName,
required super.type,
}) : super(
code: 'PowerFactorB',
operationName: 'Power Factor B',
icon: Assets.speedoMeter,
min: 0.0,
max: 1.0,
step: 0.1,
unit: "",
);
@override
List<EnergyClampOperationalValue> getOperationalValues() => [];
}
// Phase C
class EnergyConsumedCStatusFunction extends EnergyClampFunctions {
EnergyConsumedCStatusFunction({
required super.deviceId,
required super.deviceName,
required super.type,
}) : super(
code: 'EnergyConsumedC',
operationName: 'Energy Consumed C',
icon: Assets.energyConsumedIcon,
min: 0.00,
max: 20000000.00,
step: 1,
unit: "kWh",
);
@override
List<EnergyClampOperationalValue> getOperationalValues() => [];
}
class ActivePowerCStatusFunction extends EnergyClampFunctions {
ActivePowerCStatusFunction({
required super.deviceId,
required super.deviceName,
required super.type,
}) : super(
code: 'ActivePowerC',
operationName: 'Active Power C',
icon: Assets.powerActiveIcon,
min: -6600000,
max: 6600000,
step: 1,
unit: "kW",
);
@override
List<EnergyClampOperationalValue> getOperationalValues() => [];
}
class VoltageCStatusFunction extends EnergyClampFunctions {
VoltageCStatusFunction({
required super.deviceId,
required super.deviceName,
required super.type,
}) : super(
code: 'VoltageC',
operationName: 'Voltage C',
icon: Assets.voltageIcon,
min: 0.00,
max: 500,
step: 0.1,
unit: "V",
);
@override
List<EnergyClampOperationalValue> getOperationalValues() => [];
}
class CurrentCStatusFunction extends EnergyClampFunctions {
CurrentCStatusFunction({
required super.deviceId,
required super.deviceName,
required super.type,
}) : super(
code: 'CurrentC',
operationName: 'Current C',
icon: Assets.voltMeterIcon,
min: 0.000,
max: 3000.000,
step: 0.1,
unit: "A",
);
@override
List<EnergyClampOperationalValue> getOperationalValues() => [];
}
class PowerFactorCStatusFunction extends EnergyClampFunctions {
PowerFactorCStatusFunction({
required super.deviceId,
required super.deviceName,
required super.type,
}) : super(
code: 'PowerFactorC',
operationName: 'Power Factor C',
icon: Assets.speedoMeter,
min: 0.00,
max: 1.00,
step: 0.1,
unit: "",
);
@override
List<EnergyClampOperationalValue> getOperationalValues() => [];
}
@@ -0,0 +1,11 @@
class EnergyClampOperationalValue {
final String icon;
final String description;
final dynamic value;
EnergyClampOperationalValue({
required this.icon,
required this.description,
required this.value,
});
}
@@ -12,22 +12,53 @@ class ConditionToggle extends StatelessWidget {
});
static const _conditions = ["<", "==", ">"];
static const _icons = [
Icons.chevron_left,
Icons.drag_handle,
Icons.chevron_right
];
@override
Widget build(BuildContext context) {
return ToggleButtons(
onPressed: (index) => onChanged(_conditions[index]),
borderRadius: const BorderRadius.all(Radius.circular(8)),
selectedBorderColor: ColorsManager.primaryColorWithOpacity,
selectedColor: Colors.white,
fillColor: ColorsManager.primaryColorWithOpacity,
color: ColorsManager.primaryColorWithOpacity,
constraints: const BoxConstraints(
minHeight: 40.0,
minWidth: 40.0,
final selectedIndex = _conditions.indexOf(currentCondition ?? "==");
return Container(
height: 30,
width: MediaQuery.of(context).size.width * 0.1,
decoration: BoxDecoration(
color: ColorsManager.softGray.withOpacity(0.5),
borderRadius: BorderRadius.circular(50),
),
clipBehavior: Clip.antiAlias,
child: Row(
mainAxisSize: MainAxisSize.min,
children: List.generate(_conditions.length, (index) {
final isSelected = index == selectedIndex;
return Expanded(
child: InkWell(
onTap: () => onChanged(_conditions[index]),
child: AnimatedContainer(
duration: const Duration(milliseconds: 180),
curve: Curves.ease,
decoration: BoxDecoration(
color:
isSelected ? ColorsManager.vividBlue : Colors.transparent,
),
child: Center(
child: Icon(
_icons[index],
size: 20,
color: isSelected
? ColorsManager.whiteColors
: ColorsManager.blackColor,
weight: isSelected ? 700 : 500,
),
),
),
),
);
}),
),
isSelected: _conditions.map((c) => c == (currentCondition ?? "==")).toList(),
children: _conditions.map((c) => Text(c)).toList(),
);
}
}
@@ -40,6 +40,7 @@ class CustomRoutinesTextbox extends StatefulWidget {
class _CustomRoutinesTextboxState extends State<CustomRoutinesTextbox> {
late final TextEditingController _controller;
bool hasError = false;
String? errorMessage;
@@ -55,29 +56,63 @@ class _CustomRoutinesTextboxState extends State<CustomRoutinesTextbox> {
}
}
bool _isInitialized = false;
@override
void initState() {
super.initState();
int decimalPlaces = getDecimalPlaces(widget.stepIncreaseAmount);
double initialValue;
if (widget.initialValue != null &&
widget.initialValue is num &&
(widget.initialValue as num) == 0) {
initialValue = 0.0;
_initializeController();
}
void _initializeController() {
final decimalPlaces = getDecimalPlaces(widget.stepIncreaseAmount);
final dynamic initialValue = widget.initialValue;
double parsedValue;
if (initialValue is num) {
parsedValue = initialValue.toDouble();
} else if (initialValue is String) {
parsedValue = double.tryParse(initialValue) ?? widget.sliderRange.$1;
} else {
initialValue = double.tryParse(widget.displayedValue) ?? 0.0;
parsedValue = widget.sliderRange.$1;
}
_controller = TextEditingController(
text: initialValue.toStringAsFixed(decimalPlaces),
text: parsedValue.toStringAsFixed(decimalPlaces),
);
_isInitialized = true;
}
@override
void dispose() {
_controller.dispose();
super.dispose();
void didUpdateWidget(CustomRoutinesTextbox oldWidget) {
super.didUpdateWidget(oldWidget);
if (widget.initialValue != oldWidget.initialValue && _isInitialized) {
final decimalPlaces = getDecimalPlaces(widget.stepIncreaseAmount);
final dynamic initialValue = widget.initialValue;
double newValue;
if (initialValue is num) {
newValue = initialValue.toDouble();
} else if (initialValue is String) {
newValue = double.tryParse(initialValue) ?? widget.sliderRange.$1;
} else {
newValue = widget.sliderRange.$1;
}
final newValueText = newValue.toStringAsFixed(decimalPlaces);
if (_controller.text != newValueText) {
WidgetsBinding.instance.addPostFrameCallback((_) {
_controller.text = newValueText;
_controller.selection =
TextSelection.collapsed(offset: _controller.text.length);
});
}
}
}
void _validateInput(String value) {
final doubleValue = double.tryParse(value);
if (doubleValue == null) {
@@ -121,18 +156,6 @@ class _CustomRoutinesTextboxState extends State<CustomRoutinesTextbox> {
}
}
@override
void didUpdateWidget(CustomRoutinesTextbox oldWidget) {
super.didUpdateWidget(oldWidget);
if (widget.initialValue != oldWidget.initialValue) {
if (widget.initialValue != null &&
widget.initialValue is num &&
(widget.initialValue as num) == 0) {
int decimalPlaces = getDecimalPlaces(widget.stepIncreaseAmount);
_controller.text = 0.0.toStringAsFixed(decimalPlaces);
}
}
}
void _correctAndUpdateValue(String value) {
final doubleValue = double.tryParse(value) ?? 0.0;
@@ -227,9 +250,15 @@ class _CustomRoutinesTextboxState extends State<CustomRoutinesTextbox> {
color: ColorsManager.blackColor,
),
keyboardType: TextInputType.number,
inputFormatters: widget.withSpecialChar == true
? [FilteringTextInputFormatter.digitsOnly]
: null,
inputFormatters: [
FilteringTextInputFormatter.allow(
widget.withSpecialChar
? RegExp(r'^-?\d*\.?\d{0,' +
decimalPlaces.toString() +
r'}$')
: RegExp(r'\d+'),
),
],
decoration: const InputDecoration(
border: InputBorder.none,
isDense: true,
@@ -268,8 +297,9 @@ class _CustomRoutinesTextboxState extends State<CustomRoutinesTextbox> {
const SizedBox(height: 16),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 32),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
child: Wrap(
alignment: WrapAlignment.spaceBetween,
direction: Axis.horizontal,
children: [
Text(
'Min. ${widget.sliderRange.$1.toInt()}${widget.unit}',
@@ -279,6 +309,9 @@ class _CustomRoutinesTextboxState extends State<CustomRoutinesTextbox> {
fontWeight: FontWeight.w400,
),
),
const SizedBox(
width: 50,
),
Text(
'Max. ${widget.sliderRange.$2.toInt()}${widget.unit}',
style: context.textTheme.bodySmall?.copyWith(
+13 -3
View File
@@ -78,9 +78,9 @@ class IfContainer extends StatelessWidget {
'CPS',
'NCPS',
'WH',
'PC',
].contains(state.ifItems[index]
['productType'])) {
context.read<RoutineBloc>().add(
AddToIfContainer(
state.ifItems[index], false));
@@ -137,8 +137,18 @@ class IfContainer extends StatelessWidget {
context
.read<RoutineBloc>()
.add(AddToIfContainer(mutableData, false));
} else if (!['AC', '1G', '2G', '3G', 'WPS', 'GW', 'CPS', 'NCPS','WH']
.contains(mutableData['productType'])) {
} else if (![
'AC',
'1G',
'2G',
'3G',
'WPS',
'GW',
'CPS',
'NCPS',
'WH',
'PC',
].contains(mutableData['productType'])) {
context
.read<RoutineBloc>()
.add(AddToIfContainer(mutableData, false));
@@ -27,6 +27,7 @@ class _RoutineDevicesState extends State<RoutineDevices> {
'CPS',
'NCPS',
'WH',
'PC',
};
@override
@@ -74,25 +74,24 @@ class ACHelper {
SizedBox(
width: selectedFunction != null ? 320 : 360,
child: _buildFunctionsList(
context: context,
acFunctions: acFunctions,
device: device,
onFunctionSelected:
(functionCode, operationName) {
RoutineTapFunctionHelper.onTapFunction(
context,
functionCode: functionCode,
functionOperationName: operationName,
functionValueDescription:
selectedFunctionData.valueDescription,
deviceUuid: device?.uuid,
codesToAddIntoFunctionsWithDefaultValue: [
'temp_set',
'temp_current',
],
defaultValue: 0);
},
),
context: context,
acFunctions: acFunctions,
onFunctionSelected:
(functionCode, operationName) {
RoutineTapFunctionHelper.onTapFunction(
context,
functionCode: functionCode,
functionOperationName: operationName,
functionValueDescription:
selectedFunctionData
.valueDescription,
deviceUuid: device?.uuid,
codesToAddIntoFunctionsWithDefaultValue: [
'temp_set',
'temp_current',
],
defaultValue: 0);
}),
),
// Value selector
if (selectedFunction != null)
@@ -150,7 +149,6 @@ class ACHelper {
required BuildContext context,
required List<ACFunction> acFunctions,
required Function(String, String) onFunctionSelected,
required AllDevicesModel? device,
}) {
return ListView.separated(
shrinkWrap: false,
@@ -193,7 +191,6 @@ class ACHelper {
);
}
/// Build value selector for AC functions dialog
static Widget _buildValueSelector({
required BuildContext context,
required String selectedFunction,
@@ -207,19 +204,19 @@ class ACHelper {
acFunctions.firstWhere((f) => f.code == selectedFunction);
if (selectedFunction == 'temp_set' || selectedFunction == 'temp_current') {
// Convert stored integer value to display value
final displayValue =
(selectedFunctionData?.value ?? selectedFn.min ?? 0) / 10;
(selectedFunctionData?.value ?? selectedFn.min!) / 10;
final minValue = selectedFn.min! / 10;
final maxValue = selectedFn.max! / 10;
return CustomRoutinesTextbox(
withSpecialChar: true,
dividendOfRange: maxValue,
currentCondition: selectedFunctionData?.condition,
dialogType: selectedFn.type,
sliderRange: (minValue, maxValue),
displayedValue: displayValue.toStringAsFixed(1),
initialValue: displayValue.toDouble(),
displayedValue: displayValue.toString(),
initialValue: displayValue,
unit: selectedFn.unit!,
onConditionChanged: (condition) => context.read<FunctionBloc>().add(
AddFunction(
@@ -228,7 +225,7 @@ class ACHelper {
functionCode: selectedFunction,
operationName: selectedFn.operationName,
condition: condition,
value: 0,
value: (displayValue * 10).round(),
step: selectedFn.step,
unit: selectedFn.unit,
max: selectedFn.max,
@@ -236,28 +233,33 @@ class ACHelper {
),
),
),
onTextChanged: (value) => context.read<FunctionBloc>().add(
AddFunction(
functionData: DeviceFunctionData(
entityId: device?.uuid ?? '',
functionCode: selectedFunction,
operationName: selectedFn.operationName,
value: (value * 10).round(), // Store as integer
condition: selectedFunctionData?.condition,
step: selectedFn.step,
unit: selectedFn.unit,
max: selectedFn.max,
min: selectedFn.min,
onTextChanged: (value) {
final numericValue = double.tryParse(value.toString()) ?? minValue;
context.read<FunctionBloc>().add(
AddFunction(
functionData: DeviceFunctionData(
entityId: device?.uuid ?? '',
functionCode: selectedFunction,
operationName: selectedFn.operationName,
value: (numericValue * 10).round(),
condition: selectedFunctionData?.condition,
step: selectedFn.step,
unit: selectedFn.unit,
max: selectedFn.max,
min: selectedFn.min,
),
),
),
),
stepIncreaseAmount: selectedFn.step! / 10, // Convert step for display
);
},
stepIncreaseAmount: selectedFn.step! / 10,
);
}
// Rest of your existing code for other value selectors
final values = selectedFn.getOperationalValues();
return _buildOperationalValuesList(
context: context,
values: selectedFn.getOperationalValues(),
values: values,
selectedValue: selectedFunctionData?.value,
device: device,
operationName: operationName,
@@ -311,7 +313,7 @@ class ACHelper {
// );
// }
// /// Build condition toggle for AC functions dialog
/// Build condition toggle for AC functions dialog
// static Widget _buildConditionToggle(
// BuildContext context,
// String? currentCondition,
@@ -6,7 +6,6 @@ import 'package:syncrow_web/pages/routines/models/ceiling_presence_sensor_functi
import 'package:syncrow_web/pages/routines/models/device_functions.dart';
import 'package:syncrow_web/pages/routines/widgets/custom_routines_textbox.dart';
import 'package:syncrow_web/pages/routines/widgets/routine_dialogs/ceiling_sensor/cps_slider_helpers.dart';
import 'package:syncrow_web/pages/routines/widgets/slider_value_selector.dart';
class CpsDialogSliderSelector extends StatelessWidget {
const CpsDialogSliderSelector({
@@ -33,7 +32,7 @@ class CpsDialogSliderSelector extends StatelessWidget {
@override
Widget build(BuildContext context) {
return CustomRoutinesTextbox(
withSpecialChar: false,
withSpecialChar: true,
currentCondition: selectedFunctionData.condition,
dialogType: dialogType,
sliderRange:
@@ -0,0 +1,86 @@
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_svg/flutter_svg.dart';
import 'package:syncrow_web/pages/device_managment/all_devices/models/devices_model.dart';
import 'package:syncrow_web/pages/routines/bloc/functions_bloc/functions_bloc_bloc.dart';
import 'package:syncrow_web/pages/routines/models/device_functions.dart';
import 'package:syncrow_web/pages/routines/models/pc/enrgy_clamp_operational_value.dart';
class EnergyOperationalValuesList extends StatelessWidget {
final List<EnergyClampOperationalValue> values;
final dynamic selectedValue;
final AllDevicesModel? device;
final String operationName;
final String selectCode;
const EnergyOperationalValuesList({
required this.values,
required this.selectedValue,
required this.device,
required this.operationName,
required this.selectCode,
super.key,
});
@override
Widget build(BuildContext context) {
return ListView.builder(
padding: const EdgeInsets.all(20),
itemCount: values.length,
itemBuilder: (context, index) => _buildValueItem(context, values[index]),
);
}
Widget _buildValueItem(
BuildContext context, EnergyClampOperationalValue value) {
return Padding(
padding: const EdgeInsets.all(8.0),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
_buildValueIcon(context, value),
Expanded(child: _buildValueDescription(value)),
_buildValueRadio(context, value),
],
),
);
}
Widget _buildValueIcon(context, EnergyClampOperationalValue value) {
return Column(
children: [
SvgPicture.asset(value.icon, width: 25, height: 25),
],
);
}
Widget _buildValueDescription(EnergyClampOperationalValue value) {
return Padding(
padding: const EdgeInsets.symmetric(horizontal: 8.0),
child: Text(value.description),
);
}
Widget _buildValueRadio(context, EnergyClampOperationalValue value) {
return Radio<dynamic>(
value: value.value,
groupValue: selectedValue,
onChanged: (_) => _selectValue(context, value.value),
);
}
void _selectValue(BuildContext context, dynamic value) {
context.read<FunctionBloc>().add(
AddFunction(
functionData: DeviceFunctionData(
entityId: device?.uuid ?? '',
functionCode: selectCode,
operationName: operationName,
value: value,
),
),
);
}
}
@@ -0,0 +1,246 @@
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_svg/flutter_svg.dart';
import 'package:syncrow_web/pages/device_managment/all_devices/models/devices_model.dart';
import 'package:syncrow_web/pages/routines/bloc/functions_bloc/functions_bloc_bloc.dart';
import 'package:syncrow_web/pages/routines/bloc/routine_bloc/routine_bloc.dart';
import 'package:syncrow_web/pages/routines/models/device_functions.dart';
import 'package:syncrow_web/pages/routines/models/pc/energy_clamp_functions.dart';
import 'package:syncrow_web/pages/routines/widgets/dialog_footer.dart';
import 'package:syncrow_web/pages/routines/widgets/dialog_header.dart';
import 'package:syncrow_web/pages/routines/widgets/routine_dialogs/helpers/routine_tap_function_helper.dart';
import 'package:syncrow_web/pages/routines/widgets/routine_dialogs/power_clamp_enargy/energy_value_selector_widget.dart';
import 'package:syncrow_web/utils/color_manager.dart';
import 'package:syncrow_web/utils/extension/build_context_x.dart';
class EnergyClampDialog extends StatefulWidget {
final List<DeviceFunction> functions;
final AllDevicesModel? device;
final List<DeviceFunctionData>? deviceSelectedFunctions;
final String? uniqueCustomId;
final String? dialogType;
final bool removeComparetors;
const EnergyClampDialog({
super.key,
required this.functions,
this.device,
this.deviceSelectedFunctions,
this.uniqueCustomId,
this.dialogType,
this.removeComparetors = false,
});
static Future<Map<String, dynamic>?> showEnergyClampFunctionsDialog({
required BuildContext context,
required List<DeviceFunction> functions,
AllDevicesModel? device,
List<DeviceFunctionData>? deviceSelectedFunctions,
String? uniqueCustomId,
String? dialogType,
bool removeComparetors = false,
}) async {
return showDialog<Map<String, dynamic>?>(
context: context,
builder: (context) => EnergyClampDialog(
functions: functions,
device: device,
deviceSelectedFunctions: deviceSelectedFunctions,
uniqueCustomId: uniqueCustomId,
removeComparetors: removeComparetors,
dialogType: dialogType,
),
);
}
@override
State<EnergyClampDialog> createState() => _EnergyClampDialogState();
}
class _EnergyClampDialogState extends State<EnergyClampDialog> {
late final List<EnergyClampFunctions> _functions;
@override
void initState() {
super.initState();
_functions =
widget.functions.whereType<EnergyClampFunctions>().where((function) {
if (widget.dialogType == 'THEN') {
return function.type == 'THEN' || function.type == 'BOTH';
}
return function.type == 'IF' || function.type == 'BOTH';
}).toList();
}
@override
Widget build(BuildContext context) {
return BlocProvider(
create: (_) => FunctionBloc()
..add(InitializeFunctions(widget.deviceSelectedFunctions ?? [])),
child: _buildDialogContent(),
);
}
Widget _buildDialogContent() {
return AlertDialog(
contentPadding: EdgeInsets.zero,
content: BlocBuilder<FunctionBloc, FunctionBlocState>(
builder: (context, state) {
final selectedFunction = state.selectedFunction;
return Container(
width: selectedFunction != null ? 600 : 360,
height: 450,
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(20),
),
padding: const EdgeInsets.only(top: 20),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
const DialogHeader('Energy Clamp Conditions'),
Expanded(child: _buildMainContent(context, state)),
_buildDialogFooter(context, state),
],
),
);
},
),
);
}
Widget _buildMainContent(BuildContext context, FunctionBlocState state) {
return Row(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
_buildFunctionList(context, state),
if (state.selectedFunction != null) _buildValueSelector(context, state),
],
);
}
Widget _buildFunctionList(BuildContext context, FunctionBlocState state) {
final selectedFunction = state.selectedFunction;
final selectedFunctionData = state.addedFunctions.firstWhere(
(f) => f.functionCode == selectedFunction,
orElse: () => DeviceFunctionData(
entityId: '',
functionCode: selectedFunction ?? '',
operationName: '',
value: null,
),
);
return SizedBox(
width: 360,
child: ListView.separated(
shrinkWrap: false,
physics: const AlwaysScrollableScrollPhysics(),
itemCount: _functions.length,
separatorBuilder: (context, index) => const Padding(
padding: EdgeInsets.symmetric(horizontal: 40.0),
child: Divider(color: ColorsManager.dividerColor),
),
itemBuilder: (context, index) {
final function = _functions[index];
return ListTile(
leading: SvgPicture.asset(
function.icon,
width: 24,
height: 24,
placeholderBuilder: (context) => const SizedBox(
width: 24,
height: 24,
),
),
title: Text(
function.operationName,
style: context.textTheme.bodyMedium,
),
trailing: const Icon(
Icons.arrow_forward_ios,
size: 16,
color: ColorsManager.textGray,
),
onTap: () => RoutineTapFunctionHelper.onTapFunction(
context,
functionCode: function.code,
functionOperationName: function.operationName,
functionValueDescription: selectedFunctionData.valueDescription,
deviceUuid: widget.device?.uuid,
codesToAddIntoFunctionsWithDefaultValue: [
'VoltageA',
'CurrentA',
'ActivePowerA',
'PowerFactorA',
'ReactivePowerA',
'EnergyConsumedA',
'VoltageB',
'CurrentB',
'ActivePowerB',
'PowerFactorB',
'ReactivePowerB',
'EnergyConsumedB',
'VoltageC',
'CurrentC',
'ActivePowerC',
'PowerFactorC',
'ReactivePowerC',
'EnergyConsumedC',
'EnergyConsumed',
'Current',
'ActivePower',
'ReactivePower',
'Frequency',
],
),
);
},
),
);
}
Widget _buildValueSelector(BuildContext context, FunctionBlocState state) {
final selectedFunction = state.selectedFunction!;
final functionData = state.addedFunctions.firstWhere(
(f) => f.functionCode == selectedFunction,
orElse: () => DeviceFunctionData(
entityId: '',
functionCode: selectedFunction,
operationName: state.selectedOperationName ?? '',
value: null,
),
);
return Expanded(
child: EnergyValueSelectorWidget(
selectedFunction: selectedFunction,
functionData: functionData,
functions: _functions,
device: widget.device,
dialogType: widget.dialogType!,
removeComparators: widget.removeComparetors,
),
);
}
Widget _buildDialogFooter(BuildContext context, FunctionBlocState state) {
return DialogFooter(
onCancel: () => Navigator.pop(context),
onConfirm: state.addedFunctions.isNotEmpty
? () {
context.read<RoutineBloc>().add(
AddFunctionToRoutine(
state.addedFunctions,
widget.uniqueCustomId!,
),
);
Navigator.pop(
context,
{'deviceId': widget.functions.first.deviceId},
);
}
: null,
isConfirmEnabled: state.selectedFunction != null,
);
}
}
@@ -0,0 +1,85 @@
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:syncrow_web/pages/device_managment/all_devices/models/devices_model.dart';
import 'package:syncrow_web/pages/routines/bloc/functions_bloc/functions_bloc_bloc.dart';
import 'package:syncrow_web/pages/routines/models/device_functions.dart';
import 'package:syncrow_web/pages/routines/models/pc/energy_clamp_functions.dart';
import 'package:syncrow_web/pages/routines/widgets/custom_routines_textbox.dart';
import 'package:syncrow_web/pages/routines/widgets/routine_dialogs/power_clamp_enargy/enargy_operational_values_list.dart';
class EnergyValueSelectorWidget extends StatelessWidget {
final String selectedFunction;
final DeviceFunctionData functionData;
final List<EnergyClampFunctions> functions;
final AllDevicesModel? device;
final String dialogType;
final bool removeComparators;
const EnergyValueSelectorWidget({
required this.selectedFunction,
required this.functionData,
required this.functions,
required this.device,
required this.dialogType,
required this.removeComparators,
super.key,
});
@override
Widget build(BuildContext context) {
final selectedFn =
functions.firstWhere((f) => f.code == selectedFunction);
final values = selectedFn.getOperationalValues();
final step = selectedFn.step ?? 1.0;
final _unit = selectedFn.unit ?? '';
final (double, double) sliderRange =
(selectedFn.min ?? 0.0, selectedFn.max ?? 100.0);
if (_isSliderFunction(selectedFunction)) {
return CustomRoutinesTextbox(
withSpecialChar: false,
currentCondition: functionData.condition,
dialogType: dialogType,
sliderRange: sliderRange,
displayedValue: functionData.value,
initialValue: functionData.value ?? 0.0,
onConditionChanged: (condition) => context.read<FunctionBloc>().add(
AddFunction(
functionData: DeviceFunctionData(
entityId: device?.uuid ?? '',
functionCode: selectedFunction,
operationName: functionData.operationName,
condition: condition,
value: functionData.value ?? 0,
),
),
),
onTextChanged: (value) => context.read<FunctionBloc>().add(
AddFunction(
functionData: DeviceFunctionData(
entityId: device?.uuid ?? '',
functionCode: selectedFunction,
operationName: functionData.operationName,
value: value.toInt(),
condition: functionData.condition,
),
),
),
unit: _unit,
dividendOfRange: 1,
stepIncreaseAmount: step,
);
}
return EnergyOperationalValuesList(
values: values,
selectedValue: functionData.value,
device: device,
operationName: selectedFn.operationName,
selectCode: selectedFunction,
);
}
bool _isSliderFunction(String function) =>
!['voltage_phase_seq'].contains(function);
}
@@ -33,7 +33,7 @@ class WpsValueSelectorWidget extends StatelessWidget {
if (_isSliderFunction(selectedFunction)) {
return CustomRoutinesTextbox(
withSpecialChar: false,
withSpecialChar: true,
currentCondition: functionData.condition,
dialogType: dialogType,
sliderRange: sliderRange,
@@ -242,7 +242,8 @@ class ThenContainer extends StatelessWidget {
'GW',
'CPS',
"NCPS",
"WH"
"WH",
'PC',
].contains(mutableData['productType'])) {
context.read<RoutineBloc>().add(AddToThenContainer(mutableData));
}
@@ -38,7 +38,7 @@ class VisitorPasswordDialog extends StatelessWidget {
if (visitorBloc.passwordStatus!.failedOperations.isNotEmpty)
Column(
children: [
const Text('Failed Devises'),
const Text('Failed Devices'),
SizedBox(
width: 200,
height: 50,
@@ -63,7 +63,7 @@ class VisitorPasswordDialog extends StatelessWidget {
if (visitorBloc.passwordStatus!.successOperations.isNotEmpty)
Column(
children: [
const Text('Success Devises'),
const Text('Success Devices'),
SizedBox(
width: 200,
height: 50,
+10
View File
@@ -73,4 +73,14 @@ abstract class ColorsManager {
static const Color vividBlue = Color(0xFF023DFE);
static const Color semiTransparentRed = Color(0x99FF0000);
static const Color grey700 = Color(0xFF2D3748);
static const Color goodGreen = Color(0xFF0CEC16);
static const Color moderateYellow = Color(0xFFFAC96C);
static const Color poorOrange = Color(0xFFEC7400);
static const Color unhealthyRed = Color(0xFFD40000);
static const Color severePink = Color(0xFFD40094);
static const Color hazardousPurple = Color(0xFFBA01FD);
static const Color maxPurple = Color(0xFF962DFF);
static const Color maxPurpleDot = Color(0xFF5F00BD);
static const Color minBlue = Color(0xFF93AAFD);
static const Color minBlueDot = Color(0xFF023DFE);
}
+4 -1
View File
@@ -448,5 +448,8 @@ class Assets {
static const String indentLevelIcon = 'assets/icons/indent_level_icon.svg';
static const String triggerLevelIcon = 'assets/icons/trigger_level_icon.svg';
static const String blankCalendar = 'assets/icons/blank_calendar.svg';
static const String refreshStatusIcon = 'assets/icons/refresh_status_icon.svg';
static const String refreshStatusIcon =
'assets/icons/refresh_status_icon.svg';
static const String energyConsumedIcon =
'assets/icons/energy_consumed_icon.svg';
}
+2
View File
@@ -19,6 +19,7 @@ enum DeviceType {
WaterLeak,
NCPS,
DoorSensor,
PC,
Other,
}
/*
@@ -59,4 +60,5 @@ Map<String, DeviceType> devicesTypesMap = {
'GD': DeviceType.GarageDoor,
'WL': DeviceType.WaterLeak,
'NCPS': DeviceType.NCPS,
'PC': DeviceType.PC,
};