Compare commits

..

151 Commits

Author SHA1 Message Date
78f42dacf6 Adjust ConditionToggle widget dimensions and colors for improved UI consistency 2025-06-01 14:37:42 +03:00
0a9d53e5bd Refactor ConditionToggle widget to display icons with corresponding conditions 2025-05-29 10:48:12 +03:00
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
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
7c65b874eb Refactor table layout to accommodate dynamic table size 2025-05-28 16:40:44 +03:00
25db6ec687 Created pull_request_template.md . 2025-05-28 14:24:03 +03:00
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
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
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
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
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
03009ed276 made a RangeOfAqiChart._lines a getter. 2025-05-27 13:21:42 +03:00
a1142eb38c gave range of aqi chart a tooltip that shows the necessary data. 2025-05-27 13:17:05 +03:00
1aa7bf2162 fixed charts clipping overflow in chart. 2025-05-27 12:37:36 +03:00
043820f84f does not emit an entirely new state when we already have chart data on loading. 2025-05-27 12:33:48 +03:00
d90d3d4026 added loading state to range of aqi chart. 2025-05-27 12:29:06 +03:00
3ac5254abf fixed bug in total energy consumption chart. 2025-05-27 12:26:21 +03:00
f5d926f5a2 modify left side titles. 2025-05-27 12:21:59 +03:00
c1d6db8bba Merge branch 'dev' into Fix-Bugs-Related-TextForm-Routine 2025-05-27 09:56:07 +03:00
50fc5f9562 Add 'PC' device to routine 2025-05-27 09:54:21 +03:00
1b0d8d446c modified flex's values. 2025-05-27 09:47:06 +03:00
8a5173f429 made font size of AqiTypeDropdown slightly smaller. 2025-05-27 09:36:35 +03:00
bee8652d03 responsivness 2025-05-26 16:59:44 +03:00
9546d7bdd1 fixed titles widget for bottom title. 2025-05-26 16:56:38 +03:00
cb4956f915 made range of aqi fake data random and not linear. 2025-05-26 16:56:25 +03:00
ec7b0aa078 shows AnalyticsErrorWidget and spacing under it only when there is an error. 2025-05-26 15:58:34 +03:00
296b03e1aa shows month data instead of index on bottom titles of RangeOfAqiChart. 2025-05-26 15:54:56 +03:00
177c7f1030 Responsiveness of RangeOfAqiChartTitle. 2025-05-26 15:49:38 +03:00
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
0b4337fb6c sp-1493-data-formatting-2.0. 2025-05-26 15:17:29 +03:00
171dc52e28 Created AqiTypeDropdown. 2025-05-26 15:10:30 +03:00
642d8e9591 Merge pull request #204 from SyncrowIOT/SP-1493-data-formatting
SP-1493-data-formatting
2025-05-26 14:24:46 +03:00
5a8ef578c3 SP-1493-data-formatting 2025-05-26 14:16:43 +03:00
63ca98895f moved RangeOfAqiChartTitle. 2025-05-26 13:27:12 +03:00
7e54cfdccd Implemented min, max, average informative cells to RangeOfAqiChartBox. 2025-05-26 13:25:14 +03:00
fb4d44450f Disabled animation in RangeOfAqiChart. 2025-05-26 11:25:12 +03:00
12e4285b14 removed unnecessary Stack widget from RangeOfAqiChart. 2025-05-26 11:24:53 +03:00
82adbcf4df loads and clears aqi range data in FetchAirQualityDataHelper. 2025-05-26 11:24:00 +03:00
7305d511bc Added spaceUuid to GetRangeOfAqiParam model. 2025-05-26 11:23:33 +03:00
61acaa17c5 fixed typo. 2025-05-26 11:22:11 +03:00
4af81bcc10 make the aqi range chart read its data from RangeOfAqiBloc. 2025-05-26 11:22:05 +03:00
d4dd7a19ba make the generated fake aqi range data, look better on the chart. 2025-05-26 11:21:42 +03:00
9ab906d24c Injected RangeOfAqiBloc into AnalyticsPage. 2025-05-26 11:10:23 +03:00
5c57143ea5 Created RangeOfAqiBloc along with its events, and state. 2025-05-26 11:09:45 +03:00
4a3085e1b4 Created RangeOfAqiService along with its fake implementation until the API is ready. 2025-05-26 11:00:57 +03:00
eb8ba1806c Created GetRangeOfAqiParam model. 2025-05-26 10:59:07 +03:00
902419f9c4 Created RangeOfAqi model. 2025-05-26 10:58:05 +03:00
926bcd9a5d Extracted lines data into a helper method for ease of readability. 2025-05-26 10:48:04 +03:00
33f9add78a Extracted some logic of RangeOfAqiChart into a helper class. 2025-05-26 10:41:36 +03:00
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
791b71276a populated linear data for RangeOfAqiChart, for a more pleasant dev experience and debugging. 2025-05-26 10:29:38 +03:00
24e3eb2311 extracted titlesData into a private factory method to enahnce readability. 2025-05-26 10:18:15 +03:00
82006e9aaf Implemented the side titles of RangeOfAqiChart. 2025-05-26 10:12:52 +03:00
cedef666f6 Merge pull request #202 from SyncrowIOT/SP-1493-rework
SP-1493 rework
2025-05-26 10:03:10 +03:00
a10d998ec6 Merge pull request #203 from SyncrowIOT/SP-1513-rework
SP-1513-rework
2025-05-26 10:02:49 +03:00
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
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
39351a710d Added aqi info colors to ColorsManager. 2025-05-25 12:18:09 +03:00
c8fe4e3baa Created an initial version of RangeOfAqiChart. 2025-05-25 12:01:45 +03:00
12deceb7d3 SP-1513-rework 2025-05-25 11:35:01 +03:00
9d27ed2dc5 SP-1506 rework, coloring and padding. 2025-05-25 11:13:24 +03:00
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
6606491458 made active dynamic 2025-05-25 10:59:41 +03:00
92abcdc4f9 SP-1492-landing_page_analytics_button_design. 2025-05-25 10:57:23 +03:00
7aa9e7e5dc fixed typos. 2025-05-22 16:44:32 +03:00
e9abac7933 added analytics icon. 2025-05-22 16:44:22 +03:00
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
5b13962d41 removed unnecessary * 1 calculation of height. 2025-05-22 15:57:03 +03:00
8c53d5322a SP-1591 2025-05-22 15:53:18 +03:00
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
d43c1847ff SP-1591 2025-05-22 15:44:19 +03:00
4c5b390887 Fixed typos. 2025-05-22 15:42:49 +03:00
5eeac01666 cannot select a community in AirQualityDataLoadingStrategy. 2025-05-22 15:35:04 +03:00
717d698378 can select child spaces with children in AirQualityDataLoadingStrategy. 2025-05-22 15:23:42 +03:00
9adbbb9a2d Integrated and implemented devices dropdown into the newly created widget AirQualityEndSideWidget. 2025-05-22 15:19:50 +03:00
e792dbd72f SP-1591/ Implement business logic in AirQualityDataLoadingStrategy for community structure loading strategy. 2025-05-22 14:58:42 +03:00
9eaa367d32 fix horizontal scroll bar 2025-05-22 05:48:49 -05:00
d2eea33714 Prepared AirQualityView layout and structure with PlaceHolder widgets. 2025-05-22 12:24:13 +03:00
24372a0618 Merge pull request #198 from SyncrowIOT/SP-1580-FE-Watermark-Does-Not-Match-Design-Specification
SP-1580-FE-Watermark-Does-Not-Match-Design-Specification
2025-05-22 11:25:49 +03:00
8988947694 Merge pull request #191 from SyncrowIOT/syncrow_analytics_sidebar_selection_behavior
Syncrow analytics sidebar selection behavior
2025-05-22 11:25:22 +03:00
ef875ef7dc Merge pull request #197 from SyncrowIOT/SP-1510-occupancy_chart_api_integration
Sp 1510 occupancy chart api integration
2025-05-22 11:24:55 +03:00
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
568b6be354 Created AirQualityView widget for the new Air Quality analytics module. 2025-05-21 16:46:38 +03:00
94e4fbd5db Apply correct business logic in OccupancyDataLoadingStrategy. 2025-05-21 16:08:48 +03:00
302ef36b17 Merge branch 'dev' of https://github.com/SyncrowIOT/web into syncrow_analytics_sidebar_selection_behavior 2025-05-21 15:56:29 +03:00
c508d016c2 SP-1580-FE-Watermark-Does-Not-Match-Design-Specification 2025-05-21 11:08:00 +03:00
e0ad7855d3 converted GetOccupancyParam.toJson to an expression method. 2025-05-21 10:59:04 +03:00
ecf588cfcb reverted to dynamic endpoint. 2025-05-21 10:58:21 +03:00
c9d15d102b fixes in OccupancyChart for a more pleasant UI. 2025-05-21 10:57:53 +03:00
64a29681de Merge pull request #196 from SyncrowIOT/SP-1475-FE-Only-the-arrow-button-is-clickable-make-the-whole-name-clickable-with-the-arrow
Sp 1475 fe only the arrow button is clickable make the whole name clickable with the arrow
2025-05-21 10:31:25 +03:00
02b07cfdb6 Merge branch 'dev' of https://github.com/SyncrowIOT/web into SP-1510-occupancy_chart_api_integration 2025-05-21 10:23:56 +03:00
0a94557eee SP-1510-Occupancy Chart API Integration. 2025-05-21 10:23:31 +03:00
4f8d1c4ffd Merge pull request #195 from SyncrowIOT/charts-reworks
Charts reworks
2025-05-21 10:22:55 +03:00
06b320a75d move icon to the center and change subspace title name 2025-05-21 10:16:12 +03:00
000fe70663 format. 2025-05-21 09:59:50 +03:00
4257f7f0f3 Corrected color of titles in charts. 2025-05-21 09:55:17 +03:00
b2bf3866a9 Deleted pubspec.lock, and added it to .gitignore. 2025-05-21 09:09:32 +03:00
a15b5439f0 Refactor user dropdown menu to display user's full name and arrow icon in a row for better layout consistency 2025-05-20 16:39:10 +03:00
fd2a09cada Deleted unused FakeEnergyConsumptionPerDeviceService. 2025-05-20 14:22:23 +03:00
4c2802acfc date picker decorations matched with design. 2025-05-20 14:20:16 +03:00
15343be258 show space uuid in analytics devices dropdown. 2025-05-20 14:11:25 +03:00
c21842cc6d removed overflow and fixed sizing and text drawing of PowerClampEnergyStatusWidget. 2025-05-20 13:56:00 +03:00
4326559e14 shows OccupancyHeatMapBox instead of a Placeholder in vertical srcollable AnalyticsOccupancyView. 2025-05-20 13:51:04 +03:00
4ded7d5202 Merge pull request #194 from SyncrowIOT/SP-1448-FE-Use-SliderValueSelector-widget-for-all-slider-widgets-in-Web-Routine
add step parameter in onTapFunction.
2025-05-19 11:37:56 +03:00
625f737791 SP-1506 rework
Remove extra line.

The colors of the data on X axis and Y axis are not identical to design.

Display days only on the X axis.

When the bar chart loads, we see it coming from the top (check the attached video).
2025-05-19 11:08:26 +03:00
494ae1c941 SP-1495 reworks.
1. Overlapping line not removed.
2. The colors of the data on X axis and Y axis are not identical to design.
3. Day 1 and 2 are missing on the X axis.
4. When the chart loads, we see it coming from the top right corner (check the attached video).
5. Display all available devices even if they have no data and make the chart empty state.
2025-05-19 10:52:44 +03:00
f67d0e2912 SP-1494 reworks.
1. When the chart loads, we see it coming from the top right corner (check the attached video).
2. Day 1 is missing on the X axis.
3. Overlapping line not removed.
2025-05-19 10:17:48 +03:00
17aad13b2a Merge pull request #193 from SyncrowIOT/feature/make_analytics_date_picker_not_show_future_dates
Feature/make_analytics_date_picker_not_show_future_dates
2025-05-15 16:58:25 +03:00
a849c1dafb removed unused import. 2025-05-15 16:31:11 +03:00
3e3e17019a format. 2025-05-15 16:22:54 +03:00
b1bae3cb15 fixed overflow bug on charts. 2025-05-15 15:59:02 +03:00
051bf657ed Changed background color of analytics date pickers to match the design language of the platform. 2025-05-15 15:29:09 +03:00
5191c1e456 Performed selection validation, and made future dates disabled. 2025-05-15 15:28:36 +03:00
7a073f10aa Merge pull request #189 from SyncrowIOT/1495-calendar-bugfixes
1495 calendar bugfixes
2025-05-15 14:31:11 +03:00
900d47faae Merge pull request #190 from SyncrowIOT/SP-1506-FE-implement-chart-per-phase
SP-1506-FE-chart per phase api integration.
2025-05-15 14:30:58 +03:00
e35a7fdc70 Merge pull request #192 from SyncrowIOT/bugfix/charts-horizontal-lines
bugfix/charts-horizontal-lines
2025-05-15 14:30:37 +03:00
d80f5e1f3a Refactor energy consumption charts to enhance grid data configuration
Updated the grid data for EnergyConsumptionByPhasesChart, EnergyConsumptionPerDeviceChart, and TotalEnergyConsumptionChart to include horizontal line visibility and set a horizontal interval of 250. Removed unused phasesJson constant from TotalEnergyConsumptionChart for cleaner code.
2025-05-15 14:25:13 +03:00
baaf5111b1 Applied correct business logic in EnergyManagementDataLoadingStrategy. 2025-05-15 12:48:18 +03:00
745205063e added correct behavior to OccupancyDataLoadingStrategy. 2025-05-15 12:46:12 +03:00
c07b53107e SP-1506-FE-chart per phase api integration. 2025-05-15 10:51:09 +03:00
39d125ac7e loads energy management data on date changed. 2025-05-15 10:11:55 +03:00
ad15d0e138 loads occupancy chart on date changed. 2025-05-15 10:08:41 +03:00
e6d272a60d loads heatmap data on calendar change. 2025-05-15 10:06:13 +03:00
8dfe8d10d4 removed requestType from query parameters of RemoteOccupancyAnalyticsDevicesService._makeRequest. 2025-05-15 10:01:43 +03:00
5279020d08 Merge pull request #188 from SyncrowIOT/1495-energy-consumption-per-device-api-integration
1495-energy-consumption-per-device-api-integration.
2025-05-15 09:32:15 +03:00
da481536c4 1495-energy-consumption-per-device-api-integration. 2025-05-14 16:55:28 +03:00
f21366268a Merge pull request #187 from SyncrowIOT/SP-1509-FE-Implement-devices-status-based-on-the-selected-device-from-the-dropdown-list
Sp 1509 fe implement devices status based on the selected device from the dropdown list
2025-05-14 16:18:51 +03:00
c3aef736fd Merge pull request #186 from SyncrowIOT/1511-occupancy-heat-map-tooltip
1511-occupancy-heat-map-tooltip.
2025-05-14 16:18:08 +03:00
887ac58f40 fixed import. 2025-05-14 15:59:40 +03:00
c709477500 some refactors to further clarify intent. 2025-05-14 15:55:12 +03:00
63e7b3faa2 resets selection and clears data. 2025-05-14 15:47:07 +03:00
0e61e52bf8 Connected devices to widgets, and is currently making the necessary and correct api calls for everything to function properly. 2025-05-14 15:35:22 +03:00
7515b347ce analytics devices integtation. 2025-05-14 15:03:30 +03:00
3dfbcb5935 connect device dropdown to bloc. 2025-05-14 14:31:28 +03:00
4fd4a9b5bf loads analytics devices on sidebar selection. 2025-05-14 13:03:51 +03:00
14fa1b355e Added a uuid property to AnalyticsDevice. 2025-05-14 12:50:27 +03:00
78d4e58996 Added selected device state/event, and clear data event to AnalyticsDevicesBloc. 2025-05-14 12:50:16 +03:00
23b9cb5b78 Injected AnalyticsDevicesBloc into AnalyticsPage. 2025-05-14 12:42:51 +03:00
401d0a9788 Created AnalyticsDevicesBloc. 2025-05-14 12:41:44 +03:00
ac2b0d3fac Created an initial remote implementation of AnalyticsDevicesService. 2025-05-14 12:38:07 +03:00
3be7a377c0 Created AnalyticsDevicesService interface. 2025-05-14 12:37:52 +03:00
e4ee456384 Created empty AnalyticsDevice model. 2025-05-14 12:37:44 +03:00
f02c5d71ba Created GetAnalyticsDevicesParam. 2025-05-14 12:26:16 +03:00
d45ff262c7 Merge branch 'dev' into 1511-occupancy-heat-map-tooltip 2025-05-14 12:05:34 +03:00
ad227febc1 Merge pull request #185 from SyncrowIOT/SP-1512-FE-Apply-Responsive-Behavior-for-Dashboard-Layout-and-Sidebar-Collapse
Sp 1512 fe apply responsive behavior for dashboard layout and sidebar collapse
2025-05-14 12:04:41 +03:00
a9d6c6f4ee 1511-occupancy-heat-map-tooltip. 2025-05-14 12:03:47 +03:00
4d9e57c8b5 Created and connected a remote implementation that fetches the heat map occupancy per space from the API. 2025-05-14 10:51:37 +03:00
d1bb8da484 Updated OccupancyHeatMapModel model with what the api returns, and only used the necessary fields that the api returns for this feature to work. 2025-05-14 10:51:19 +03:00
300f9ae358 Matched the GetOccupancyHeatMapParam with what the API expects and removed the communityId since it is no longer necessary for the api, and renamed spaceId to spaceUuid for more clarity. 2025-05-14 10:49:32 +03:00
c1dab3400b removed a force unwrap from OccupancyHeatMap._maxValue to avoid any bugs. 2025-05-14 10:48:28 +03:00
46815585cb Fixed error in AnalyticsErrorWidget where it used to add the default error message to the errorMessage. 2025-05-14 10:47:54 +03:00
113 changed files with 4120 additions and 2168 deletions

30
.github/pull_request_template.md vendored Normal file
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

1
.gitignore vendored
View File

@ -30,6 +30,7 @@ migrate_working_dir/
.pub-cache/
.pub/
/build/
pubspec.lock
# Symbolication related
app.*.symbols

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

View File

@ -0,0 +1,19 @@
<svg width="101" height="101" viewBox="0 0 101 101" fill="none" xmlns="http://www.w3.org/2000/svg">
<g clip-path="url(#clip0_9554_2115)">
<path d="M93.8334 86.1048C97.5937 86.1048 100.653 83.0454 100.653 79.2849V9.83487C100.653 4.4501 96.2722 0.0692444 90.8875 0.0692444H10.4187C5.03394 0.0692444 0.653076 4.4501 0.653076 9.83487V79.2849C0.653076 83.0454 3.71245 86.1048 7.4728 86.1048H9.63745V96.163H2.6062C1.52749 96.163 0.653076 97.0376 0.653076 98.1161C0.653076 99.1946 1.52749 100.069 2.6062 100.069H98.7C99.7787 100.069 100.653 99.1946 100.653 98.1161C100.653 97.0376 99.7787 96.163 98.7 96.163H91.6687V86.1048H93.8334ZM4.55933 9.83487C4.55933 6.60401 7.18784 3.97549 10.4187 3.97549H90.8875C94.1183 3.97549 96.7468 6.60401 96.7468 9.83487V14.1317H4.75542C4.68921 14.1317 4.62378 14.1353 4.55933 14.1417V9.83487ZM22.1375 96.163H13.5437V78.5849H22.1375V96.163ZM44.0125 96.163H35.4187V56.9896H44.0125V96.163ZM65.8875 96.163H57.2937V73.5067H65.8875V96.163ZM89.7156 50.1669H77.2156C76.1369 50.1669 75.2625 51.0415 75.2625 52.12V61.9831C75.2625 63.0616 76.1369 63.9362 77.2156 63.9362C78.2943 63.9362 79.1687 63.0616 79.1687 61.9831V54.0732H87.7625V96.163H79.1687V75.1181C79.1687 74.0396 78.2943 73.1649 77.2156 73.1649C76.1369 73.1649 75.2625 74.0396 75.2625 75.1181V96.163H69.7937V71.5536C69.7937 70.4751 68.9193 69.6005 67.8406 69.6005H55.3406C54.2619 69.6005 53.3875 70.4751 53.3875 71.5536V96.163H47.9187V55.0364C47.9187 53.9579 47.0443 53.0833 45.9656 53.0833H33.4656C32.3869 53.0833 31.5125 53.9579 31.5125 55.0364V96.163H26.0437V76.6317C26.0437 75.5532 25.1693 74.6786 24.0906 74.6786H11.5906C10.5119 74.6786 9.63745 75.5532 9.63745 76.6317V82.1985H7.4728C5.86616 82.1985 4.55933 80.8915 4.55933 79.2849V18.028C4.62378 18.0345 4.68921 18.038 4.75542 18.038H96.7468V79.2849C96.7468 80.8915 95.4398 82.1985 93.8334 82.1985H91.6687V52.12C91.6687 51.0415 90.7943 50.1669 89.7156 50.1669Z" fill="white"/>
<path d="M88.1531 7.10049H44.0251C42.9464 7.10049 42.072 7.9751 42.072 9.05362C42.072 10.1321 42.9464 11.0067 44.0251 11.0067H88.1531C89.2318 11.0067 90.1062 10.1321 90.1062 9.05362C90.1062 7.9751 89.2318 7.10049 88.1531 7.10049Z" fill="white"/>
<path d="M22.3464 7.67276C21.9832 7.30948 21.4792 7.10049 20.9656 7.10049C20.4519 7.10049 19.948 7.30928 19.5847 7.67276C19.2214 8.03604 19.0125 8.53995 19.0125 9.05362C19.0125 9.56729 19.2214 10.0712 19.5847 10.4343C19.948 10.7978 20.4519 11.0067 20.9656 11.0067C21.4792 11.0067 21.9832 10.7978 22.3464 10.4343C22.7097 10.0712 22.9187 9.56729 22.9187 9.05362C22.9187 8.53995 22.7097 8.03604 22.3464 7.67276Z" fill="white"/>
<path d="M14.5339 7.67276C14.1707 7.30948 13.6667 7.10049 13.1531 7.10049C12.6394 7.10049 12.1355 7.30928 11.7722 7.67276C11.4089 8.03604 11.2 8.53995 11.2 9.05362C11.2 9.56729 11.4089 10.0712 11.7722 10.4343C12.1355 10.7978 12.6394 11.0067 13.1531 11.0067C13.6667 11.0067 14.1707 10.7978 14.5339 10.4343C14.8972 10.0712 15.1062 9.56729 15.1062 9.05362C15.1062 8.53995 14.8972 8.03604 14.5339 7.67276Z" fill="white"/>
<path d="M30.1589 7.67276C29.7957 7.30948 29.2937 7.10049 28.7781 7.10049C28.2644 7.10049 27.7605 7.30928 27.3972 7.67276C27.0339 8.03604 26.825 8.53995 26.825 9.05362C26.825 9.56729 27.0339 10.0712 27.3972 10.4343C27.7605 10.7978 28.2644 11.0067 28.7781 11.0067C29.2917 11.0067 29.7957 10.7978 30.1589 10.4343C30.5222 10.0712 30.7312 9.56729 30.7312 9.05362C30.7312 8.53995 30.5222 8.03604 30.1589 7.67276Z" fill="white"/>
<path d="M78.5964 67.5634C78.2332 67.1981 77.7292 66.9911 77.2156 66.9911C76.7019 66.9911 76.198 67.1983 75.8347 67.5634C75.4714 67.9267 75.2625 68.4306 75.2625 68.9442C75.2625 69.4579 75.4714 69.9618 75.8347 70.3251C76.198 70.6882 76.7019 70.8974 77.2156 70.8974C77.7292 70.8974 78.2332 70.6884 78.5964 70.3251C78.9597 69.9618 79.1687 69.4579 79.1687 68.9442C79.1687 68.4286 78.9597 67.9267 78.5964 67.5634Z" fill="white"/>
<path d="M83.4656 26.6198C79.5885 26.6198 76.4344 29.7739 76.4344 33.6511C76.4344 35.1911 76.9334 36.6161 77.7764 37.7757L64.8027 50.7493C63.7959 50.1921 62.6393 49.8737 61.4092 49.8737C60.3008 49.8737 59.252 50.1325 58.3186 50.5913L44.7719 37.0446C45.3291 36.0378 45.6475 34.8812 45.6475 33.6511C45.6475 29.7739 42.4934 26.6198 38.6162 26.6198C34.7391 26.6198 31.585 29.7739 31.585 33.6511C31.585 35.0062 31.9713 36.2722 32.6381 37.3468L21.1043 48.8804C20.0975 48.3232 18.9408 48.0048 17.7109 48.0048C13.8338 48.0048 10.6797 51.1589 10.6797 55.0361C10.6797 58.9132 13.8338 62.0673 17.7109 62.0673C21.5881 62.0673 24.7422 58.9132 24.7422 55.0361C24.7422 53.806 24.4238 52.6493 23.8666 51.6425L35.5381 39.971C36.4684 40.4259 37.5129 40.6821 38.6162 40.6821C39.8461 40.6821 41.0027 40.3638 42.0096 39.8066L55.4236 53.2204C54.7613 54.2927 54.3779 55.5544 54.3779 56.9046C54.3779 60.7818 57.532 63.9359 61.4092 63.9359C65.2863 63.9359 68.4404 60.7818 68.4404 56.9046C68.4404 55.6745 68.1221 54.5179 67.5648 53.5111L80.8861 40.1898C81.6855 40.5066 82.5553 40.6823 83.4656 40.6823C87.3428 40.6823 90.4969 37.5282 90.4969 33.6511C90.4969 29.7739 87.3428 26.6198 83.4656 26.6198ZM17.7111 58.1614C15.9881 58.1614 14.5861 56.7595 14.5861 55.0364C14.5861 53.3134 15.9881 51.9114 17.7111 51.9114C19.4342 51.9114 20.8361 53.3134 20.8361 55.0364C20.8361 56.7595 19.4342 58.1614 17.7111 58.1614ZM38.6164 36.7761C36.8934 36.7761 35.4914 35.3741 35.4914 33.6511C35.4914 31.928 36.8934 30.5261 38.6164 30.5261C40.3395 30.5261 41.7414 31.928 41.7414 33.6511C41.7414 35.3741 40.3395 36.7761 38.6164 36.7761ZM61.4094 60.03C59.6863 60.03 58.2844 58.628 58.2844 56.905C58.2844 55.182 59.6863 53.78 61.4094 53.78C63.1324 53.78 64.5344 55.182 64.5344 56.905C64.5344 58.628 63.1324 60.03 61.4094 60.03ZM83.4656 36.7761C81.7426 36.7761 80.3406 35.3741 80.3406 33.6511C80.3406 31.928 81.7426 30.5261 83.4656 30.5261C85.1887 30.5261 86.5906 31.928 86.5906 33.6511C86.5906 35.3741 85.1887 36.7761 83.4656 36.7761Z" fill="white"/>
<path d="M29.6238 25.255C29.2585 24.8917 28.7566 24.6825 28.2429 24.6825C27.7273 24.6825 27.2234 24.8915 26.8601 25.255C26.4968 25.6181 26.2898 26.122 26.2898 26.6357C26.2898 27.1493 26.4968 27.6532 26.8601 28.0165C27.2253 28.3798 27.7273 28.5888 28.2429 28.5888C28.7566 28.5888 29.2585 28.38 29.6238 28.0165C29.9871 27.6532 30.196 27.1493 30.196 26.6357C30.196 26.122 29.9871 25.6181 29.6238 25.255Z" fill="white"/>
<path d="M20.9656 24.6827H11.5906C10.5119 24.6827 9.63745 25.5573 9.63745 26.6358C9.63745 27.7143 10.5119 28.589 11.5906 28.589H20.9656C22.0443 28.589 22.9187 27.7143 22.9187 26.6358C22.9187 25.5573 22.0443 24.6827 20.9656 24.6827Z" fill="white"/>
<path d="M20.9656 34.0577H11.5906C10.5119 34.0577 9.63745 34.9323 9.63745 36.0108C9.63745 37.0893 10.5119 37.964 11.5906 37.964H20.9656C22.0443 37.964 22.9187 37.0893 22.9187 36.0108C22.9187 34.9323 22.0443 34.0577 20.9656 34.0577Z" fill="white"/>
</g>
<defs>
<clipPath id="clip0_9554_2115">
<rect width="100" height="100" fill="white" transform="translate(0.653076 0.0692444)"/>
</clipPath>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 6.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 40 KiB

View File

@ -0,0 +1,71 @@
class AnalyticsDevice {
const AnalyticsDevice({
required this.uuid,
required this.name,
this.createdAt,
this.updatedAt,
this.deviceTuyaUuid,
this.isActive,
this.productDevice,
this.spaceUuid,
});
final String uuid;
final String name;
final DateTime? createdAt;
final DateTime? updatedAt;
final String? deviceTuyaUuid;
final bool? isActive;
final ProductDevice? productDevice;
final String? spaceUuid;
factory AnalyticsDevice.fromJson(Map<String, dynamic> json) {
return AnalyticsDevice(
uuid: json['uuid'] as String,
name: json['name'] as String,
createdAt: json['createdAt'] != null ? DateTime.parse(json['createdAt'] as String) : null,
updatedAt: json['updatedAt'] != null ? DateTime.parse(json['updatedAt'] as String) : null,
deviceTuyaUuid: json['deviceTuyaUuid'] as String?,
isActive: json['isActive'] as bool?,
productDevice: json['productDevice'] != null
? ProductDevice.fromJson(json['productDevice'] as Map<String, dynamic>)
: null,
spaceUuid: (json['spaces'] as List<dynamic>?)
?.map((e) => e['uuid'])
.firstOrNull
?.toString(),
);
}
}
class ProductDevice {
const ProductDevice({
this.uuid,
this.createdAt,
this.updatedAt,
this.catName,
this.prodId,
this.name,
this.prodType,
});
final String? uuid;
final DateTime? createdAt;
final DateTime? updatedAt;
final String? catName;
final String? prodId;
final String? name;
final String? prodType;
factory ProductDevice.fromJson(Map<String, dynamic> json) {
return ProductDevice(
uuid: json['uuid'] as String?,
createdAt: json['createdAt'] != null ? DateTime.parse(json['createdAt'] as String) : null,
updatedAt: json['updatedAt'] != null ? DateTime.parse(json['updatedAt'] as String) : null,
catName: json['catName'] as String?,
prodId: json['prodId'] as String?,
name: json['name'] as String?,
prodType: json['prodType'] as String?,
);
}
}

View File

@ -1,18 +1,32 @@
import 'package:equatable/equatable.dart';
class Occupacy extends Equatable {
final String date;
final DateTime date;
final String occupancy;
final String spaceUuid;
final int occupiedSeconds;
const Occupacy({required this.date, required this.occupancy});
const Occupacy({
required this.date,
required this.occupancy,
required this.spaceUuid,
required this.occupiedSeconds,
});
factory Occupacy.fromJson(Map<String, dynamic> json) {
return Occupacy(
date: json['date'] as String,
occupancy: json['occupancy'] as String,
date: DateTime.parse(json['event_date'] as String? ?? '${DateTime.now()}'),
occupancy: (json['occupancy_percentage'] ?? 0).toString(),
spaceUuid: json['space_uuid'] as String? ?? '',
occupiedSeconds: json['occupied_seconds'] as int? ?? 0,
);
}
@override
List<Object?> get props => [date, occupancy];
List<Object?> get props => [
date,
occupancy,
spaceUuid,
occupiedSeconds,
];
}

View File

@ -1,22 +1,28 @@
import 'package:equatable/equatable.dart';
class OccupancyHeatMapModel extends Equatable {
final DateTime date;
final String uuid;
final int occupancy;
final DateTime eventDate;
final int countTotalPresenceDetected;
const OccupancyHeatMapModel({
required this.date,
required this.occupancy,
required this.uuid,
required this.eventDate,
required this.countTotalPresenceDetected,
});
factory OccupancyHeatMapModel.fromJson(Map<String, dynamic> json) {
return OccupancyHeatMapModel(
date: DateTime.parse(json['date'] as String),
occupancy: json['occupancy'] as int,
uuid: json['uuid'] as String? ?? '',
eventDate: DateTime.parse(
json['event_date'] as String? ?? '${DateTime.now()}',
),
countTotalPresenceDetected: json['count_total_presence_detected'] as int? ?? 0,
);
}
@override
List<Object?> get props => [date, occupancy];
List<Object?> get props => [uuid, eventDate, countTotalPresenceDetected];
}

View File

@ -1,27 +1,66 @@
import 'package:equatable/equatable.dart';
class PhasesEnergyConsumption extends Equatable {
final int month;
final double phaseA;
final double phaseB;
final double phaseC;
final String uuid;
final DateTime createdAt;
final DateTime updatedAt;
final String deviceUuid;
final DateTime date;
final double energyConsumedKw;
final double energyConsumedA;
final double energyConsumedB;
final double energyConsumedC;
const PhasesEnergyConsumption({
required this.month,
required this.phaseA,
required this.phaseB,
required this.phaseC,
required this.uuid,
required this.createdAt,
required this.updatedAt,
required this.deviceUuid,
required this.date,
required this.energyConsumedKw,
required this.energyConsumedA,
required this.energyConsumedB,
required this.energyConsumedC,
});
@override
List<Object?> get props => [month, phaseA, phaseB, phaseC];
List<Object?> get props => [
uuid,
createdAt,
updatedAt,
deviceUuid,
date,
energyConsumedKw,
energyConsumedA,
energyConsumedB,
energyConsumedC,
];
factory PhasesEnergyConsumption.fromJson(Map<String, dynamic> json) {
return PhasesEnergyConsumption(
month: json['month'] as int,
phaseA: (json['phaseA'] as num).toDouble(),
phaseB: (json['phaseB'] as num).toDouble(),
phaseC: (json['phaseC'] as num).toDouble(),
uuid: json['uuid'] as String,
createdAt: DateTime.parse(json['createdAt'] as String),
updatedAt: DateTime.parse(json['updatedAt'] as String),
deviceUuid: json['deviceUuid'] as String,
date: DateTime.parse(json['date'] as String),
energyConsumedKw: double.parse(json['energyConsumedKw']),
energyConsumedA: double.parse(json['energyConsumedA']),
energyConsumedB: double.parse(json['energyConsumedB']),
energyConsumedC: double.parse(json['energyConsumedC']),
);
}
Map<String, dynamic> toJson() {
return {
'uuid': uuid,
'createdAt': createdAt.toIso8601String(),
'updatedAt': updatedAt.toIso8601String(),
'deviceUuid': deviceUuid,
'date': date.toIso8601String().split('T')[0],
'energyConsumedKw': energyConsumedKw.toString(),
'energyConsumedA': energyConsumedA.toString(),
'energyConsumedB': energyConsumedB.toString(),
'energyConsumedC': energyConsumedC.toString(),
};
}
}

View File

@ -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];
}

View File

@ -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());
}
}

View File

@ -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();
}

View File

@ -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];
}

View File

@ -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,
),
),
);
}
}

View File

@ -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,
),
),
);
}
}

View File

@ -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()),
],
),
),
],
),
),
);
},
);
}
}

View File

@ -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,
);
},
),
),
),
],
);
}
}

View File

@ -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,
);
}
}

View File

@ -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),
);
}
}

View File

@ -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)),
],
),
);
},
);
}
}

View File

@ -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,
);
},
),
),
),
],
);
}
}

View File

@ -0,0 +1,69 @@
import 'package:bloc/bloc.dart';
import 'package:equatable/equatable.dart';
import 'package:syncrow_web/pages/analytics/models/analytics_device.dart';
import 'package:syncrow_web/pages/analytics/params/get_analytics_devices_param.dart';
import 'package:syncrow_web/pages/analytics/services/analytics_devices/analytics_devices_service.dart';
part 'analytics_devices_event.dart';
part 'analytics_devices_state.dart';
class AnalyticsDevicesBloc
extends Bloc<AnalyticsDevicesEvent, AnalyticsDevicesState> {
AnalyticsDevicesBloc(
this._analyticsDevicesService,
) : super(const AnalyticsDevicesState()) {
on<LoadAnalyticsDevicesEvent>(_onLoadAnalyticsDevices);
on<SelectAnalyticsDeviceEvent>(_onSelectAnalyticsDevice);
on<ClearAnalyticsDeviceEvent>(_onClearAnalyticsDevice);
}
final AnalyticsDevicesService _analyticsDevicesService;
Future<void> _onLoadAnalyticsDevices(
LoadAnalyticsDevicesEvent event,
Emitter<AnalyticsDevicesState> emit,
) async {
emit(const AnalyticsDevicesState(status: AnalyticsDevicesStatus.loading));
try {
final devices = await _analyticsDevicesService.getDevices(event.param);
emit(
AnalyticsDevicesState(
status: AnalyticsDevicesStatus.loaded,
devices: devices,
selectedDevice: devices.firstOrNull,
),
);
if (devices.isNotEmpty) {
event.onSuccess(devices.first);
}
} catch (e) {
emit(
AnalyticsDevicesState(
status: AnalyticsDevicesStatus.failure,
errorMessage: e.toString(),
),
);
}
}
void _onSelectAnalyticsDevice(
SelectAnalyticsDeviceEvent event,
Emitter<AnalyticsDevicesState> emit,
) {
emit(
AnalyticsDevicesState(
selectedDevice: event.device,
devices: state.devices,
errorMessage: state.errorMessage,
status: state.status,
),
);
}
void _onClearAnalyticsDevice(
ClearAnalyticsDeviceEvent event,
Emitter<AnalyticsDevicesState> emit,
) {
emit(const AnalyticsDevicesState());
}
}

View File

@ -0,0 +1,31 @@
part of 'analytics_devices_bloc.dart';
sealed class AnalyticsDevicesEvent extends Equatable {
const AnalyticsDevicesEvent();
@override
List<Object> get props => [];
}
final class LoadAnalyticsDevicesEvent extends AnalyticsDevicesEvent {
const LoadAnalyticsDevicesEvent({required this.param, required this.onSuccess});
final GetAnalyticsDevicesParam param;
final void Function(AnalyticsDevice device) onSuccess;
@override
List<Object> get props => [param];
}
final class SelectAnalyticsDeviceEvent extends AnalyticsDevicesEvent {
const SelectAnalyticsDeviceEvent(this.device);
final AnalyticsDevice device;
@override
List<Object> get props => [device];
}
final class ClearAnalyticsDeviceEvent extends AnalyticsDevicesEvent {
const ClearAnalyticsDeviceEvent();
}

View File

@ -0,0 +1,20 @@
part of 'analytics_devices_bloc.dart';
enum AnalyticsDevicesStatus { initial, loading, loaded, failure }
final class AnalyticsDevicesState extends Equatable {
const AnalyticsDevicesState({
this.status = AnalyticsDevicesStatus.initial,
this.devices = const [],
this.errorMessage,
this.selectedDevice,
});
final AnalyticsDevicesStatus status;
final List<AnalyticsDevice> devices;
final AnalyticsDevice? selectedDevice;
final String? errorMessage;
@override
List<Object?> get props => [status, devices, errorMessage, selectedDevice];
}

View File

@ -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({

View File

@ -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);
}
}

View File

@ -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(),
};
}
}

View File

@ -20,6 +20,13 @@ class EnergyManagementDataLoadingStrategy implements AnalyticsDataLoadingStrateg
spaces,
),
);
final spaceTreeState = context.read<SpaceTreeBloc>().state;
if (spaceTreeState.selectedCommunities.contains(community.uuid)) {
clearData(context);
return;
}
FetchEnergyManagementDataHelper.loadEnergyManagementData(
context,
communityId: community.uuid,
@ -41,6 +48,13 @@ class EnergyManagementDataLoadingStrategy implements AnalyticsDataLoadingStrateg
),
);
final spaceTreeState = context.read<SpaceTreeBloc>().state;
if (spaceTreeState.selectedCommunities.contains(community.uuid) ||
spaceTreeState.selectedSpaces.contains(space.uuid)) {
clearData(context);
return;
}
FetchEnergyManagementDataHelper.loadEnergyManagementData(
context,
communityId: community.uuid,
@ -54,7 +68,7 @@ class EnergyManagementDataLoadingStrategy implements AnalyticsDataLoadingStrateg
CommunityModel community,
SpaceModel child,
) {
// Do nothing
return onSpaceSelected(context, community, child);
}
@override

View File

@ -14,17 +14,7 @@ class OccupancyDataLoadingStrategy implements AnalyticsDataLoadingStrategy {
CommunityModel community,
List<SpaceModel> spaces,
) {
context.read<SpaceTreeBloc>().add(
OnCommunitySelected(
community.uuid,
spaces.isNotEmpty ? [spaces.first] : [],
),
);
FetchOccupancyDataHelper.loadOccupancyData(
context,
communityId: community.uuid,
spaceId: spaces.isNotEmpty ? spaces.first.uuid ?? '' : '',
);
// Do Nothing
}
@override
@ -34,19 +24,17 @@ class OccupancyDataLoadingStrategy implements AnalyticsDataLoadingStrategy {
SpaceModel space,
) {
final spaceTreeBloc = context.read<SpaceTreeBloc>();
final selectedSpacesIds = spaceTreeBloc.state.selectedSpaces;
final isSpaceSelected = selectedSpacesIds.contains(space.uuid);
final isSpaceSelected = spaceTreeBloc.state.selectedSpaces.contains(space.uuid);
if (selectedSpacesIds.isEmpty) {
spaceTreeBloc.add(OnCommunitySelected(community.uuid, [space]));
} else if (isSpaceSelected) {
spaceTreeBloc.add(const SpaceTreeClearSelectionEvent());
} else {
spaceTreeBloc
..add(const SpaceTreeClearSelectionEvent())
..add(OnSpaceSelected(community, space.uuid ?? '', []));
if (isSpaceSelected) {
clearData(context);
return;
}
spaceTreeBloc
..add(const SpaceTreeClearSelectionEvent())
..add(OnSpaceSelected(community, space.uuid ?? '', []));
FetchOccupancyDataHelper.loadOccupancyData(
context,
communityId: community.uuid,
@ -60,12 +48,12 @@ class OccupancyDataLoadingStrategy implements AnalyticsDataLoadingStrategy {
CommunityModel community,
SpaceModel child,
) {
// Do nothing
return onSpaceSelected(context, community, child);
}
@override
void clearData(BuildContext context) {
context.read<SpaceTreeBloc>().add(const SpaceTreeClearSelectionEvent());
// FetchOccupancyDataHelper.clearAllData(context);
FetchOccupancyDataHelper.clearAllData(context);
}
}

View File

@ -1,6 +1,8 @@
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';
import 'package:syncrow_web/pages/analytics/modules/analytics/widgets/analytics_communities_sidebar.dart';
import 'package:syncrow_web/pages/analytics/modules/analytics/widgets/analytics_page_tabs_and_children.dart';
@ -11,11 +13,15 @@ import 'package:syncrow_web/pages/analytics/modules/energy_management/blocs/real
import 'package:syncrow_web/pages/analytics/modules/energy_management/blocs/total_energy_consumption/total_energy_consumption_bloc.dart';
import 'package:syncrow_web/pages/analytics/modules/occupancy/blocs/occupancy/occupancy_bloc.dart';
import 'package:syncrow_web/pages/analytics/modules/occupancy/blocs/occupancy_heat_map/occupancy_heat_map_bloc.dart';
import 'package:syncrow_web/pages/analytics/services/energy_consumption_by_phases/fake_energy_consumption_by_phases_service.dart';
import 'package:syncrow_web/pages/analytics/services/energy_consumption_per_device/fake_energy_consumption_per_device_service.dart';
import 'package:syncrow_web/pages/analytics/services/occupacy/fake_occupacy_service.dart';
import 'package:syncrow_web/pages/analytics/services/occupancy_heat_map/fake_occupancy_heat_map_service.dart';
import 'package:syncrow_web/pages/analytics/services/analytics_devices/analytics_devices_service_delagate.dart';
import 'package:syncrow_web/pages/analytics/services/analytics_devices/remote_energy_management_analytics_devices_service.dart';
import 'package:syncrow_web/pages/analytics/services/analytics_devices/remote_occupancy_analytics_devices_service.dart';
import 'package:syncrow_web/pages/analytics/services/energy_consumption_by_phases/remote_energy_consumption_by_phases_service.dart';
import 'package:syncrow_web/pages/analytics/services/energy_consumption_per_device/remote_energy_consumption_per_device_service.dart';
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';
@ -23,9 +29,22 @@ import 'package:syncrow_web/services/api/http_service.dart';
import 'package:syncrow_web/utils/theme/responsive_text_theme.dart';
import 'package:syncrow_web/web_layout/web_scaffold.dart';
class AnalyticsPage extends StatelessWidget {
class AnalyticsPage extends StatefulWidget {
const AnalyticsPage({super.key});
@override
State<AnalyticsPage> createState() => _AnalyticsPageState();
}
class _AnalyticsPageState extends State<AnalyticsPage> {
late final HTTPService _httpService;
@override
void initState() {
super.initState();
_httpService = HTTPService();
}
@override
Widget build(BuildContext context) {
return MultiBlocProvider(
@ -35,22 +54,22 @@ class AnalyticsPage extends StatelessWidget {
),
BlocProvider(
create: (context) => TotalEnergyConsumptionBloc(
RemoteTotalEnergyConsumptionService(HTTPService()),
RemoteTotalEnergyConsumptionService(_httpService),
),
),
BlocProvider(
create: (context) => EnergyConsumptionByPhasesBloc(
FakeEnergyConsumptionByPhasesService(),
RemoteEnergyConsumptionByPhasesService(_httpService),
),
),
BlocProvider(
create: (context) => EnergyConsumptionPerDeviceBloc(
FakeEnergyConsumptionPerDeviceService(),
RemoteEnergyConsumptionPerDeviceService(_httpService),
),
),
BlocProvider(
create: (context) => PowerClampInfoBloc(
RemotePowerClampInfoService(HTTPService()),
RemotePowerClampInfoService(_httpService),
),
),
BlocProvider<RealtimeDeviceChangesBloc>(
@ -58,11 +77,30 @@ class AnalyticsPage extends StatelessWidget {
FirebaseRealtimeDeviceService(),
),
),
BlocProvider(create: (context) => OccupancyBloc(FakeOccupacyService())),
BlocProvider(
create: (context) => OccupancyHeatMapBloc(FakeOccupancyHeatMapService()),
create: (context) => OccupancyBloc(
RemoteOccupancyService(_httpService),
),
),
BlocProvider(
create: (context) => OccupancyHeatMapBloc(
RemoteOccupancyHeatMapService(_httpService),
),
),
BlocProvider(create: (context) => AnalyticsDatePickerBloc()),
BlocProvider(
create: (context) => AnalyticsDevicesBloc(
AnalyticsDevicesServiceDelegate(
RemoteOccupancyAnalyticsDevicesService(_httpService),
RemoteEnergyManagementAnalyticsDevicesService(_httpService),
),
),
),
BlocProvider(
create: (context) => RangeOfAqiBloc(
FakeRangeOfAqiService(),
),
),
],
child: const AnalyticsPageForm(),
);

View File

@ -14,7 +14,6 @@ class AnalyticsPageTabsAndChildren extends StatelessWidget {
@override
Widget build(BuildContext context) {
final spaceTreeState = context.read<SpaceTreeBloc>().state;
return BlocBuilder<AnalyticsTabBloc, AnalyticsPageTab>(
buildWhen: (previous, current) => previous != current,
builder: (context, selectedTab) => Column(
@ -68,15 +67,22 @@ class AnalyticsPageTabsAndChildren extends StatelessWidget {
context.read<AnalyticsDatePickerBloc>().add(
UpdateAnalyticsDatePickerEvent(montlyDate: value),
);
FetchEnergyManagementDataHelper.loadEnergyManagementData(
context,
selectedDate: value,
communityId:
spaceTreeState.selectedCommunities.firstOrNull ??
'',
spaceId:
spaceTreeState.selectedSpaces.firstOrNull ?? '',
);
final spaceTreeState =
context.read<SpaceTreeBloc>().state;
if (spaceTreeState.selectedSpaces.isNotEmpty) {
FetchEnergyManagementDataHelper
.loadEnergyManagementData(
context,
shouldFetchAnalyticsDevices: false,
selectedDate: value,
communityId:
spaceTreeState.selectedCommunities.firstOrNull ??
'',
spaceId:
spaceTreeState.selectedSpaces.firstOrNull ?? '',
);
}
},
selectedDate: context
.watch<AnalyticsDatePickerBloc>()

View File

@ -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,
),
],
),
),
);
}
}

View File

@ -45,7 +45,7 @@ class _MonthPickerWidgetState extends State<MonthPickerWidget> {
@override
Widget build(BuildContext context) {
return Dialog(
backgroundColor: Theme.of(context).colorScheme.surface,
backgroundColor: ColorsManager.whiteColors,
child: Container(
padding: const EdgeInsetsDirectional.all(20),
width: 320,
@ -121,6 +121,7 @@ class _MonthPickerWidgetState extends State<MonthPickerWidget> {
}
Row _buildYearSelector() {
final currentYear = DateTime.now().year;
return Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
@ -134,17 +135,35 @@ class _MonthPickerWidgetState extends State<MonthPickerWidget> {
),
const Spacer(),
IconButton(
onPressed: () => setState(() => _currentYear = _currentYear - 1),
onPressed: () {
setState(() {
_currentYear = _currentYear - 1;
});
},
icon: const Icon(
Icons.chevron_left,
color: ColorsManager.grey700,
),
),
IconButton(
onPressed: () => setState(() => _currentYear = _currentYear + 1),
icon: const Icon(
onPressed: _currentYear < currentYear
? () {
setState(() {
_currentYear = _currentYear + 1;
// Clear selected month if it becomes invalid in the new year
if (_currentYear == currentYear &&
_selectedMonth != null &&
_selectedMonth! > DateTime.now().month - 1) {
_selectedMonth = null;
}
});
}
: null,
icon: Icon(
Icons.chevron_right,
color: ColorsManager.grey700,
color: _currentYear < currentYear
? ColorsManager.grey700
: ColorsManager.grey700.withValues(alpha: 0.3),
),
),
],
@ -152,11 +171,13 @@ class _MonthPickerWidgetState extends State<MonthPickerWidget> {
}
Widget _buildMonthsGrid() {
final currentDate = DateTime.now();
final isCurrentYear = _currentYear == currentDate.year;
return GridView.builder(
shrinkWrap: true,
itemCount: 12,
physics: const NeverScrollableScrollPhysics(),
padding: const EdgeInsets.all(8),
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 3,
childAspectRatio: 2.5,
@ -165,25 +186,43 @@ class _MonthPickerWidgetState extends State<MonthPickerWidget> {
),
itemBuilder: (context, index) {
final isSelected = _selectedMonth == index;
final isFutureMonth = isCurrentYear && index > currentDate.month - 1;
return InkWell(
onTap: () => setState(() => _selectedMonth = index),
child: Container(
alignment: Alignment.center,
onTap: isFutureMonth ? null : () => setState(() => _selectedMonth = index),
child: DecoratedBox(
decoration: BoxDecoration(
color: isSelected
? ColorsManager.vividBlue.withValues(alpha: 0.7)
: const Color(0xFFEDF2F7),
borderRadius:
isSelected ? BorderRadius.circular(15) : BorderRadius.zero,
color: const Color(0xFFEDF2F7),
borderRadius: BorderRadius.only(
topLeft: index % 3 == 0 ? const Radius.circular(16) : Radius.zero,
bottomLeft: index % 3 == 0 ? const Radius.circular(16) : Radius.zero,
topRight: index % 3 == 2 ? const Radius.circular(16) : Radius.zero,
bottomRight:
index % 3 == 2 ? const Radius.circular(16) : Radius.zero,
),
),
child: Text(
_monthNames[index],
style: context.textTheme.titleSmall?.copyWith(
fontSize: 12,
child: Container(
alignment: Alignment.center,
decoration: BoxDecoration(
color: isSelected
? ColorsManager.whiteColors
: ColorsManager.blackColor.withValues(alpha: 0.8),
fontWeight: FontWeight.w500,
? ColorsManager.vividBlue.withValues(alpha: 0.7)
: isFutureMonth
? ColorsManager.grey700.withValues(alpha: 0.1)
: const Color(0xFFEDF2F7),
borderRadius:
isSelected ? BorderRadius.circular(15) : BorderRadius.zero,
),
child: Text(
_monthNames[index],
style: context.textTheme.titleSmall?.copyWith(
fontSize: 12,
color: isSelected
? ColorsManager.whiteColors
: isFutureMonth
? ColorsManager.blackColor.withValues(alpha: 0.3)
: ColorsManager.blackColor.withValues(alpha: 0.8),
fontWeight: FontWeight.w500,
),
),
),
),

View File

@ -20,9 +20,9 @@ class _YearPickerWidgetState extends State<YearPickerWidget> {
late int _currentYear;
static final years = List.generate(
DateTime.now().year - 2020 + 1,
DateTime.now().year - (DateTime.now().year - 5) + 1,
(index) => (2020 + index),
);
).where((year) => year <= DateTime.now().year).toList();
@override
void initState() {
@ -33,7 +33,7 @@ class _YearPickerWidgetState extends State<YearPickerWidget> {
@override
Widget build(BuildContext context) {
return Dialog(
backgroundColor: Theme.of(context).colorScheme.surface,
backgroundColor: ColorsManager.whiteColors,
child: Container(
padding: const EdgeInsetsDirectional.all(20),
width: 320,
@ -109,7 +109,6 @@ class _YearPickerWidgetState extends State<YearPickerWidget> {
shrinkWrap: true,
itemCount: years.length,
physics: const NeverScrollableScrollPhysics(),
padding: const EdgeInsets.all(8),
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 3,
childAspectRatio: 2.5,
@ -120,23 +119,35 @@ class _YearPickerWidgetState extends State<YearPickerWidget> {
final isSelected = _currentYear == years[index];
return InkWell(
onTap: () => setState(() => _currentYear = years[index]),
child: Container(
alignment: Alignment.center,
child: DecoratedBox(
decoration: BoxDecoration(
color: isSelected
? ColorsManager.vividBlue.withValues(alpha: 0.7)
: const Color(0xFFEDF2F7),
borderRadius:
isSelected ? BorderRadius.circular(15) : BorderRadius.zero,
color: const Color(0xFFEDF2F7),
borderRadius: BorderRadius.only(
topLeft: index % 3 == 0 ? const Radius.circular(16) : Radius.zero,
bottomLeft: index % 3 == 0 ? const Radius.circular(16) : Radius.zero,
topRight: index % 3 == 2 ? const Radius.circular(16) : Radius.zero,
bottomRight:
index % 3 == 2 ? const Radius.circular(16) : Radius.zero,
),
),
child: Text(
years[index].toString(),
style: context.textTheme.titleSmall?.copyWith(
fontSize: 12,
child: Container(
alignment: Alignment.center,
decoration: BoxDecoration(
color: isSelected
? ColorsManager.whiteColors
: ColorsManager.blackColor.withValues(alpha: 0.8),
fontWeight: FontWeight.w500,
? ColorsManager.vividBlue.withValues(alpha: 0.7)
: const Color(0xFFEDF2F7),
borderRadius:
isSelected ? BorderRadius.circular(15) : BorderRadius.zero,
),
child: Text(
years[index].toString(),
style: context.textTheme.titleSmall?.copyWith(
fontSize: 12,
color: isSelected
? ColorsManager.whiteColors
: ColorsManager.blackColor.withValues(alpha: 0.8),
fontWeight: FontWeight.w500,
),
),
),
),

View File

@ -1,20 +0,0 @@
import 'package:syncrow_web/pages/analytics/models/phases_energy_consumption.dart';
abstract final class EnergyConsumptionByPhasesChartHelper {
const EnergyConsumptionByPhasesChartHelper._();
static const fakeData = <PhasesEnergyConsumption>[
PhasesEnergyConsumption(month: 1, phaseA: 200, phaseB: 300, phaseC: 400),
PhasesEnergyConsumption(month: 2, phaseA: 300, phaseB: 400, phaseC: 500),
PhasesEnergyConsumption(month: 3, phaseA: 400, phaseB: 500, phaseC: 600),
PhasesEnergyConsumption(month: 4, phaseA: 100, phaseB: 100, phaseC: 100),
PhasesEnergyConsumption(month: 5, phaseA: 300, phaseB: 400, phaseC: 500),
PhasesEnergyConsumption(month: 6, phaseA: 300, phaseB: 100, phaseC: 400),
PhasesEnergyConsumption(month: 7, phaseA: 300, phaseB: 100, phaseC: 400),
PhasesEnergyConsumption(month: 8, phaseA: 500, phaseB: 100, phaseC: 100),
PhasesEnergyConsumption(month: 9, phaseA: 500, phaseB: 100, phaseC: 200),
PhasesEnergyConsumption(month: 10, phaseA: 100, phaseB: 50, phaseC: 50),
PhasesEnergyConsumption(month: 11, phaseA: 600, phaseB: 750, phaseC: 130),
PhasesEnergyConsumption(month: 12, phaseA: 100, phaseB: 80, phaseC: 100),
];
}

View File

@ -21,12 +21,13 @@ abstract final class EnergyManagementChartsHelper {
reservedSize: 32,
showTitles: true,
maxIncluded: true,
minIncluded: true,
getTitlesWidget: (value, meta) => Padding(
padding: const EdgeInsetsDirectional.only(top: 20.0),
child: Text(
(value + 1).toString(),
value.toString(),
style: context.textTheme.bodySmall?.copyWith(
color: ColorsManager.greyColor,
color: ColorsManager.lightGreyColor,
fontSize: 12,
),
),
@ -36,7 +37,8 @@ abstract final class EnergyManagementChartsHelper {
leftTitles: AxisTitles(
sideTitles: SideTitles(
showTitles: true,
maxIncluded: true,
maxIncluded: false,
minIncluded: false,
interval: leftTitlesInterval,
reservedSize: 110,
getTitlesWidget: (value, meta) => Padding(
@ -48,7 +50,7 @@ abstract final class EnergyManagementChartsHelper {
value.formatNumberToKwh,
style: context.textTheme.bodySmall?.copyWith(
fontSize: 12,
color: ColorsManager.greyColor,
color: ColorsManager.lightGreyColor,
),
),
),
@ -70,7 +72,7 @@ abstract final class EnergyManagementChartsHelper {
static List<LineTooltipItem?> getTooltipItems(List<LineBarSpot> touchedSpots) {
return touchedSpots.map((spot) {
return LineTooltipItem(
getToolTipLabel(spot.x + 1, spot.y),
getToolTipLabel(spot.x, spot.y),
const TextStyle(
color: ColorsManager.textPrimaryColor,
fontWeight: FontWeight.w600,
@ -91,31 +93,40 @@ abstract final class EnergyManagementChartsHelper {
);
}
static FlBorderData borderData() {
return FlBorderData(
show: true,
border: const Border.symmetric(
horizontal: BorderSide(
color: ColorsManager.greyColor,
style: BorderStyle.solid,
width: 1,
),
),
);
}
static FlGridData gridData() {
return const FlGridData(
static FlGridData gridData({
double horizontalInterval = 250,
}) {
return FlGridData(
show: true,
drawVerticalLine: false,
drawHorizontalLine: true,
horizontalInterval: horizontalInterval,
getDrawingHorizontalLine: (value) {
return FlLine(
color: ColorsManager.greyColor,
strokeWidth: 1,
dashArray: value == 0 ? null : [5, 5],
);
},
);
}
static FlBorderData borderData() {
return FlBorderData(
border: const Border(
bottom: BorderSide(
color: ColorsManager.greyColor,
style: BorderStyle.solid,
),
),
show: true,
);
}
static LineTouchData lineTouchData() {
return LineTouchData(
handleBuiltInTouches: true,
touchSpotThreshold: 2,
touchSpotThreshold: 16,
touchTooltipData: EnergyManagementChartsHelper.lineTouchTooltipData(),
);
}

View File

@ -1,11 +1,14 @@
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:syncrow_web/pages/analytics/models/analytics_device.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/energy_consumption_by_phases/energy_consumption_by_phases_bloc.dart';
import 'package:syncrow_web/pages/analytics/modules/energy_management/blocs/energy_consumption_per_device/energy_consumption_per_device_bloc.dart';
import 'package:syncrow_web/pages/analytics/modules/energy_management/blocs/power_clamp_info/power_clamp_info_bloc.dart';
import 'package:syncrow_web/pages/analytics/modules/energy_management/blocs/realtime_device_changes/realtime_device_changes_bloc.dart';
import 'package:syncrow_web/pages/analytics/modules/energy_management/blocs/total_energy_consumption/total_energy_consumption_bloc.dart';
import 'package:syncrow_web/pages/analytics/params/get_analytics_devices_param.dart';
import 'package:syncrow_web/pages/analytics/params/get_energy_consumption_by_phases_param.dart';
import 'package:syncrow_web/pages/analytics/params/get_energy_consumption_per_device_param.dart';
import 'package:syncrow_web/pages/analytics/params/get_total_energy_consumption_param.dart';
@ -13,13 +16,17 @@ import 'package:syncrow_web/pages/analytics/params/get_total_energy_consumption_
abstract final class FetchEnergyManagementDataHelper {
const FetchEnergyManagementDataHelper._();
static const String _powerClampId = 'cb71d6ad-6e29-4eaa-ae3e-1a0d1c5f60fa';
// static const String _powerClampId = 'cb71d6ad-6e29-4eaa-ae3e-1a0d1c5f60fa';
static AnalyticsDevice? getSelectedDevice(BuildContext context) {
return context.read<AnalyticsDevicesBloc>().state.selectedDevice;
}
static void loadEnergyManagementData(
BuildContext context, {
required String communityId,
required String spaceId,
DateTime? selectedDate,
bool shouldFetchAnalyticsDevices = true,
}) {
if (communityId.isEmpty && spaceId.isEmpty) {
clearAllData(context);
@ -28,28 +35,46 @@ abstract final class FetchEnergyManagementDataHelper {
final datePickerState = context.read<AnalyticsDatePickerBloc>().state;
final selectedDate0 = selectedDate ?? datePickerState.monthlyDate;
if (shouldFetchAnalyticsDevices) {
loadAnalyticsDevices(
context,
communityUuid: communityId,
spaceUuid: spaceId,
selectedDate: selectedDate0,
);
loadRealtimeDeviceChanges(context);
loadPowerClampInfo(context);
}
loadTotalEnergyConsumption(
context,
selectedDate: selectedDate0,
communityId: communityId,
spaceId: spaceId,
);
loadEnergyConsumptionByPhases(context, selectedDate: selectedDate);
loadEnergyConsumptionPerDevice(context);
loadRealtimeDeviceChanges(context);
loadPowerClampInfo(context);
final selectedDevice = getSelectedDevice(context);
if (selectedDevice case final AnalyticsDevice device) {
loadEnergyConsumptionByPhases(
context,
powerClampUuid: device.uuid,
selectedDate: selectedDate0,
);
}
loadEnergyConsumptionPerDevice(
context,
communityId: communityId,
spaceId: spaceId,
selectedDate: selectedDate0,
);
}
static void loadEnergyConsumptionByPhases(
BuildContext context, {
required String powerClampUuid,
DateTime? selectedDate,
}) {
final param = GetEnergyConsumptionByPhasesParam(
startDate: selectedDate,
spaceId: '',
date: selectedDate,
powerClampUuid: powerClampUuid,
);
context.read<EnergyConsumptionByPhasesBloc>().add(
LoadEnergyConsumptionByPhasesEvent(param: param),
@ -72,33 +97,80 @@ abstract final class FetchEnergyManagementDataHelper {
);
}
static void loadEnergyConsumptionPerDevice(BuildContext context) {
const param = GetEnergyConsumptionPerDeviceParam();
static void loadEnergyConsumptionPerDevice(
BuildContext context, {
DateTime? selectedDate,
required String communityId,
required String spaceId,
}) {
final param = GetEnergyConsumptionPerDeviceParam(
spaceId: spaceId,
communityId: communityId,
monthDate: selectedDate,
);
context.read<EnergyConsumptionPerDeviceBloc>().add(
const LoadEnergyConsumptionPerDeviceEvent(param),
LoadEnergyConsumptionPerDeviceEvent(param),
);
}
static void loadPowerClampInfo(BuildContext context) {
context.read<PowerClampInfoBloc>().add(
const LoadPowerClampInfoEvent(_powerClampId),
final selectedDevice = getSelectedDevice(context);
if (selectedDevice case final AnalyticsDevice device) {
context.read<PowerClampInfoBloc>().add(
LoadPowerClampInfoEvent(device.uuid),
);
}
}
static void loadRealtimeDeviceChanges(
BuildContext context, {
String? deviceUuid,
}) {
final selectedDevice = getSelectedDevice(context);
context.read<RealtimeDeviceChangesBloc>().add(
RealtimeDeviceChangesStarted(deviceUuid ?? selectedDevice?.uuid ?? ''),
);
}
static void loadRealtimeDeviceChanges(BuildContext context) {
context.read<RealtimeDeviceChangesBloc>().add(
const RealtimeDeviceChangesStarted(_powerClampId),
static void loadAnalyticsDevices(
BuildContext context, {
required String communityUuid,
required String spaceUuid,
required DateTime selectedDate,
}) {
context.read<AnalyticsDevicesBloc>().add(
LoadAnalyticsDevicesEvent(
onSuccess: (device) {
context.read<PowerClampInfoBloc>().add(
LoadPowerClampInfoEvent(device.uuid),
);
loadEnergyConsumptionByPhases(
context,
powerClampUuid: device.uuid,
selectedDate: selectedDate,
);
context.read<RealtimeDeviceChangesBloc>().add(
RealtimeDeviceChangesStarted(device.uuid),
);
},
param: GetAnalyticsDevicesParam(
communityUuid: communityUuid,
spaceUuid: spaceUuid,
deviceTypes: ['PC'],
requestType: AnalyticsDeviceRequestType.energyManagement,
),
),
);
}
static void clearAllData(BuildContext context) {
context.read<RealtimeDeviceChangesBloc>().add(
const RealtimeDeviceChangesClosed(),
);
context.read<PowerClampInfoBloc>().add(
const ClearPowerClampInfoEvent(),
);
context.read<RealtimeDeviceChangesBloc>().add(
const RealtimeDeviceChangesClosed(),
);
context.read<EnergyConsumptionPerDeviceBloc>().add(
const ClearEnergyConsumptionPerDeviceEvent(),
@ -111,5 +183,6 @@ abstract final class FetchEnergyManagementDataHelper {
context.read<EnergyConsumptionByPhasesBloc>().add(
const ClearEnergyConsumptionByPhasesEvent(),
);
context.read<AnalyticsDevicesBloc>().add(const ClearAnalyticsDeviceEvent());
}
}

View File

@ -0,0 +1,108 @@
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:syncrow_web/pages/analytics/models/analytics_device.dart';
import 'package:syncrow_web/pages/analytics/modules/analytics/blocs/analytics_devices/analytics_devices_bloc.dart';
import 'package:syncrow_web/utils/color_manager.dart';
import 'package:syncrow_web/utils/extension/build_context_x.dart';
class AnalyticsDeviceDropdown extends StatelessWidget {
const AnalyticsDeviceDropdown({required this.onChanged, super.key});
final ValueChanged<AnalyticsDevice> onChanged;
@override
Widget build(BuildContext context) {
return BlocBuilder<AnalyticsDevicesBloc, AnalyticsDevicesState>(
builder: (context, state) {
return Container(
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(16),
border: Border.all(
color: ColorsManager.greyColor,
width: 1,
),
),
child: Visibility(
visible: state.devices.isNotEmpty,
replacement: _buildNoDevicesFound(context),
child: _buildDevicesDropdown(context, state),
),
);
},
);
}
static const _defaultPadding = EdgeInsetsDirectional.symmetric(
horizontal: 20,
vertical: 2,
);
Widget _buildNoDevicesFound(BuildContext context) {
return Padding(
padding: _defaultPadding,
child: Text(
'no devices found',
style: _getTextStyle(context),
),
);
}
Widget _buildDevicesDropdown(BuildContext context, AnalyticsDevicesState state) {
final spaceUuid = state.selectedDevice?.spaceUuid;
return DropdownButton<AnalyticsDevice?>(
value: state.selectedDevice,
isDense: true,
borderRadius: BorderRadius.circular(16),
dropdownColor: ColorsManager.whiteColors,
underline: const SizedBox.shrink(),
icon: const RotatedBox(
quarterTurns: 1,
child: Icon(Icons.chevron_right, size: 16),
),
style: _getTextStyle(context),
padding: _defaultPadding,
selectedItemBuilder: (context) {
return state.devices.map((e) => Text(e.name)).toList();
},
items: state.devices.map((e) {
return DropdownMenuItem(
value: e,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: [
Text(e.name),
if (spaceUuid != null)
FittedBox(
fit: BoxFit.scaleDown,
alignment: AlignmentDirectional.centerStart,
child: Text(
spaceUuid,
style: _getTextStyle(context)?.copyWith(
fontSize: 10,
),
),
),
],
),
);
}).toList(),
onChanged: (value) {
if (value case final AnalyticsDevice device) {
context.read<AnalyticsDevicesBloc>().add(
SelectAnalyticsDeviceEvent(device),
);
onChanged.call(device);
}
},
);
}
TextStyle? _getTextStyle(BuildContext context) {
return context.textTheme.labelSmall?.copyWith(
color: ColorsManager.textPrimaryColor,
fontWeight: FontWeight.w700,
fontSize: 14,
);
}
}

View File

@ -1,6 +1,6 @@
import 'package:fl_chart/fl_chart.dart';
import 'package:flutter/material.dart';
import 'package:syncrow_web/pages/analytics/helpers/get_month_name_from_int.dart';
import 'package:intl/intl.dart';
import 'package:syncrow_web/pages/analytics/models/phases_energy_consumption.dart';
import 'package:syncrow_web/pages/analytics/modules/energy_management/helpers/energy_management_charts_helper.dart';
import 'package:syncrow_web/utils/color_manager.dart';
@ -18,7 +18,11 @@ class EnergyConsumptionByPhasesChart extends StatelessWidget {
Widget build(BuildContext context) {
return BarChart(
BarChartData(
gridData: EnergyManagementChartsHelper.gridData(),
gridData: EnergyManagementChartsHelper.gridData().copyWith(
checkToShowHorizontalLine: (value) => true,
horizontalInterval: 250,
),
borderData: EnergyManagementChartsHelper.borderData(),
barTouchData: _barTouchData(context),
titlesData: _titlesData(context),
@ -31,25 +35,29 @@ class EnergyConsumptionByPhasesChart extends StatelessWidget {
barRods: [
BarChartRodData(
color: ColorsManager.vividBlue.withValues(alpha: 0.1),
toY: data.phaseA + data.phaseB + data.phaseC,
toY: data.energyConsumedA +
data.energyConsumedB +
data.energyConsumedC,
rodStackItems: [
BarChartRodStackItem(
0,
data.phaseA,
data.energyConsumedA,
ColorsManager.vividBlue.withValues(alpha: 0.8),
),
BarChartRodStackItem(
data.phaseA,
data.phaseA + data.phaseB,
data.energyConsumedA,
data.energyConsumedA + data.energyConsumedB,
ColorsManager.vividBlue.withValues(alpha: 0.4),
),
BarChartRodStackItem(
data.phaseA + data.phaseB,
data.phaseA + data.phaseB + data.phaseC,
data.energyConsumedA + data.energyConsumedB,
data.energyConsumedA +
data.energyConsumedB +
data.energyConsumedC,
ColorsManager.vividBlue.withValues(alpha: 0.15),
),
],
width: 16,
width: 8,
borderRadius: const BorderRadius.only(
topLeft: Radius.circular(8),
topRight: Radius.circular(8),
@ -59,6 +67,7 @@ class EnergyConsumptionByPhasesChart extends StatelessWidget {
);
}).toList(),
),
duration: Duration.zero,
);
}
@ -91,18 +100,27 @@ class EnergyConsumptionByPhasesChart extends StatelessWidget {
}) {
final data = energyData;
final month = data[group.x.toInt()].month.getMonthName;
final phaseA = data[group.x.toInt()].phaseA;
final phaseB = data[group.x.toInt()].phaseB;
final phaseC = data[group.x.toInt()].phaseC;
final date = DateFormat('dd/MM/yyyy').format(data[group.x.toInt()].date);
final phaseA = data[group.x.toInt()].energyConsumedA;
final phaseB = data[group.x.toInt()].energyConsumedB;
final phaseC = data[group.x.toInt()].energyConsumedC;
final total = data[group.x.toInt()].energyConsumedKw;
return BarTooltipItem(
'$month\n',
'$date\n',
context.textTheme.bodyMedium!.copyWith(
color: ColorsManager.blackColor,
fontSize: 14,
),
textAlign: TextAlign.start,
children: [
TextSpan(
text: 'Total: $total\n',
style: context.textTheme.bodySmall?.copyWith(
color: ColorsManager.blackColor,
fontSize: 12,
),
),
TextSpan(
text: 'Phase A: $phaseA\n',
style: context.textTheme.bodySmall?.copyWith(
@ -144,23 +162,23 @@ class EnergyConsumptionByPhasesChart extends StatelessWidget {
sideTitles: SideTitles(
showTitles: true,
getTitlesWidget: (value, _) {
final month = energyData[value.toInt()].month.getMonthName;
final month = DateFormat('d').format(energyData[value.toInt()].date);
return FittedBox(
alignment: AlignmentDirectional.bottomCenter,
alignment: AlignmentDirectional.center,
fit: BoxFit.scaleDown,
child: RotatedBox(
quarterTurns: 3,
child: Text(
month,
style: context.textTheme.bodySmall?.copyWith(
color: ColorsManager.greyColor,
color: ColorsManager.lightGreyColor,
fontSize: 11,
),
),
),
);
},
reservedSize: 36,
reservedSize: 18,
),
);

View File

@ -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,

View File

@ -12,11 +12,16 @@ class EnergyConsumptionPerDeviceChart extends StatelessWidget {
Widget build(BuildContext context) {
return LineChart(
LineChartData(
clipData: const FlClipData.vertical(),
titlesData: EnergyManagementChartsHelper.titlesData(
context,
leftTitlesInterval: 250,
),
gridData: EnergyManagementChartsHelper.gridData(),
gridData: EnergyManagementChartsHelper.gridData().copyWith(
checkToShowHorizontalLine: (value) => true,
horizontalInterval: 250,
),
borderData: EnergyManagementChartsHelper.borderData(),
lineTouchData: EnergyManagementChartsHelper.lineTouchData(),
lineBarsData: chartData.map((e) {
@ -33,7 +38,7 @@ class EnergyConsumptionPerDeviceChart extends StatelessWidget {
);
}).toList(),
),
duration: Durations.extralong1,
duration: Duration.zero,
curve: Curves.easeIn,
);
}

View File

@ -1,5 +1,6 @@
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/blocs/energy_consumption_per_device/energy_consumption_per_device_bloc.dart';
import 'package:syncrow_web/pages/analytics/modules/energy_management/widgets/chart_title.dart';
import 'package:syncrow_web/pages/analytics/modules/energy_management/widgets/energy_consumption_per_device_chart.dart';
@ -46,6 +47,7 @@ class EnergyConsumptionPerDeviceChartBox extends StatelessWidget {
flex: 2,
child: EnergyConsumptionPerDeviceDevicesList(
chartData: state.chartData,
devices: context.watch<AnalyticsDevicesBloc>().state.devices,
),
),
],

View File

@ -1,10 +1,16 @@
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({required this.chartData, super.key});
const EnergyConsumptionPerDeviceDevicesList({
required this.chartData,
required this.devices,
super.key,
});
final List<AnalyticsDevice> devices;
final List<DeviceEnergyDataModel> chartData;
@override
@ -16,47 +22,27 @@ class EnergyConsumptionPerDeviceDevicesList extends StatelessWidget {
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.end,
mainAxisAlignment: MainAxisAlignment.end,
children: chartData.map((e) => _buildDeviceCell(context, e)).toList(),
children: devices.map((e) => _buildDeviceCell(context, e)).toList(),
),
);
}
Widget _buildDeviceCell(BuildContext context, DeviceEnergyDataModel device) {
return 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: device.color,
),
Text(
device.deviceName,
textAlign: TextAlign.center,
style: const TextStyle(
color: ColorsManager.blackColor,
fontWeight: FontWeight.w400,
fontSize: 14,
),
),
],
),
),
Widget _buildDeviceCell(BuildContext context, AnalyticsDevice device) {
final deviceColor = chartData
.firstWhere(
(element) => element.deviceId == device.uuid,
orElse: () => const DeviceEnergyDataModel(
energy: [],
deviceName: '',
deviceId: '',
color: Colors.red,
),
)
.color;
return Tooltip(
message: '${device.name}\n${device.productDevice?.uuid ?? ''}',
child: ChartInformativeCell(title: Text(device.name), color: deviceColor),
);
}
}

View File

@ -1,55 +0,0 @@
import 'package:flutter/material.dart';
import 'package:syncrow_web/utils/color_manager.dart';
import 'package:syncrow_web/utils/extension/build_context_x.dart';
class PowerClampEnergyDataDeviceDropdown extends StatelessWidget {
const PowerClampEnergyDataDeviceDropdown({super.key});
@override
Widget build(BuildContext context) {
return Container(
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(16),
border: Border.all(
color: ColorsManager.greyColor,
width: 1,
),
),
child: DropdownButton<String>(
value: 'Device 1',
isDense: true,
borderRadius: BorderRadius.circular(16),
dropdownColor: ColorsManager.whiteColors,
underline: const SizedBox.shrink(),
icon: const RotatedBox(
quarterTurns: 1,
child: Icon(Icons.chevron_right, size: 16),
),
style: context.textTheme.labelSmall?.copyWith(
color: ColorsManager.textPrimaryColor,
fontWeight: FontWeight.w700,
fontSize: 14,
),
padding: const EdgeInsetsDirectional.symmetric(
horizontal: 20,
vertical: 2,
),
items: [
for (var i = 1; i < 10; i++)
DropdownMenuItem(
value: 'Device $i',
child: Text(
'Device $i',
style: context.textTheme.labelSmall?.copyWith(
color: ColorsManager.textPrimaryColor,
fontWeight: FontWeight.w700,
fontSize: 14,
),
),
),
],
onChanged: (value) {},
),
);
}
}

View File

@ -1,10 +1,13 @@
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:syncrow_web/pages/analytics/models/power_clamp_energy_status.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/power_clamp_info/power_clamp_info_bloc.dart';
import 'package:syncrow_web/pages/analytics/modules/energy_management/blocs/realtime_device_changes/realtime_device_changes_bloc.dart';
import 'package:syncrow_web/pages/analytics/modules/energy_management/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/pages/analytics/modules/energy_management/widgets/energy_consumption_by_phases_chart_box.dart';
import 'package:syncrow_web/pages/analytics/modules/energy_management/widgets/power_clamp_energy_data_device_dropdown.dart';
import 'package:syncrow_web/pages/analytics/modules/energy_management/widgets/power_clamp_energy_status_widget.dart';
import 'package:syncrow_web/pages/analytics/modules/energy_management/widgets/power_clamp_phases_data_widget.dart';
import 'package:syncrow_web/pages/analytics/widgets/analytics_error_widget.dart';
@ -50,7 +53,8 @@ class PowerClampEnergyDataWidget extends StatelessWidget {
),
const SizedBox(height: 6),
SelectableText(
state.powerClampModel?.productUuid ?? 'N/A',
context.watch<AnalyticsDevicesBloc>().state.selectedDevice?.uuid ??
'N/A',
style: context.textTheme.bodySmall?.copyWith(
color: ColorsManager.blackColor,
fontWeight: FontWeight.w400,
@ -65,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(
@ -107,7 +116,7 @@ class PowerClampEnergyDataWidget extends StatelessWidget {
mainAxisSize: MainAxisSize.min,
children: [
Expanded(
flex: 2,
flex: 3,
child: FittedBox(
fit: BoxFit.scaleDown,
alignment: AlignmentDirectional.centerStart,
@ -122,11 +131,25 @@ class PowerClampEnergyDataWidget extends StatelessWidget {
),
),
const Spacer(),
const Expanded(
Expanded(
flex: 2,
child: FittedBox(
fit: BoxFit.scaleDown,
alignment: AlignmentDirectional.centerEnd,
child: PowerClampEnergyDataDeviceDropdown(),
child: AnalyticsDeviceDropdown(
onChanged: (value) {
FetchEnergyManagementDataHelper.loadEnergyConsumptionByPhases(
context,
powerClampUuid: value.uuid,
selectedDate:
context.read<AnalyticsDatePickerBloc>().state.monthlyDate,
);
FetchEnergyManagementDataHelper.loadRealtimeDeviceChanges(
context,
deviceUuid: value.uuid,
);
},
),
),
),
],

View File

@ -48,6 +48,9 @@ class PowerClampEnergyStatusWidget extends StatelessWidget {
fontWeight: FontWeight.w400,
fontSize: 16,
),
softWrap: true,
maxLines: 2,
overflow: TextOverflow.ellipsis,
),
trailing: Text.rich(
TextSpan(

View File

@ -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)}';
}
}

View File

@ -4,15 +4,6 @@ import 'package:syncrow_web/pages/analytics/models/energy_data_model.dart';
import 'package:syncrow_web/pages/analytics/modules/energy_management/helpers/energy_management_charts_helper.dart';
import 'package:syncrow_web/utils/color_manager.dart';
// energy_consumption_chart will return id, name and consumption
const phasesJson = {
"1": {
"phaseOne": 1000,
"phaseTwo": 2000,
"phaseThree": 3000,
}
};
class TotalEnergyConsumptionChart extends StatelessWidget {
const TotalEnergyConsumptionChart({required this.chartData, super.key});
@ -23,14 +14,22 @@ class TotalEnergyConsumptionChart extends StatelessWidget {
return Expanded(
child: LineChart(
LineChartData(
titlesData: EnergyManagementChartsHelper.titlesData(context),
gridData: EnergyManagementChartsHelper.gridData(),
clipData: const FlClipData.vertical(),
titlesData: EnergyManagementChartsHelper.titlesData(
context,
leftTitlesInterval: 250,
),
gridData: EnergyManagementChartsHelper.gridData().copyWith(
checkToShowHorizontalLine: (value) => true,
horizontalInterval: 250,
),
borderData: EnergyManagementChartsHelper.borderData(),
lineTouchData: EnergyManagementChartsHelper.lineTouchData(),
lineBarsData: _lineBarsData,
),
duration: Durations.extralong1,
duration: Duration.zero,
curve: Curves.easeIn,
),
);
}
@ -38,15 +37,12 @@ class TotalEnergyConsumptionChart extends StatelessWidget {
List<LineChartBarData> get _lineBarsData {
return [
LineChartBarData(
preventCurveOvershootingThreshold: 0.1,
curveSmoothness: 0.55,
preventCurveOverShooting: true,
spots: chartData
.asMap()
.entries
.map(
(entry) => FlSpot(
entry.key.toDouble(),
entry.value.date.day.toDouble(),
entry.value.value,
),
)

View File

@ -1,9 +1,12 @@
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:syncrow_web/pages/analytics/models/analytics_device.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/modules/occupancy/blocs/occupancy/occupancy_bloc.dart';
import 'package:syncrow_web/pages/analytics/modules/occupancy/blocs/occupancy_heat_map/occupancy_heat_map_bloc.dart';
import 'package:syncrow_web/pages/analytics/params/get_analytics_devices_param.dart';
import 'package:syncrow_web/pages/analytics/params/get_occupancy_heat_map_param.dart';
import 'package:syncrow_web/pages/analytics/params/get_occupancy_param.dart';
@ -17,36 +20,77 @@ abstract final class FetchOccupancyDataHelper {
}) {
if (communityId.isEmpty && spaceId.isEmpty) {
clearAllData(context);
return;
}
final datePickerState = context.read<AnalyticsDatePickerBloc>().state;
loadAnalyticsDevices(context, communityUuid: communityId, spaceUuid: spaceId);
final selectedDevice = context.read<AnalyticsDevicesBloc>().state.selectedDevice;
loadOccupancyChartData(
context,
spaceUuid: spaceId,
date: datePickerState.monthlyDate,
);
loadHeatMapData(context, spaceUuid: spaceId, year: datePickerState.yearlyDate);
if (selectedDevice case final AnalyticsDevice device) {
context.read<RealtimeDeviceChangesBloc>()
..add(const RealtimeDeviceChangesClosed())
..add(
RealtimeDeviceChangesStarted(device.uuid),
);
}
}
static void loadHeatMapData(
BuildContext context, {
required String spaceUuid,
required DateTime year,
}) {
context.read<OccupancyHeatMapBloc>().add(
LoadOccupancyHeatMapEvent(
GetOccupancyHeatMapParam(spaceUuid: spaceUuid, year: year),
),
);
}
static void loadOccupancyChartData(
BuildContext context, {
required String spaceUuid,
required DateTime date,
}) {
context.read<OccupancyBloc>().add(
LoadOccupancyEvent(
GetOccupancyParam(
monthDate:
'${datePickerState.monthlyDate.year}-${datePickerState.monthlyDate.month}',
spaceUuid: spaceId,
communityUuid: communityId,
monthDate: '${date.year}-${date.month.toString().padLeft(2, '0')}',
spaceUuid: spaceUuid,
),
),
);
}
context.read<OccupancyHeatMapBloc>().add(
LoadOccupancyHeatMapEvent(
GetOccupancyHeatMapParam(
spaceId: spaceId,
communityId: communityId,
year: datePickerState.yearlyDate,
static void loadAnalyticsDevices(
BuildContext context, {
required String communityUuid,
required String spaceUuid,
}) {
context.read<AnalyticsDevicesBloc>().add(
LoadAnalyticsDevicesEvent(
param: GetAnalyticsDevicesParam(
communityUuid: communityUuid,
spaceUuid: spaceUuid,
deviceTypes: ['WPS', 'CPS'],
requestType: AnalyticsDeviceRequestType.occupancy,
),
onSuccess: (device) {
context.read<RealtimeDeviceChangesBloc>()
..add(const RealtimeDeviceChangesClosed())
..add(RealtimeDeviceChangesStarted(device.uuid));
},
),
);
context.read<RealtimeDeviceChangesBloc>()
..add(const RealtimeDeviceChangesClosed())
..add(
const RealtimeDeviceChangesStarted('14fe6e7e-47af-4a07-ae0a-7c4a26ef8135'),
);
}
static void clearAllData(BuildContext context) {
@ -59,5 +103,9 @@ abstract final class FetchOccupancyDataHelper {
context.read<RealtimeDeviceChangesBloc>().add(
const RealtimeDeviceChangesClosed(),
);
context.read<AnalyticsDevicesBloc>().add(
const ClearAnalyticsDeviceEvent(),
);
}
}

View File

@ -20,9 +20,9 @@ class AnalyticsOccupancyView extends StatelessWidget {
child: Column(
spacing: 32,
children: [
SizedBox(height: height * 0.45, child: const OccupancyEndSideBar()),
SizedBox(height: height * 0.46, child: const OccupancyEndSideBar()),
SizedBox(height: height * 0.5, child: const OccupancyChartBox()),
SizedBox(height: height * 0.5, child: const Placeholder()),
SizedBox(height: height * 0.5, child: const OccupancyHeatMapBox()),
],
),
);

View File

@ -0,0 +1,54 @@
import 'package:flutter/material.dart';
import 'package:intl/intl.dart';
import 'package:syncrow_web/utils/color_manager.dart';
import 'package:syncrow_web/utils/extension/build_context_x.dart';
class HeatMapTooltip extends StatelessWidget {
const HeatMapTooltip({
required this.date,
required this.value,
super.key,
});
final DateTime date;
final int value;
@override
Widget build(BuildContext context) {
return FittedBox(
fit: BoxFit.scaleDown,
alignment: AlignmentDirectional.topStart,
child: Container(
padding: const EdgeInsets.all(8),
decoration: BoxDecoration(
color: ColorsManager.grey700,
borderRadius: BorderRadius.circular(3),
),
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisAlignment: MainAxisAlignment.start,
children: [
Text(
DateFormat('MMM d, yyyy').format(date),
style: context.textTheme.bodySmall?.copyWith(
fontSize: 12,
fontWeight: FontWeight.w700,
color: ColorsManager.whiteColors,
),
),
const Divider(height: 2, thickness: 1),
Text(
'$value Occupants',
style: context.textTheme.bodySmall?.copyWith(
fontSize: 10,
fontWeight: FontWeight.w500,
color: ColorsManager.whiteColors,
),
),
],
),
),
);
}
}

View File

@ -0,0 +1,108 @@
import 'package:flutter/material.dart';
import 'package:syncrow_web/pages/analytics/modules/occupancy/widgets/heat_map_tooltip.dart';
import 'package:syncrow_web/pages/analytics/modules/occupancy/widgets/occupancy_painter.dart';
class InteractiveHeatMap extends StatefulWidget {
const InteractiveHeatMap({
required this.items,
required this.maxValue,
required this.cellSize,
super.key,
});
final List<OccupancyPaintItem> items;
final int maxValue;
final double cellSize;
@override
State<InteractiveHeatMap> createState() => _InteractiveHeatMapState();
}
class _InteractiveHeatMapState extends State<InteractiveHeatMap> {
OccupancyPaintItem? _hoveredItem;
OverlayEntry? _overlayEntry;
final LayerLink _layerLink = LayerLink();
@override
void dispose() {
_removeOverlay();
_overlayEntry?.dispose();
super.dispose();
}
void _removeOverlay() {
_overlayEntry?.remove();
_overlayEntry = null;
}
void _showTooltip(OccupancyPaintItem item, Offset localPosition) {
_removeOverlay();
final column = item.index ~/ 7;
final row = item.index % 7;
final x = column * widget.cellSize;
final y = row * widget.cellSize;
_overlayEntry = OverlayEntry(
builder: (context) => Positioned(
child: CompositedTransformFollower(
link: _layerLink,
offset: Offset(x + widget.cellSize, y),
child: Material(
color: Colors.transparent,
child: Transform.translate(
offset: Offset(-(widget.cellSize * 2.5), -50),
child: HeatMapTooltip(date: item.date, value: item.value),
),
),
),
),
);
Overlay.of(context).insert(_overlayEntry!);
}
@override
Widget build(BuildContext context) {
return CompositedTransformTarget(
link: _layerLink,
child: MouseRegion(
onHover: (event) {
final column = event.localPosition.dx ~/ widget.cellSize;
final row = event.localPosition.dy ~/ widget.cellSize;
final index = column * 7 + row;
if (index >= 0 && index < widget.items.length) {
final item = widget.items[index];
if (_hoveredItem != item) {
setState(() => _hoveredItem = item);
_showTooltip(item, event.localPosition);
}
} else {
_removeOverlay();
setState(() => _hoveredItem = null);
}
},
onExit: (_) {
_removeOverlay();
setState(() => _hoveredItem = null);
},
child: CustomPaint(
isComplex: true,
size: _painterSize,
painter: OccupancyPainter(
items: widget.items,
maxValue: widget.maxValue,
hoveredItem: _hoveredItem,
),
),
),
);
}
Size get _painterSize {
final height = 7 * widget.cellSize;
final width = widget.items.length ~/ 7 * widget.cellSize;
return Size(width, height);
}
}

View File

@ -16,30 +16,38 @@ class OccupancyChart extends StatelessWidget {
Widget build(BuildContext context) {
return BarChart(
BarChartData(
maxY: 1.0,
maxY: 100.0,
gridData: EnergyManagementChartsHelper.gridData().copyWith(
checkToShowHorizontalLine: (value) => true,
horizontalInterval: 0.25,
horizontalInterval: 20,
),
borderData: EnergyManagementChartsHelper.borderData(),
barTouchData: _barTouchData(context),
titlesData: _titlesData(context),
titlesData: _titlesData(context).copyWith(
leftTitles: _titlesData(context).leftTitles.copyWith(
sideTitles: _titlesData(context).leftTitles.sideTitles.copyWith(
maxIncluded: true,
minIncluded: true,
),
),
),
barGroups: List.generate(chartData.length, (index) {
final actual = chartData[index];
final occupancyValue = double.parse(actual.occupancy);
return BarChartGroupData(
x: index,
barsSpace: 0,
groupVertically: true,
barRods: [
BarChartRodData(
toY: 1.0,
fromY: double.parse(actual.occupancy) + 0.025,
toY: 100.0,
fromY: occupancyValue == 0 ? occupancyValue : occupancyValue + 2.5,
color: ColorsManager.graysColor,
width: _chartWidth,
borderRadius: BorderRadius.circular(10),
),
BarChartRodData(
toY: double.parse(actual.occupancy),
toY: occupancyValue,
color: ColorsManager.vividBlue.withValues(alpha: 0.8),
width: _chartWidth,
borderRadius: BorderRadius.circular(10),
@ -81,7 +89,7 @@ class OccupancyChart extends StatelessWidget {
final data = chartData;
final occupancyValue = double.parse(data[group.x.toInt()].occupancy);
final percentage = '${(occupancyValue * 100).toStringAsFixed(0)}%';
final percentage = '${(occupancyValue).toStringAsFixed(0)}%';
return BarTooltipItem(
percentage,
@ -101,14 +109,14 @@ class OccupancyChart extends StatelessWidget {
final leftTitles = titlesData.leftTitles.copyWith(
sideTitles: titlesData.leftTitles.sideTitles.copyWith(
reservedSize: 70,
interval: 0.25,
interval: 20,
getTitlesWidget: (value, meta) => Padding(
padding: const EdgeInsetsDirectional.only(end: 12),
child: FittedBox(
alignment: AlignmentDirectional.centerStart,
fit: BoxFit.scaleDown,
child: Text(
'${(value * 100).toStringAsFixed(0)}%',
'${(value).toStringAsFixed(0)}%',
style: context.textTheme.bodySmall?.copyWith(
fontSize: 12,
color: ColorsManager.greyColor,

View File

@ -47,11 +47,14 @@ class OccupancyChartBox extends StatelessWidget {
context.read<AnalyticsDatePickerBloc>().add(
UpdateAnalyticsDatePickerEvent(montlyDate: value),
);
FetchOccupancyDataHelper.loadOccupancyData(
context,
communityId: spaceTreeState.selectedCommunities.firstOrNull ?? '',
spaceId: spaceTreeState.selectedSpaces.firstOrNull ?? '',
);
if (spaceTreeState.selectedSpaces.isNotEmpty) {
FetchOccupancyDataHelper.loadOccupancyChartData(
context,
spaceUuid:
spaceTreeState.selectedSpaces.firstOrNull ?? '',
date: value,
);
}
},
selectedDate: context
.watch<AnalyticsDatePickerBloc>()

View File

@ -1,15 +1,16 @@
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:syncrow_web/pages/analytics/models/power_clamp_energy_status.dart';
import 'package:syncrow_web/pages/analytics/modules/analytics/blocs/analytics_devices/analytics_devices_bloc.dart';
import 'package:syncrow_web/pages/analytics/modules/energy_management/blocs/realtime_device_changes/realtime_device_changes_bloc.dart';
import 'package:syncrow_web/pages/analytics/modules/energy_management/widgets/power_clamp_energy_data_device_dropdown.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/pages/analytics/modules/energy_management/widgets/power_clamp_energy_status_widget.dart';
import 'package:syncrow_web/pages/device_managment/all_devices/models/device_status.dart';
import 'package:syncrow_web/utils/color_manager.dart';
import 'package:syncrow_web/utils/constants/assets.dart';
import 'package:syncrow_web/utils/extension/build_context_x.dart';
import 'package:syncrow_web/utils/style.dart';
import 'package:uuid/uuid.dart';
class OccupancyEndSideBar extends StatelessWidget {
const OccupancyEndSideBar({super.key});
@ -37,7 +38,8 @@ class OccupancyEndSideBar extends StatelessWidget {
),
const SizedBox(height: 6),
SelectableText(
(const Uuid().v4()),
context.watch<AnalyticsDevicesBloc>().state.selectedDevice?.uuid ??
'N/A',
style: context.textTheme.bodySmall?.copyWith(
color: ColorsManager.blackColor,
fontWeight: FontWeight.w400,
@ -105,7 +107,7 @@ class OccupancyEndSideBar extends StatelessWidget {
mainAxisSize: MainAxisSize.min,
children: [
Expanded(
flex: 2,
flex: 3,
child: FittedBox(
fit: BoxFit.scaleDown,
alignment: AlignmentDirectional.centerStart,
@ -120,11 +122,18 @@ class OccupancyEndSideBar extends StatelessWidget {
),
),
const Spacer(),
const Expanded(
Expanded(
flex: 2,
child: FittedBox(
fit: BoxFit.scaleDown,
alignment: AlignmentDirectional.centerEnd,
child: PowerClampEnergyDataDeviceDropdown(),
child: AnalyticsDeviceDropdown(
onChanged: (value) =>
FetchEnergyManagementDataHelper.loadRealtimeDeviceChanges(
context,
deviceUuid: value.uuid,
),
),
),
),
],

View File

@ -1,6 +1,7 @@
import 'dart:math' as math show max;
import 'package:flutter/material.dart';
import 'package:syncrow_web/pages/analytics/modules/occupancy/widgets/interactive_heat_map.dart';
import 'package:syncrow_web/pages/analytics/modules/occupancy/widgets/occupancy_heat_map_days.dart';
import 'package:syncrow_web/pages/analytics/modules/occupancy/widgets/occupancy_heat_map_gradient.dart';
import 'package:syncrow_web/pages/analytics/modules/occupancy/widgets/occupancy_heat_map_months.dart';
@ -15,7 +16,7 @@ class OccupancyHeatMap extends StatelessWidget {
static const _totalWeeks = 53;
int get _maxValue => heatMapData.isNotEmpty
? heatMapData.keys.map((key) => heatMapData[key]!).reduce(math.max)
? heatMapData.keys.map((key) => heatMapData[key] ?? 0).reduce(math.max)
: 0;
DateTime _getStartingDate() {
@ -28,7 +29,7 @@ class OccupancyHeatMap extends StatelessWidget {
return List.generate(_totalWeeks * 7, (index) {
final date = startDate.add(Duration(days: index));
final value = heatMapData[date] ?? 0;
return OccupancyPaintItem(index: index, value: value);
return OccupancyPaintItem(index: index, value: value, date: date);
});
}
@ -58,15 +59,13 @@ class OccupancyHeatMap extends StatelessWidget {
child: Row(
children: [
const OccupancyHeatMapDays(cellSize: _cellSize),
CustomPaint(
size: const Size(_totalWeeks * _cellSize, 7 * _cellSize),
child: CustomPaint(
isComplex: true,
size: const Size(_totalWeeks * _cellSize, 7 * _cellSize),
painter: OccupancyPainter(
items: paintItems,
maxValue: _maxValue,
),
SizedBox(
width: _totalWeeks * _cellSize,
height: 7 * _cellSize,
child: InteractiveHeatMap(
items: paintItems,
maxValue: _maxValue,
cellSize: _cellSize,
),
),
],

View File

@ -47,12 +47,14 @@ class OccupancyHeatMapBox extends StatelessWidget {
context.read<AnalyticsDatePickerBloc>().add(
UpdateAnalyticsDatePickerEvent(yearlyDate: value),
);
FetchOccupancyDataHelper.loadOccupancyData(
context,
communityId:
spaceTreeState.selectedCommunities.firstOrNull ?? '',
spaceId: spaceTreeState.selectedSpaces.firstOrNull ?? '',
);
if (spaceTreeState.selectedSpaces.isNotEmpty) {
FetchOccupancyDataHelper.loadHeatMapData(
context,
spaceUuid:
spaceTreeState.selectedSpaces.firstOrNull ?? '',
year: value,
);
}
},
datePickerType: DatePickerType.year,
selectedDate: context
@ -68,7 +70,10 @@ class OccupancyHeatMapBox extends StatelessWidget {
Expanded(
child: OccupancyHeatMap(
heatMapData: state.heatMapData.asMap().map(
(_, value) => MapEntry(value.date, value.occupancy),
(_, value) => MapEntry(
value.eventDate,
value.countTotalPresenceDetected,
),
),
),
),

View File

@ -4,18 +4,25 @@ import 'package:syncrow_web/utils/color_manager.dart';
class OccupancyPaintItem {
final int index;
final int value;
final DateTime date;
const OccupancyPaintItem({required this.index, required this.value});
const OccupancyPaintItem({
required this.index,
required this.value,
required this.date,
});
}
class OccupancyPainter extends CustomPainter {
OccupancyPainter({
required this.items,
required this.maxValue,
this.hoveredItem,
});
final List<OccupancyPaintItem> items;
final int maxValue;
final OccupancyPaintItem? hoveredItem;
static const double cellSize = 16.0;
@ -25,6 +32,10 @@ class OccupancyPainter extends CustomPainter {
final Paint borderPaint = Paint()
..color = ColorsManager.grayBorder.withValues(alpha: 0.4)
..style = PaintingStyle.stroke;
final Paint hoveredBorderPaint = Paint()
..color = Colors.black
..style = PaintingStyle.stroke
..strokeWidth = 1.5;
for (final item in items) {
final column = item.index ~/ 7;
@ -37,22 +48,27 @@ class OccupancyPainter extends CustomPainter {
final rect = Rect.fromLTWH(x, y, cellSize, cellSize);
canvas.drawRect(rect, fillPaint);
_drawDashedLine(
canvas,
Offset(x, y),
Offset(x + cellSize, y),
borderPaint,
);
_drawDashedLine(
canvas,
Offset(x, y + cellSize),
Offset(x + cellSize, y + cellSize),
borderPaint,
);
// Highlight the hovered item
if (hoveredItem != null && hoveredItem!.index == item.index) {
canvas.drawRect(rect, hoveredBorderPaint);
} else {
_drawDashedLine(
canvas,
Offset(x, y),
Offset(x + cellSize, y),
borderPaint,
);
_drawDashedLine(
canvas,
Offset(x, y + cellSize),
Offset(x + cellSize, y + cellSize),
borderPaint,
);
canvas.drawLine(Offset(x, y), Offset(x, y + cellSize), borderPaint);
canvas.drawLine(
Offset(x + cellSize, y), Offset(x + cellSize, y + cellSize), borderPaint);
canvas.drawLine(Offset(x, y), Offset(x, y + cellSize), borderPaint);
canvas.drawLine(Offset(x + cellSize, y), Offset(x + cellSize, y + cellSize),
borderPaint);
}
}
}
@ -80,5 +96,6 @@ class OccupancyPainter extends CustomPainter {
}
@override
bool shouldRepaint(covariant CustomPainter oldDelegate) => false;
bool shouldRepaint(covariant OccupancyPainter oldDelegate) =>
oldDelegate.hoveredItem != hoveredItem;
}

View File

@ -0,0 +1,22 @@
enum AnalyticsDeviceRequestType { energyManagement, occupancy }
class GetAnalyticsDevicesParam {
final String? spaceUuid;
final List<String> deviceTypes;
final String? communityUuid;
final AnalyticsDeviceRequestType requestType;
const GetAnalyticsDevicesParam({
required this.requestType,
required this.spaceUuid,
required this.deviceTypes,
required this.communityUuid,
});
Map<String, dynamic> toJson() {
return <String, dynamic>{
if (spaceUuid != null) 'spaceUuid': spaceUuid,
if (communityUuid != null) 'communityUuid': communityUuid,
};
}
}

View File

@ -1,24 +1,20 @@
import 'package:equatable/equatable.dart';
class GetEnergyConsumptionByPhasesParam extends Equatable {
final DateTime? startDate;
final DateTime? endDate;
final String? spaceId;
final String powerClampUuid;
final DateTime? date;
const GetEnergyConsumptionByPhasesParam({
this.startDate,
this.endDate,
this.spaceId,
required this.powerClampUuid,
this.date,
});
Map<String, dynamic> toJson() {
return {
'startDate': startDate?.toIso8601String(),
'endDate': endDate?.toIso8601String(),
'spaceId': spaceId,
'monthDate': '${date?.year}-${date?.month.toString().padLeft(2, '0')}',
};
}
@override
List<Object?> get props => [startDate, endDate, spaceId];
List<Object?> get props => [powerClampUuid, date];
}

View File

@ -1,3 +1,19 @@
class GetEnergyConsumptionPerDeviceParam {
const GetEnergyConsumptionPerDeviceParam();
const GetEnergyConsumptionPerDeviceParam({
this.monthDate,
this.spaceId,
this.communityId,
});
final DateTime? monthDate;
final String? spaceId;
final String? communityId;
Map<String, dynamic> toJson() => {
'monthDate':
'${monthDate?.year}-${monthDate?.month.toString().padLeft(2, '0')}',
if (spaceId == null || spaceId == null) 'spaceUuid': spaceId,
'communityUuid': communityId,
'groupByDevice': true,
};
}

View File

@ -1,19 +1,13 @@
class GetOccupancyHeatMapParam {
final DateTime year;
final String communityId;
final String spaceId;
final String spaceUuid;
const GetOccupancyHeatMapParam({
required this.year,
required this.communityId,
required this.spaceId,
required this.spaceUuid,
});
Map<String, dynamic> toJson() {
return {
'year': year.toIso8601String(),
'communityId': communityId,
'spaceId': spaceId,
};
return {'year': year.year};
}
}

View File

@ -1,19 +1,11 @@
class GetOccupancyParam {
final String monthDate;
final String? spaceUuid;
final String communityUuid;
GetOccupancyParam({
required this.monthDate,
required this.spaceUuid,
required this.communityUuid,
});
Map<String, dynamic> toJson() {
return {
'monthDate': monthDate,
'spaceUuid': spaceUuid,
'communityUuid': communityUuid,
};
}
Map<String, dynamic> toJson() => {'monthDate': monthDate};
}

View File

@ -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];
}

View File

@ -13,7 +13,7 @@ class GetTotalEnergyConsumptionParam {
return {
'monthDate':
'${monthDate?.year}-${monthDate?.month.toString().padLeft(2, '0')}',
if (communityId == null || communityId!.isEmpty) 'spaceUuid': spaceId,
if (spaceId == null || spaceId == null) 'spaceUuid': spaceId,
'communityUuid': communityId,
'groupByDevice': false,
};

View File

@ -0,0 +1,6 @@
import 'package:syncrow_web/pages/analytics/models/analytics_device.dart';
import 'package:syncrow_web/pages/analytics/params/get_analytics_devices_param.dart';
abstract interface class AnalyticsDevicesService {
Future<List<AnalyticsDevice>> getDevices(GetAnalyticsDevicesParam param);
}

View File

@ -0,0 +1,24 @@
import 'package:syncrow_web/pages/analytics/models/analytics_device.dart';
import 'package:syncrow_web/pages/analytics/params/get_analytics_devices_param.dart';
import 'package:syncrow_web/pages/analytics/services/analytics_devices/analytics_devices_service.dart';
class AnalyticsDevicesServiceDelegate implements AnalyticsDevicesService {
const AnalyticsDevicesServiceDelegate(
this._occupancyService,
this._energyManagementService,
);
final AnalyticsDevicesService _occupancyService;
final AnalyticsDevicesService _energyManagementService;
@override
Future<List<AnalyticsDevice>> getDevices(
GetAnalyticsDevicesParam param,
) {
return switch (param.requestType) {
AnalyticsDeviceRequestType.occupancy => _occupancyService.getDevices(param),
AnalyticsDeviceRequestType.energyManagement =>
_energyManagementService.getDevices(param),
};
}
}

View File

@ -0,0 +1,36 @@
import 'package:syncrow_web/pages/analytics/models/analytics_device.dart';
import 'package:syncrow_web/pages/analytics/params/get_analytics_devices_param.dart';
import 'package:syncrow_web/pages/analytics/services/analytics_devices/analytics_devices_service.dart';
import 'package:syncrow_web/services/api/http_service.dart';
final class RemoteEnergyManagementAnalyticsDevicesService
implements AnalyticsDevicesService {
const RemoteEnergyManagementAnalyticsDevicesService(this._httpService);
final HTTPService _httpService;
@override
Future<List<AnalyticsDevice>> getDevices(GetAnalyticsDevicesParam param) async {
try {
final response = await _httpService.get(
path: '/devices-space-community/recursive-child',
queryParameters: param.toJson()
..addAll({'productType': param.deviceTypes.first}),
expectedResponseModel: (response) {
final json = response as Map<String, dynamic>;
final dailyData = json['data'] as List<dynamic>? ?? <dynamic>[];
final result = dailyData.map(
(json) => AnalyticsDevice.fromJson(json as Map<String, dynamic>),
);
return result.toList();
},
);
return response;
} catch (e) {
throw Exception('Failed to load total energy consumption: $e');
}
}
}

View File

@ -0,0 +1,61 @@
import 'package:syncrow_web/pages/analytics/models/analytics_device.dart';
import 'package:syncrow_web/pages/analytics/params/get_analytics_devices_param.dart';
import 'package:syncrow_web/pages/analytics/services/analytics_devices/analytics_devices_service.dart';
import 'package:syncrow_web/pages/common/bloc/project_manager.dart';
import 'package:syncrow_web/services/api/http_service.dart';
class RemoteOccupancyAnalyticsDevicesService implements AnalyticsDevicesService {
const RemoteOccupancyAnalyticsDevicesService(this._httpService);
final HTTPService _httpService;
@override
Future<List<AnalyticsDevice>> getDevices(GetAnalyticsDevicesParam param) async {
try {
final requests = await Future.wait<List<AnalyticsDevice>>(
param.deviceTypes.map((e) {
final mappedParam = GetAnalyticsDevicesParam(
requestType: AnalyticsDeviceRequestType.occupancy,
spaceUuid: param.spaceUuid,
deviceTypes: [e],
communityUuid: param.communityUuid,
);
return _makeRequest(mappedParam);
}).toList(),
);
final result = requests.map((e) => e.first).toList();
return result;
} catch (e) {
throw Exception('Failed to load total energy consumption: $e');
}
}
Future<List<AnalyticsDevice>> _makeRequest(GetAnalyticsDevicesParam param) async {
try {
final projectUuid = await ProjectManager.getProjectUUID();
final response = await _httpService.get(
path:
'/projects/$projectUuid/communities/${param.communityUuid}/spaces/${param.spaceUuid}/devices',
queryParameters: {
'communityUuid': param.communityUuid,
'spaceUuid': param.spaceUuid,
'productType': param.deviceTypes.first,
},
expectedResponseModel: (response) {
final json = response as Map<String, dynamic>;
final dailyData = json['data'] as List<dynamic>? ?? <dynamic>[];
final result = dailyData.map(
(json) => AnalyticsDevice.fromJson(json as Map<String, dynamic>),
);
return result.toList();
},
);
return response;
} catch (e) {
rethrow;
}
}
}

View File

@ -1,29 +0,0 @@
import 'package:syncrow_web/pages/analytics/models/phases_energy_consumption.dart';
import 'package:syncrow_web/pages/analytics/params/get_energy_consumption_by_phases_param.dart';
import 'package:syncrow_web/pages/analytics/services/energy_consumption_by_phases/energy_consumption_by_phases_service.dart';
class FakeEnergyConsumptionByPhasesService
implements EnergyConsumptionByPhasesService {
@override
Future<List<PhasesEnergyConsumption>> load(
GetEnergyConsumptionByPhasesParam param,
) {
return Future.delayed(
const Duration(milliseconds: 500),
() => const [
PhasesEnergyConsumption(month: 1, phaseA: 200, phaseB: 300, phaseC: 400),
PhasesEnergyConsumption(month: 2, phaseA: 300, phaseB: 400, phaseC: 500),
PhasesEnergyConsumption(month: 3, phaseA: 400, phaseB: 500, phaseC: 600),
PhasesEnergyConsumption(month: 4, phaseA: 100, phaseB: 100, phaseC: 100),
PhasesEnergyConsumption(month: 5, phaseA: 300, phaseB: 400, phaseC: 500),
PhasesEnergyConsumption(month: 6, phaseA: 300, phaseB: 100, phaseC: 400),
PhasesEnergyConsumption(month: 7, phaseA: 300, phaseB: 100, phaseC: 400),
PhasesEnergyConsumption(month: 8, phaseA: 500, phaseB: 100, phaseC: 100),
PhasesEnergyConsumption(month: 9, phaseA: 500, phaseB: 100, phaseC: 200),
PhasesEnergyConsumption(month: 10, phaseA: 100, phaseB: 50, phaseC: 50),
PhasesEnergyConsumption(month: 11, phaseA: 600, phaseB: 750, phaseC: 130),
PhasesEnergyConsumption(month: 12, phaseA: 100, phaseB: 100, phaseC: 100),
],
);
}
}

View File

@ -15,8 +15,9 @@ final class RemoteEnergyConsumptionByPhasesService
) async {
try {
final response = await _httpService.get(
path: 'endpoint',
path: '/power-clamp/${param.powerClampUuid}/historical',
showServerMessage: true,
queryParameters: param.toJson(),
expectedResponseModel: (data) {
final json = data as Map<String, dynamic>? ?? {};
final mappedData = json['data'] as List<dynamic>? ?? [];
@ -28,7 +29,7 @@ final class RemoteEnergyConsumptionByPhasesService
);
return response;
} catch (e) {
throw Exception('Failed to load energy consumption per device: $e');
throw Exception('Failed to load energy consumption per phase: $e');
}
}
}

View File

@ -1,39 +0,0 @@
import 'dart:math' as math show Random;
import 'package:flutter/material.dart';
import 'package:syncrow_web/pages/analytics/models/device_energy_data_model.dart';
import 'package:syncrow_web/pages/analytics/models/energy_data_model.dart';
import 'package:syncrow_web/pages/analytics/params/get_energy_consumption_per_device_param.dart';
import 'package:syncrow_web/pages/analytics/services/energy_consumption_per_device/energy_consumption_per_device_service.dart';
class FakeEnergyConsumptionPerDeviceService
implements EnergyConsumptionPerDeviceService {
@override
Future<List<DeviceEnergyDataModel>> load(
GetEnergyConsumptionPerDeviceParam param,
) {
final random = math.Random();
return Future.delayed(const Duration(milliseconds: 500), () {
return [
(Colors.redAccent, 1),
(Colors.lightBlueAccent, 2),
(Colors.purpleAccent, 3),
].map((e) {
final (color, index) = e;
return DeviceEnergyDataModel(
color: color,
energy: List.generate(30, (i) => i)
.map(
(index) => EnergyDataModel(
date: DateTime(2025, 1, index + 1),
value: random.nextInt(100) + (index * 100),
),
)
.toList(),
deviceName: 'Device $index',
deviceId: 'device_$index',
);
}).toList();
});
}
}

View File

@ -1,4 +1,6 @@
import 'package:flutter/material.dart';
import 'package:syncrow_web/pages/analytics/models/device_energy_data_model.dart';
import 'package:syncrow_web/pages/analytics/models/energy_data_model.dart';
import 'package:syncrow_web/pages/analytics/params/get_energy_consumption_per_device_param.dart';
import 'package:syncrow_web/pages/analytics/services/energy_consumption_per_device/energy_consumption_per_device_service.dart';
import 'package:syncrow_web/services/api/http_service.dart';
@ -15,16 +17,10 @@ class RemoteEnergyConsumptionPerDeviceService
) async {
try {
final response = await _httpService.get(
path: 'endpoint',
path: '/power-clamp/historical',
showServerMessage: true,
expectedResponseModel: (data) {
final json = data as Map<String, dynamic>? ?? {};
final mappedData = json['data'] as List<dynamic>? ?? [];
return mappedData.map((e) {
final jsonData = e as Map<String, dynamic>;
return DeviceEnergyDataModel.fromJson(jsonData);
}).toList();
},
queryParameters: param.toJson(),
expectedResponseModel: _EnergyConsumptionPerDeviceMapper.map,
);
return response;
} catch (e) {
@ -32,3 +28,30 @@ class RemoteEnergyConsumptionPerDeviceService
}
}
}
abstract final class _EnergyConsumptionPerDeviceMapper {
const _EnergyConsumptionPerDeviceMapper._();
static List<DeviceEnergyDataModel> map(dynamic data) {
final json = data as Map<String, dynamic>? ?? {};
final mappedData = json['data'] as List<dynamic>? ?? [];
return mappedData.map((e) {
final deviceData = e as Map<String, dynamic>;
final energyData = deviceData['data'] as List<dynamic>;
return DeviceEnergyDataModel(
deviceId: deviceData['deviceUuid'] as String,
deviceName: deviceData['deviceName'] as String,
color: Color((DateTime.now().microsecondsSinceEpoch +
deviceData['deviceUuid'].hashCode) |
0xFF000000),
energy: energyData.map((data) {
final energyJson = data as Map<String, dynamic>;
return EnergyDataModel(
date: DateTime.parse(energyJson['date'] as String),
value: double.parse(energyJson['total_energy_consumed_kw'] as String),
);
}).toList(),
);
}).toList();
}
}

View File

@ -1,19 +0,0 @@
import 'package:syncrow_web/pages/analytics/models/occupacy.dart';
import 'package:syncrow_web/pages/analytics/params/get_occupancy_param.dart';
import 'package:syncrow_web/pages/analytics/services/occupacy/occupacy_service.dart';
class FakeOccupacyService implements OccupacyService {
@override
Future<List<Occupacy>> load(GetOccupancyParam param) async {
return await Future.delayed(
const Duration(seconds: 1),
() => List.generate(
30,
(index) => Occupacy(
date: DateTime.now().subtract(Duration(days: index)).toString(),
occupancy: ((index / 100)).toString(),
),
),
);
}
}

View File

@ -0,0 +1,32 @@
import 'package:syncrow_web/pages/analytics/models/occupacy.dart';
import 'package:syncrow_web/pages/analytics/params/get_occupancy_param.dart';
import 'package:syncrow_web/pages/analytics/services/occupacy/occupacy_service.dart';
import 'package:syncrow_web/services/api/http_service.dart';
final class RemoteOccupancyService implements OccupacyService {
const RemoteOccupancyService(this._httpService);
final HTTPService _httpService;
@override
Future<List<Occupacy>> load(GetOccupancyParam param) async {
try {
final response = await _httpService.get(
path: '/occupancy/duration/space/${param.spaceUuid}',
showServerMessage: true,
queryParameters: param.toJson(),
expectedResponseModel: (data) {
final json = data as Map<String, dynamic>? ?? {};
final mappedData = json['data'] as List<dynamic>? ?? [];
return mappedData.map((e) {
final jsonData = e as Map<String, dynamic>;
return Occupacy.fromJson(jsonData);
}).toList();
},
);
return response;
} catch (e) {
throw Exception('Failed to load energy consumption per phase: $e');
}
}
}

View File

@ -1,25 +0,0 @@
import 'package:syncrow_web/pages/analytics/models/occupancy_heat_map_model.dart';
import 'package:syncrow_web/pages/analytics/params/get_occupancy_heat_map_param.dart';
import 'package:syncrow_web/pages/analytics/services/occupancy_heat_map/occupancy_heat_map_service.dart';
class FakeOccupancyHeatMapService implements OccupancyHeatMapService {
@override
Future<List<OccupancyHeatMapModel>> load(GetOccupancyHeatMapParam param) {
return Future.delayed(const Duration(milliseconds: 200), () {
final now = DateTime.now();
final startOfYear = DateTime(now.year, 1, 1);
final endOfYear = DateTime(now.year, 12, 31);
final daysInYear = endOfYear.difference(startOfYear).inDays + 1;
final List<OccupancyHeatMapModel> data = List.generate(
daysInYear,
(index) => OccupancyHeatMapModel(
date: startOfYear.add(Duration(days: index)),
occupancy: ((index + 1) * 10) % 100,
),
);
return data;
});
}
}

View File

@ -0,0 +1,35 @@
import 'package:syncrow_web/pages/analytics/models/occupancy_heat_map_model.dart';
import 'package:syncrow_web/pages/analytics/params/get_occupancy_heat_map_param.dart';
import 'package:syncrow_web/pages/analytics/services/occupancy_heat_map/occupancy_heat_map_service.dart';
import 'package:syncrow_web/services/api/http_service.dart';
final class RemoteOccupancyHeatMapService implements OccupancyHeatMapService {
const RemoteOccupancyHeatMapService(this._httpService);
final HTTPService _httpService;
@override
Future<List<OccupancyHeatMapModel>> load(GetOccupancyHeatMapParam param) async {
try {
final response = await _httpService.get(
path: '/occupancy/heat-map/space/${param.spaceUuid}',
showServerMessage: true,
queryParameters: param.toJson(),
expectedResponseModel: (response) {
final json = response as Map<String, dynamic>;
final dailyData = json['data'] as List<dynamic>? ?? <dynamic>[];
final result = dailyData.map(
(json) => OccupancyHeatMapModel.fromJson(json as Map<String, dynamic>),
);
return result.toList();
},
);
return response;
} catch (e) {
throw Exception('Failed to load total energy consumption:');
}
}
}

View File

@ -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,
);
});
});
}
}

View File

@ -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);
}

View File

@ -12,7 +12,7 @@ class AnalyticsErrorWidget extends StatelessWidget {
return Visibility(
visible: errorMessage != null || (errorMessage?.isNotEmpty ?? false),
child: Text(
'$errorMessage ?? "Something went wrong"',
errorMessage ?? 'Something went wrong',
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: context.textTheme.bodySmall?.copyWith(

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
@ -63,7 +74,8 @@ class _DynamicTableState extends State<DynamicTable> {
}
}
bool _compareListOfLists(List<List<dynamic>> oldList, List<List<dynamic>> newList) {
bool _compareListOfLists(
List<List<dynamic>> oldList, List<List<dynamic>> newList) {
// Check if the old and new lists are the same
if (oldList.length != newList.length) return false;
@ -101,87 +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: false,
trackVisibility: false,
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
? 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(),
],
);
}),
),
),
),
),
),
),
),
],
),
);
}
@ -196,11 +208,39 @@ class _DynamicTableState extends State<DynamicTable> {
),
child: Checkbox(
value: _selectAll,
onChanged: widget.withSelectAll && widget.data.isNotEmpty ? _toggleSelectAll : null,
onChanged: widget.withSelectAll && widget.data.isNotEmpty
? _toggleSelectAll
: null,
),
);
}
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,
@ -238,7 +278,9 @@ class _DynamicTableState extends State<DynamicTable> {
constraints: const BoxConstraints.expand(height: 40),
alignment: Alignment.centerLeft,
child: Padding(
padding: EdgeInsets.symmetric(horizontal: index == widget.headers.length - 1 ? 12 : 8.0, vertical: 4),
padding: EdgeInsets.symmetric(
horizontal: index == widget.headers.length - 1 ? 12 : 8.0,
vertical: 4),
child: Text(
title,
style: context.textTheme.titleSmall!.copyWith(
@ -253,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) {
@ -311,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),
),
);
}
}

View File

@ -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,
};
}

View File

@ -97,7 +97,8 @@ class DeviceControlDialog extends StatelessWidget with RouteControlsBasedCode {
children: [
_buildInfoRow('Space Name:',
device.spaces?.firstOrNull?.spaceName ?? 'N/A'),
_buildInfoRow('Room:', device.subspace?.subspaceName ?? 'N/A'),
_buildInfoRow(
'Sub space:', device.subspace?.subspaceName ?? 'N/A'),
],
),
TableRow(

View File

@ -11,16 +11,11 @@ import 'package:syncrow_web/pages/routines/bloc/routine_bloc/routine_bloc.dart';
import 'package:syncrow_web/pages/space_tree/bloc/space_tree_bloc.dart';
import 'package:syncrow_web/pages/space_tree/bloc/space_tree_event.dart';
import 'package:syncrow_web/services/home_api.dart';
import 'package:syncrow_web/utils/color_manager.dart';
import 'package:syncrow_web/utils/constants/assets.dart';
import 'package:syncrow_web/utils/constants/routes_const.dart';
import 'package:syncrow_web/utils/navigation_service.dart';
class HomeBloc extends Bloc<HomeEvent, HomeState> {
// final Graph graph = Graph()..isTree = true;
// final BuchheimWalkerConfiguration builder = BuchheimWalkerConfiguration();
// List<Node> sourcesList = [];
// List<Node> destinationsList = [];
UserModel? user;
String terms = '';
String policy = '';
@ -33,22 +28,6 @@ class HomeBloc extends Bloc<HomeEvent, HomeState> {
on<ConfirmUserAgreementEvent>(_confirmUserAgreement);
}
// void _createNode(CreateNewNode event, Emitter<HomeState> emit) async {
// emit(HomeInitial());
// sourcesList.add(event.sourceNode);
// destinationsList.add(event.destinationNode);
// for (int i = 0; i < sourcesList.length; i++) {
// graph.addEdge(sourcesList[i], destinationsList[i]);
// }
// builder
// ..siblingSeparation = (100)
// ..levelSeparation = (150)
// ..subtreeSeparation = (150)
// ..orientation = (BuchheimWalkerConfiguration.ORIENTATION_TOP_BOTTOM);
// emit(HomeUpdateTree(graph: graph, builder: builder));
// }
Future _fetchUserInfo(FetchUserInfo event, Emitter<HomeState> emit) async {
try {
var uuid = await const FlutterSecureStorage().read(key: UserModel.userUuidKey);
@ -99,16 +78,6 @@ class HomeBloc extends Bloc<HomeEvent, HomeState> {
}
}
// static Future fetchUserInfo() async {
// try {
// var uuid =
// await const FlutterSecureStorage().read(key: UserModel.userUuidKey);
// user = await HomeApi().fetchUserInfo(uuid);
// } catch (e) {
// return;
// }
// }
List<HomeItemModel> homeItems = [
HomeItemModel(
title: 'Access Management',
@ -118,7 +87,7 @@ class HomeBloc extends Bloc<HomeEvent, HomeState> {
context.read<SpaceTreeBloc>().add(ClearCachedData());
context.go(RoutesConst.accessManagementPage);
},
color: null,
color: const Color(0xFF0036E6),
),
HomeItemModel(
title: 'Space Management',
@ -128,7 +97,7 @@ class HomeBloc extends Bloc<HomeEvent, HomeState> {
context.read<SpaceTreeBloc>().add(ClearCachedData());
context.go(RoutesConst.spacesManagementPage);
},
color: ColorsManager.primaryColor,
color: const Color(0xFF0026A2),
),
HomeItemModel(
title: 'Devices Management',
@ -140,12 +109,11 @@ class HomeBloc extends Bloc<HomeEvent, HomeState> {
.add(const TriggerSwitchTabsEvent(isRoutineTab: false));
context.go(RoutesConst.deviceManagementPage);
},
color: ColorsManager.primaryColor,
color: const Color(0xFF00165E),
),
HomeItemModel(
title: 'Syncrow Analytics',
icon: Assets.devicesIcon,
icon: Assets.analyticsIcon,
active: true,
onPress: (context) {
context.read<SpaceTreeBloc>().add(ClearCachedData());
@ -153,43 +121,7 @@ class HomeBloc extends Bloc<HomeEvent, HomeState> {
.add(const TriggerSwitchTabsEvent(isRoutineTab: false));
context.go(RoutesConst.analytics);
},
color: ColorsManager.primaryColor,
color: const Color(0xFF023DFE),
),
// HomeItemModel(
// title: 'Move in',
// icon: Assets.moveinIcon,
// active: false,
// onPress: (context) {},
// color: ColorsManager.primaryColor,
// ),
// HomeItemModel(
// title: 'Construction',
// icon: Assets.constructionIcon,
// active: false,
// onPress: (context) {},
// color: ColorsManager.primaryColor,
// ),
// HomeItemModel(
// title: 'Energy',
// icon: Assets.energyIcon,
// active: false,
// onPress: (context) {},
// color: ColorsManager.slidingBlueColor.withOpacity(0.2),
// ),
// HomeItemModel(
// title: 'Integrations',
// icon: Assets.integrationsIcon,
// active: false,
// onPress: (context) {},
// color: ColorsManager.slidingBlueColor.withOpacity(0.2),
// ),
// HomeItemModel(
// title: 'Asset',
// icon: Assets.assetIcon,
// active: false,
// onPress: (context) {},
// color: ColorsManager.slidingBlueColor.withOpacity(0.2),
// ),
];
}

View File

@ -1,6 +1,5 @@
import 'package:flutter/material.dart';
import 'package:flutter_svg/flutter_svg.dart';
import 'package:syncrow_web/utils/color_manager.dart';
class HomeCard extends StatelessWidget {
final bool active;
@ -8,6 +7,7 @@ class HomeCard extends StatelessWidget {
final int index;
final String name;
final Function()? onTap;
final Color? color;
const HomeCard({
super.key,
required this.name,
@ -15,28 +15,16 @@ class HomeCard extends StatelessWidget {
this.active = false,
required this.img,
required this.onTap,
required this.color,
});
@override
Widget build(BuildContext context) {
// bool evenNumbers = index % 2 == 0;
return InkWell(
onTap: active ? onTap : null,
child: Container(
padding: const EdgeInsets.only(left: 10, right: 10, bottom: 10),
decoration: BoxDecoration(
color: index == 0 && active
? ColorsManager.blue1.withOpacity(0.9)
: index == 1 && active
? ColorsManager.blue2.withOpacity(0.9)
: index == 2 && active
? ColorsManager.blue3
: index == 4 && active == false
? ColorsManager.blue4.withOpacity(0.2)
: index == 7 && active == false
? ColorsManager.blue4.withOpacity(0.2)
: ColorsManager.blueColor.withOpacity(0.2),
// (active ?ColorsManager.blueColor
// : ColorsManager.blueColor.withOpacity(0.2)),
color: color,
borderRadius: BorderRadius.circular(30),
),
child: Column(
@ -64,15 +52,9 @@ class HomeCard extends StatelessWidget {
),
const SizedBox(height: 10),
Expanded(
child: Row(
mainAxisAlignment: MainAxisAlignment.end,
children: [
SizedBox(
child: SvgPicture.asset(
img,
),
),
],
child: Align(
alignment: AlignmentDirectional.bottomEnd,
child: SvgPicture.asset(img),
),
),
],

View File

@ -50,7 +50,7 @@ class HomeMobilePage extends StatelessWidget {
height: size.height * 0.6,
width: size.width * 0.68,
child: GridView.builder(
itemCount: homeItems.length,
itemCount: homeBloc.homeItems.length,
gridDelegate:
const SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 2,
@ -61,7 +61,8 @@ class HomeMobilePage extends StatelessWidget {
itemBuilder: (context, index) {
return HomeCard(
index: index,
active: homeBloc.homeItems[index].active!,
active: true,
color: homeBloc.homeItems[index].color,
name: homeBloc.homeItems[index].title!,
img: homeBloc.homeItems[index].icon!,
onTap: () =>
@ -78,56 +79,4 @@ class HomeMobilePage extends StatelessWidget {
),
);
}
final dynamic homeItems = [
{
'title': 'Access',
'icon': Assets.accessIcon,
'active': true,
},
{
'title': 'Space\nManagement',
'icon': Assets.spaseManagementIcon,
'color': ColorsManager.primaryColor,
'active': true,
},
{
'title': 'Devices',
'icon': Assets.devicesIcon,
'active': true,
},
{
'title': 'Syncrow Analytics',
'icon': Assets.iconEdit,
'active': true,
},
// {
// 'title': 'Move in',
// 'icon': Assets.moveinIcon,
// 'active': false,
// },
// {
// 'title': 'Construction',
// 'icon': Assets.constructionIcon,
// 'active': false,
// },
// {
// 'title': 'Energy',
// 'icon': Assets.energyIcon,
// 'color': ColorsManager.slidingBlueColor.withOpacity(0.2),
// 'active': false,
// },
// {
// 'title': 'Integrations',
// 'icon': Assets.integrationsIcon,
// 'color': ColorsManager.slidingBlueColor.withOpacity(0.2),
// 'active': false,
// },
// {
// 'title': 'Asset',
// 'icon': Assets.assetIcon,
// 'color': ColorsManager.slidingBlueColor.withOpacity(0.2),
// 'active': false,
// },
];
}

View File

@ -106,6 +106,7 @@ class _HomeWebPageState extends State<HomeWebPage> {
),
itemBuilder: (context, index) {
return HomeCard(
color: homeBloc.homeItems[index].color,
index: index,
active: homeBloc.homeItems[index].active!,
name: homeBloc.homeItems[index].title!,

View File

@ -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,

View File

@ -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()),
],
),
),

View File

@ -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));
},
),
),

View File

@ -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;
}

View File

@ -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,
);
}
}

View File

@ -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() => [];
}

View File

@ -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,
});
}

View File

@ -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(),
);
}
}

View File

@ -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(

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));

View File

@ -27,6 +27,7 @@ class _RoutineDevicesState extends State<RoutineDevices> {
'CPS',
'NCPS',
'WH',
'PC',
};
@override

View File

@ -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,

View File

@ -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:

Some files were not shown because too many files have changed in this diff Show More