Compare commits

..

109 Commits

Author SHA1 Message Date
0d45a155e3 add step parameter in onTapFunction.
Add dialogType parameter in WaterHeaterPresenceSensor and CeilingSensorDialog.
Update step parameter in FlushValueSelectorWidget.
Update step parameter in FunctionBloc and WaterHeaterFunctions.
Update step, unit, min, and max parameters in ACFunction subclasses.
2025-05-19 11:22:15 +03:00
7f9d044f7e Merge pull request #184 from SyncrowIOT/SP-1530-FE-Add-card-for-the-water-heater-in-the-routine-web
add water heater operational values to routines
2025-05-14 09:20:07 +03:00
996a847a27 Refactor water heater value selector widget 2025-05-14 09:16:04 +03:00
5645fb7826 Merge pull request #182 from SyncrowIOT/SP-1519-FE-Handle-Loading-Skeletons-and-No-Data-Error-States
Sp 1519 fe handle loading skeletons and no data error states
2025-05-13 16:55:54 +03:00
e8f7c29652 Applies correct business logic of the sidebar. 2025-05-13 16:46:34 +03:00
36c5712c79 add water heater operational values to routines 2025-05-13 16:24:08 +03:00
c7fef11aec Fixed typo Tab to run to Tap to run. 2025-05-12 12:06:37 +03:00
ef29d78d70 Clears data when needed. 2025-05-12 10:02:56 +03:00
cd9941f544 Doesn't load occupancy data on initState in AnalyticsOccupancyView. 2025-05-12 10:02:08 +03:00
71aa64ba9e Merge pull request #181 from SyncrowIOT/bugfix/analytics_expansion_bugfix
bugfix/analytics_expansion_bugfix.
2025-05-12 09:22:12 +03:00
2262d3b2ba bugfix/analytics_expansion_bugfix. 2025-05-12 09:20:01 +03:00
b7ef9da35d Sp 1513 fe implement device dropdown and live status card presence vacancy (#179)
* Called the widget of presence sensor status widgets.

* Enahnced `PowerClampEnergyDataDeviceDropdown` design and made it a dropdown.

* connected the realtime feature to the occupancy side bar, but with a mock id.

* revert default tab to energyManagement.
2025-05-11 16:59:15 +03:00
49e93329c8 Sp 1511 fe build occupancy heat map weekly monthly intensity view (#178)
* set the default tab to occupancy for ease of development.

* Implemented an initial design for the occupancy chart.

* Add Occupacy model and service for occupancy data handling.

* Created `OccupancyBloc`.

* Implemented the sidebar of Occupancy view.

* Moved `OccupancyEndSideBar` widget to its own file.

* Removed unnecessary widgets.

* Matched the `OccupancyChart` with the figma design.

* Added `AnalyticsDateFilterButton` to `OccupancyChartBox`.

* Hides `AnalyticsDateFilterButton` that is in the page header, when the selected tab isn't `AnalyticsPageTab.energyManagement`.

* Added animation to`AnalyticsDateFilterButton`.

* modified the implementation of `FakeOccupacyService` to clamp all the generated values to less than a 100.

* Injected `OccupancyBloc` into `AnalyticsPage`.

* Made `OccupancyChart` read its data from `OccupancyBloc`.

* Refactor AnalyticsCommunitiesSidebar to load data based on selected tab and implement loadEnergyManagementData method

* Refactor Analytics views to use StatefulWidget and load data in initState

* Created `OccupancyHeatMapModel`.

* Add FakeOccupancyHeatMapService implementation.

* Created `OccupancyHeatMapBloc`.

* Injected `OccupancyHeatMapBloc` into `AnalyticsPage`.

* Add OccupancyHeatMapBox widget and integrate into AnalyticsOccupancyView

* Matching the heat map with the design, and added week days.

* Made the HeatMap cells have a dashed border.

* shows months.

* responsiveness.

* Integrate OccupancyHeatMapBloc and update OccupancyHeatMapBox to display heat map data with error handling

* Integrate OccupancyHeatMapBloc and update OccupancyHeatMapBox to display heat map data with error handling

* made the heatmap loading fast af by using painters instead of individually creating a widget for every single event.

* Extracted `OccupancyHeatMapMonths` into its own widgte.

* Moved `OccupancyHeatMapMonths` to its own file.

* Adjusted design of `OccupancyHeatMapMonths`.

* Adjust layout flex properties for `OccupancyEndSideBar` and its parent column in `AnalyticsOccupancyView`.

* moved `OccupancyPaintItem` to `OccupancyPainter`s file.

* removed comments from `OccupancyPainter`.

* used color.withValues instead of .withOpacity.

* re-added `OccupancyHeatMapGradient`.

* Revert initial tab to `energyManagement`.

* Made datepicker dynamic for multiple states.

* Add year picker functionality to date filter button and implement dynamic date selection

* Align date filter button to the end in occupancy chart and heat map boxes for improved UI consistency.

* Enahnced color of border in `OccupancyPainter`.

* Add ClearOccupancyHeatMapEvent to reset heat map state and update occupancy data helper to trigger event on empty selections

* show percentage of value in tool tip of `OccupancyChart`.
2025-05-11 16:58:13 +03:00
d6f0b53b59 Sp 1494 api integration (#180)
* SP-1494-api-integration.

* fixed left stide titles intervals in total energy consumption chart.

* Adjusted tooltip and title intervals in energy management charts to improve accuracy by incrementing displayed values by one.

* Refactor AnalyticsCommunitiesSidebar to use AnalyticsSpaceTreeView and enhance community/space selection handling

* Gave every tab its own selection logic using the strategy design pattern, along with clearing the selection when changing tabes to avoid collision between features.
2025-05-11 16:46:00 +03:00
7154693379 SP-1495-fix-deployment by wrapping ChartsLoadingWidget.CircularProgressIndicator with a padding instead of adding padding as a property of CircularProgressIndicator. (#175) 2025-05-08 16:32:50 +03:00
2e2bc99501 Merge pull request #176 from SyncrowIOT/SP-1510-FE-Build-Occupancy-Bar-Chart-Monthly-Consumption-View
Sp 1510 fe build occupancy bar chart monthly consumption view
2025-05-08 16:32:21 +03:00
53222bee81 Merge pull request #174 from SyncrowIOT/SP-1495-FE-Build-Energy-Consumption-per-Device-Chart-Placeholder
Sp 1495 fe build energy consumption per device chart placeholder
2025-05-07 15:54:37 +03:00
bfb9158652 Replaced hardcoded device id from RemotePowerClampInfoService.getInfo, and instead used the one from the method parameter. 2025-05-07 15:47:41 +03:00
7f03222c12 Removed unnecessary widgets. 2025-05-07 15:46:24 +03:00
5e6c14efeb added loading indicators to charts. 2025-05-07 12:20:46 +03:00
9bbf3e75fa bugfixes. 2025-05-07 11:55:04 +03:00
303b0236f1 Added default error message for edge case. 2025-05-07 11:44:29 +03:00
4e3e63723e added error messages everywhere. 2025-05-07 11:43:05 +03:00
38ff20f86a Created initial remote implementation for all the services in the energy management module. 2025-05-07 11:28:31 +03:00
d539e6266e gets data when changing the date. 2025-05-07 11:12:39 +03:00
7467f8d0ea Removed the analytics overview view for now, since there will be no implementation for it. 2025-05-07 10:59:55 +03:00
a11e20147e preparing for integration, by fetching data when selecting a community. 2025-05-07 10:58:46 +03:00
55a6974bdc deselect selected spaces when selecting a new space in analytics side bar. 2025-05-06 15:53:43 +03:00
f8f58a24b8 . 2025-05-06 15:47:30 +03:00
682e69e65f modify data to migrate to days instead of months. 2025-05-06 15:44:33 +03:00
59a59231ec Merge branches 'SP-1495-FE-Build-Energy-Consumption-per-Device-Chart-Placeholder' and 'SP-1495-FE-Build-Energy-Consumption-per-Device-Chart-Placeholder' of https://github.com/SyncrowIOT/web into SP-1495-FE-Build-Energy-Consumption-per-Device-Chart-Placeholder 2025-05-06 15:02:46 +03:00
ad41a2a87e Implemented calendar widget and bloc. 2025-05-06 15:02:09 +03:00
974aa8f2a4 Implemented calendar widget and bloc. 2025-05-06 14:59:54 +03:00
428cd34492 Fixed device cell sizing. 2025-05-06 13:19:55 +03:00
1a6121c452 Made analytics energy management view scrollable, to allow for a better UX experience. 2025-05-06 12:24:47 +03:00
e8f9ae944c Refactor BlocListener in PowerClampEnergyDataWidget to simplify state handling 2025-05-06 09:17:35 +03:00
7e37aed026 formatted PowerClampInfoBloc. 2025-05-06 09:09:27 +03:00
d89e227599 removed unnecesary type annotations from FirebaseRealtimeDeviceService. 2025-05-06 09:06:33 +03:00
5a68b22f0c Update RealtimeDeviceChangesBloc to handle loading and failure states 2025-05-06 09:05:16 +03:00
38184ca8b2 Integrated realtime data. 2025-05-05 16:58:48 +03:00
4d5de7bc05 making good progress on realtime data integration. 2025-05-05 16:34:56 +03:00
1a3006fa43 Power clamp info integration. 2025-05-05 12:57:27 +03:00
490ca2057e Reduce bar width in EnergyConsumptionPerDeviceChart for improved visual clarity. 2025-05-05 10:34:23 +03:00
06637a16bb Add EnergyConsumptionPerDevice functionality and update related components 2025-05-05 10:32:12 +03:00
696978a78d Injected EnergyConsumptionPerDeviceBloc into AnalyticsPage. 2025-05-05 10:20:43 +03:00
818e4e4d51 Created EnergyConsumptionPerDeviceBloc. 2025-05-05 10:19:52 +03:00
af877d7839 Created EnergyConsumptionPerDeviceService and a fake implementation for it. 2025-05-05 10:15:35 +03:00
a33b1e3f49 uses MediaQuery.sizeOf instead of MediaQuery.of. 2025-05-05 10:02:48 +03:00
c3cce334ab data populator. 2025-05-05 09:56:29 +03:00
947e9e404c left side intervals. 2025-05-05 09:54:17 +03:00
cd8264b6ce Progress on making EnergyManagementView responsive. 2025-05-05 09:47:54 +03:00
7467be6980 Refactored PowerClampPhase to be more readable by extracting widgets into helper private methods to enhance readability. 2025-05-05 09:15:13 +03:00
0353c73dac Moved PowerClampPhase widget to its own file. 2025-05-05 09:12:27 +03:00
a050792f32 extracted PowerClampPhase into its own widget. 2025-05-05 09:11:11 +03:00
464f7b7347 working on responsiveness. 2025-05-04 16:50:28 +03:00
cd54574279 progress towards drawing the phases widgts. 2025-05-04 16:07:51 +03:00
18acae3e85 Added type annotation. 2025-05-04 15:02:27 +03:00
f081a7fc2d implemented header, and power clamp data widgets. 2025-05-04 14:59:49 +03:00
5996ff3928 Extracted fetching energy management data from the widgets themselves to the parent widget AnalyticsEnergyManagementView. 2025-05-04 14:29:35 +03:00
a0d1cb988a moved EnergyConsumptionByPhasesTitle to its own file. 2025-05-04 14:26:52 +03:00
c3ec9000d4 Extracted EnergyConsumptionByPhasesTitle into its own widget to improve readability and assert separation of concerns. 2025-05-04 14:26:38 +03:00
3d6a60b406 load energy consumption by phases from bloc, and made the widget use the said data. 2025-05-04 14:24:53 +03:00
69c9240641 Injected EnergyConsumptionByPhasesBloc into AnalyticsPage. 2025-05-04 14:22:01 +03:00
098013e5c8 Created EnergyConsumptionByPhasesBloc. 2025-05-04 14:20:59 +03:00
11fb9e4894 Abstracted EnergyConsumptionByPhasesService, and created a fake implementation that returns mock data, which also simulates a network delay. 2025-05-04 14:20:52 +03:00
390da9213d Made PhasesEnergyConsumption extend from Equatable. 2025-05-04 14:19:15 +03:00
cae8b029fe Created GetEnergyConsumptionByPhasesParam class. 2025-05-04 14:18:56 +03:00
6b883c8bb3 Implemented Energy Consumption by Phases chart. 2025-05-04 13:22:38 +03:00
08c99bcbcb Renamed EnergyConsumptionByPhasesChart to EnergyConsumptionByPhasesChartBox 2025-05-04 12:02:09 +03:00
f6448d3eff Implement EnergyConsumptionByPhasesChart with structured layout and phase indicators 2025-05-04 12:01:40 +03:00
a657a9a25e Refactor padding and add const constructors for improved performance and consistency in chart widgets 2025-05-04 11:20:47 +03:00
f55fa25bdf Added prefer_const_constructors to analysis_options.yaml. 2025-05-04 11:11:32 +03:00
7242218b2f Textstyles. 2025-05-04 11:10:22 +03:00
e43de3f64c Introduce ChartTitle widget for consistent chart headings in EnergyConsumptionPerDeviceChartBox and TotalEnergyConsumptionChartBox 2025-05-04 11:08:56 +03:00
9c250986b2 Moved EnergyConsumptionPerDeviceDevicesList to its own file. 2025-05-04 10:55:52 +03:00
d8faafd1c0 Extracted and implemented EnergyConsumptionPerDeviceDevicesList. 2025-05-04 10:55:32 +03:00
24c30ddcb5 Refactor chart data generation in EnergyConsumptionPerDeviceChart for improved readability and maintainability 2025-05-04 10:47:39 +03:00
bafd2b4d13 Extracted reusbale logic and ui componenets into a shared helper class for the total energy chart, and energy cosumption per devices, to avoid any code duplication. If another chart required some change, we dont need to edit the helper itself, we can just add out own implementation into the new chart. 2025-05-04 10:46:12 +03:00
56f9b1fc9a Update padding in AnalyticsEnergyManagementView and simplify title visibility in TotalEnergyConsumptionChart 2025-05-04 09:37:11 +03:00
a9cc92ff86 Merge pull request #173 from SyncrowIOT/bugfix/add-space-tree-loading 2025-05-02 22:02:03 +04:00
3c7edae88a added loading widget, till spaces are valid 2025-05-02 21:59:45 +04:00
56c2d11535 Merge pull request #172 from SyncrowIOT/bugfix/pagination-scroll 2025-05-01 13:10:51 +04:00
3aa5bff758 Merge branch 'dev' of https://github.com/SyncrowIOT/web into bugfix/pagination-scroll 2025-05-01 13:10:16 +04:00
28d1e5a5a7 Merge pull request #171 from SyncrowIOT/bugfix/sibling-name 2025-05-01 12:41:22 +04:00
fe036a8190 added validation for name 2025-05-01 12:40:12 +04:00
82e145de9d added spinning indicator 2025-04-30 23:29:48 +04:00
ebeb514a5b Merge pull request #170 from SyncrowIOT:bugfix/fix-issue-in-save
fixed save issue
2025-04-30 22:49:41 +04:00
6b7e02ee53 Merge branch 'dev' of https://github.com/SyncrowIOT/web into bugfix/fix-issue-in-save 2025-04-30 22:48:41 +04:00
b01136b6e9 fixed on save issue 2025-04-30 22:47:54 +04:00
97f8c6c8c9 Create EnergyConsumptionPerDeviceChartBox widget and update imports in AnalyticsEnergyManagementView 2025-04-30 16:54:11 +03:00
6e527503c1 Add missing Divider widget above TotalEnergyConsumptionChart in TotalEnergyConsumptionChartBox 2025-04-30 16:39:07 +03:00
d6ef06c1b3 Simplify widget structure in TotalEnergyConsumptionChart by removing unnecessary FittedBox wrapper around month title text. 2025-04-30 16:07:37 +03:00
c9aaf2580f Refactor TotalEnergyConsumptionChart to accept chartData as a parameter that it takes from TotalEnergyConsumptionBlocand update TotalEnergyConsumptionChartBox to use Bloc for state management. 2025-04-30 15:56:17 +03:00
d9cd5d0438 Injected TotalEnergyConsumptionBloc into AnalyticsPage. 2025-04-30 15:46:05 +03:00
3eb87dfde1 Created TotalEnergyConsumptionBloc and its implementation. 2025-04-30 15:45:07 +03:00
f29ff2551f Rename method getTotalEnergyConsumption to load in TotalEnergyConsumptionService and FakeTotalEnergyConsumptionService for consistency. 2025-04-30 15:34:10 +03:00
67dd59ee9c Add GetTotalEnergyConsumptionParam and FakeTotalEnergyConsumptionService implementations 2025-04-30 15:30:25 +03:00
bb3c3906d1 Add EnergyDataModel and update TotalEnergyConsumptionChart to use it 2025-04-30 15:15:05 +03:00
3873deca90 Created EnergyData model. 2025-04-30 15:09:30 +03:00
9431dd4500 extracted reusable helper methods into global extensions. 2025-04-30 15:02:53 +03:00
63718185e7 Refactor TotalEnergyConsumptionChart to TotalEnergyConsumptionChartBox for improved layout and encapsulation. 2025-04-30 14:59:01 +03:00
1f4e82d567 Enhance TotalEnergyConsumptionChart layout and tooltip functionality. 2025-04-30 14:56:01 +03:00
9f68d171ff progress towards making TotalEnergyConsumptionChart functional and look like the design. 2025-04-30 13:08:28 +03:00
6eba640037 bump-dependencies 2025-04-30 09:48:02 +03:00
7a088074e3 Prepared the layout of all charts. 2025-04-30 09:44:04 +03:00
d8f40badc0 animated tab buttons. 2025-04-30 09:20:02 +03:00
fdd5d0feed rem oved comments and removed copyWith fromAnalyticsTabState because the state object only has one property, so i replaced the state object with AnalyticsPageTab enum. 2025-04-30 09:17:52 +03:00
fb1f79c7bb Made AnalyticsPageTabsAndChildren responsive. 2025-04-30 09:14:30 +03:00
1923ac7014 Progress towards 1493. 2025-04-29 17:00:06 +03:00
148 changed files with 6778 additions and 766 deletions

View File

@ -10,6 +10,7 @@
analyzer:
errors:
constant_identifier_names: ignore
overridden_fields: ignore
include: package:flutter_lints/flutter.yaml
linter:
@ -26,6 +27,7 @@ linter:
rules:
# avoid_print: false # Uncomment to disable the `avoid_print` rule
# prefer_single_quotes: true # Uncomment to enable the `prefer_single_quotes` rule
prefer_const_constructors: true
# Additional information about this file can be found at
# https://dart.dev/guides/language/analysis-options

View File

@ -0,0 +1,3 @@
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M16.25 2.1875H14.6875V1.875C14.6875 1.62636 14.5887 1.3879 14.4129 1.21209C14.2371 1.03627 13.9986 0.9375 13.75 0.9375C13.5014 0.9375 13.2629 1.03627 13.0871 1.21209C12.9113 1.3879 12.8125 1.62636 12.8125 1.875V2.1875H7.1875V1.875C7.1875 1.62636 7.08873 1.3879 6.91291 1.21209C6.7371 1.03627 6.49864 0.9375 6.25 0.9375C6.00136 0.9375 5.7629 1.03627 5.58709 1.21209C5.41127 1.3879 5.3125 1.62636 5.3125 1.875V2.1875H3.75C3.3356 2.1875 2.93817 2.35212 2.64515 2.64515C2.35212 2.93817 2.1875 3.3356 2.1875 3.75V16.25C2.1875 16.6644 2.35212 17.0618 2.64515 17.3549C2.93817 17.6479 3.3356 17.8125 3.75 17.8125H16.25C16.6644 17.8125 17.0618 17.6479 17.3549 17.3549C17.6479 17.0618 17.8125 16.6644 17.8125 16.25V3.75C17.8125 3.3356 17.6479 2.93817 17.3549 2.64515C17.0618 2.35212 16.6644 2.1875 16.25 2.1875ZM5.3125 4.0625C5.3125 4.31114 5.41127 4.5496 5.58709 4.72541C5.7629 4.90123 6.00136 5 6.25 5C6.49864 5 6.7371 4.90123 6.91291 4.72541C7.08873 4.5496 7.1875 4.31114 7.1875 4.0625H12.8125C12.8125 4.31114 12.9113 4.5496 13.0871 4.72541C13.2629 4.90123 13.5014 5 13.75 5C13.9986 5 14.2371 4.90123 14.4129 4.72541C14.5887 4.5496 14.6875 4.31114 14.6875 4.0625H15.9375V5.9375H4.0625V4.0625H5.3125ZM4.0625 15.9375V7.8125H15.9375V15.9375H4.0625Z" fill="#475569"/>
</svg>

After

Width:  |  Height:  |  Size: 1.3 KiB

View File

@ -0,0 +1,12 @@
<svg width="22" height="22" viewBox="0 0 22 22" fill="none" xmlns="http://www.w3.org/2000/svg">
<g clip-path="url(#clip0_7305_15779)">
<path d="M17.0872 11.5142C17.0872 13.2025 16.427 14.8021 15.2211 15.9954C14.0278 17.2014 12.4283 17.8615 10.7399 17.8615C9.05141 17.8615 7.45185 17.2014 6.25856 15.9954C5.05262 14.8021 4.39249 13.2025 4.39249 11.5142C4.39249 9.82574 5.05266 8.22618 6.25856 7.03289C7.45185 5.8269 9.05141 5.16681 10.7399 5.16681C11.8063 5.16681 12.8471 5.43337 13.7866 5.95388L11.2984 8.97523H21.0861L18.6486 0L16.2113 2.97053C14.5737 1.91691 12.6948 1.35835 10.7398 1.35835C8.02314 1.35835 5.47142 2.41197 3.55459 4.32888C1.63765 6.24578 0.583984 8.79747 0.583984 11.5142C0.583984 14.2309 1.63765 16.7825 3.55459 18.6994C5.47146 20.6163 8.0231 21.67 10.7398 21.67C13.4565 21.67 16.0082 20.6163 17.925 18.6994C19.8419 16.7825 20.8956 14.2309 20.8956 11.5142V10.8794H17.0872V11.5142Z" fill="#77DD00"/>
<path d="M17.0876 10.8799H20.8961V11.5146C20.8961 14.2313 19.8424 16.7829 17.9254 18.6998C16.0086 20.6168 13.4569 21.6704 10.7402 21.6704V17.862C12.4287 17.862 14.0282 17.2019 15.2215 15.9959C16.4275 14.8026 17.0876 13.203 17.0876 11.5147V10.8799H17.0876Z" fill="#66BB00"/>
<path d="M13.787 5.95388C12.8475 5.43333 11.8066 5.16681 10.7402 5.16681V1.35835C12.6952 1.35835 14.5741 1.91691 16.2117 2.97057L18.6491 0L21.0866 8.97523H11.2989L13.787 5.95388Z" fill="#66BB00"/>
</g>
<defs>
<clipPath id="clip0_7305_15779">
<rect width="21.67" height="21.67" fill="white"/>
</clipPath>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 1.5 KiB

View File

@ -0,0 +1,56 @@
import 'dart:ui';
import 'package:flutter/material.dart';
class DashedBorderPainter extends CustomPainter {
final double dashWidth;
final double dashSpace;
final Color color;
DashedBorderPainter({
this.dashWidth = 4.0,
this.dashSpace = 2.0,
this.color = Colors.black,
});
@override
void paint(Canvas canvas, Size size) {
final paint = Paint()
..color = color
..strokeWidth = 0.5
..style = PaintingStyle.stroke;
final Path topPath = Path()
..moveTo(0, 0)
..lineTo(size.width, 0);
final Path bottomPath = Path()
..moveTo(0, size.height)
..lineTo(size.width, size.height);
final dashedTopPath = _createDashedPath(topPath, dashWidth, dashSpace);
final dashedBottomPath = _createDashedPath(bottomPath, dashWidth, dashSpace);
canvas.drawPath(dashedTopPath, paint);
canvas.drawPath(dashedBottomPath, paint);
}
Path _createDashedPath(Path source, double dashWidth, double dashSpace) {
final Path dashedPath = Path();
for (PathMetric pathMetric in source.computeMetrics()) {
double distance = 0.0;
while (distance < pathMetric.length) {
final double nextDistance = distance + dashWidth;
dashedPath.addPath(
pathMetric.extractPath(distance, nextDistance),
Offset.zero,
);
distance = nextDistance + dashSpace;
}
}
return dashedPath;
}
@override
bool shouldRepaint(covariant CustomPainter oldDelegate) => false;
}

View File

@ -0,0 +1,6 @@
extension FormatNumberToKwh on num {
String get formatNumberToKwh {
final regExp = RegExp(r'(\d)(?=(\d{3})+$)');
return '${toStringAsFixed(0).replaceAllMapped(regExp, (match) => '${match[1]},')} kWh';
}
}

View File

@ -0,0 +1,19 @@
extension GetMonthNameFromNumber on num {
String get getMonthName {
return switch (this) {
1 => 'JAN',
2 => 'FEB',
3 => 'MAR',
4 => 'APR',
5 => 'MAY',
6 => 'JUN',
7 => 'JUL',
8 => 'AUG',
9 => 'SEP',
10 => 'OCT',
11 => 'NOV',
12 => 'DEC',
_ => 'N/A'
};
}
}

View File

@ -0,0 +1,32 @@
import 'package:equatable/equatable.dart';
import 'package:flutter/widgets.dart';
import 'package:syncrow_web/pages/analytics/models/energy_data_model.dart';
class DeviceEnergyDataModel extends Equatable {
const DeviceEnergyDataModel({
required this.energy,
required this.deviceName,
required this.deviceId,
required this.color,
});
final List<EnergyDataModel> energy;
final String deviceName;
final String deviceId;
final Color color;
@override
List<Object?> get props => [energy, deviceName, deviceId];
factory DeviceEnergyDataModel.fromJson(Map<String, dynamic> json) {
final energy = (json['energy'] as List<dynamic>? ?? [])
.map((e) => EnergyDataModel.fromJson(e))
.toList();
return DeviceEnergyDataModel(
energy: energy,
deviceName: json['device_name'] as String? ?? '',
deviceId: json['device_id'] as String? ?? '',
color: Color(int.parse(json['color'] as String? ?? '0xFF000000')),
);
}
}

View File

@ -0,0 +1,21 @@
import 'package:equatable/equatable.dart';
class EnergyDataModel extends Equatable {
const EnergyDataModel({
required this.date,
required this.value,
});
final DateTime date;
final double value;
factory EnergyDataModel.fromJson(Map<String, dynamic> json) {
return EnergyDataModel(
date: DateTime.parse(json['date'] as String),
value: (json['value'] as num).toDouble(),
);
}
@override
List<Object?> get props => [date, value];
}

View File

@ -0,0 +1,18 @@
import 'package:equatable/equatable.dart';
class Occupacy extends Equatable {
final String date;
final String occupancy;
const Occupacy({required this.date, required this.occupancy});
factory Occupacy.fromJson(Map<String, dynamic> json) {
return Occupacy(
date: json['date'] as String,
occupancy: json['occupancy'] as String,
);
}
@override
List<Object?> get props => [date, occupancy];
}

View File

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

View File

@ -0,0 +1,27 @@
import 'package:equatable/equatable.dart';
class PhasesEnergyConsumption extends Equatable {
final int month;
final double phaseA;
final double phaseB;
final double phaseC;
const PhasesEnergyConsumption({
required this.month,
required this.phaseA,
required this.phaseB,
required this.phaseC,
});
@override
List<Object?> get props => [month, phaseA, phaseB, phaseC];
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(),
);
}
}

View File

@ -0,0 +1,13 @@
class PowerClampEnergyStatus {
final String iconPath;
final String title;
final String value;
final String unit;
const PowerClampEnergyStatus({
required this.iconPath,
required this.title,
required this.value,
required this.unit,
});
}

View File

@ -0,0 +1,24 @@
import 'package:bloc/bloc.dart';
import 'package:equatable/equatable.dart';
part 'analytics_date_picker_event.dart';
part 'analytics_date_picker_state.dart';
class AnalyticsDatePickerBloc
extends Bloc<AnalyticsDatePickerEvent, AnalyticsDatePickerState> {
AnalyticsDatePickerBloc() : super(AnalyticsDatePickerState()) {
on<UpdateAnalyticsDatePickerEvent>(_onUpdateAnalyticsDatePickerEvent);
}
void _onUpdateAnalyticsDatePickerEvent(
UpdateAnalyticsDatePickerEvent event,
Emitter<AnalyticsDatePickerState> emit,
) {
emit(
state.copyWith(
monthlyDate: event.montlyDate ?? state.monthlyDate,
yearlyDate: event.yearlyDate ?? state.yearlyDate,
),
);
}
}

View File

@ -0,0 +1,18 @@
part of 'analytics_date_picker_bloc.dart';
sealed class AnalyticsDatePickerEvent extends Equatable {
const AnalyticsDatePickerEvent();
@override
List<Object?> get props => [];
}
final class UpdateAnalyticsDatePickerEvent extends AnalyticsDatePickerEvent {
const UpdateAnalyticsDatePickerEvent({this.montlyDate, this.yearlyDate});
final DateTime? montlyDate;
final DateTime? yearlyDate;
@override
List<Object?> get props => [montlyDate, yearlyDate];
}

View File

@ -0,0 +1,25 @@
part of 'analytics_date_picker_bloc.dart';
final class AnalyticsDatePickerState extends Equatable {
AnalyticsDatePickerState({
DateTime? monthlyDate,
DateTime? yearlyDate,
}) : monthlyDate = monthlyDate ?? DateTime.now(),
yearlyDate = yearlyDate ?? DateTime.now();
final DateTime monthlyDate;
final DateTime yearlyDate;
AnalyticsDatePickerState copyWith({
DateTime? monthlyDate,
DateTime? yearlyDate,
}) {
return AnalyticsDatePickerState(
monthlyDate: monthlyDate ?? this.monthlyDate,
yearlyDate: yearlyDate ?? this.yearlyDate,
);
}
@override
List<Object?> get props => [monthlyDate, yearlyDate];
}

View File

@ -0,0 +1,18 @@
import 'package:bloc/bloc.dart';
import 'package:equatable/equatable.dart';
import 'package:syncrow_web/pages/analytics/modules/analytics/enums/analytics_page_tab.dart';
part 'analytics_tab_event.dart';
class AnalyticsTabBloc extends Bloc<AnalyticsTabEvent, AnalyticsPageTab> {
AnalyticsTabBloc() : super(AnalyticsPageTab.energyManagement) {
on<UpdateAnalyticsTabEvent>(_onUpdateAnalyticsTabEvent);
}
void _onUpdateAnalyticsTabEvent(
UpdateAnalyticsTabEvent event,
Emitter<AnalyticsPageTab> emit,
) {
emit(event.analyticsTab);
}
}

View File

@ -0,0 +1,17 @@
part of 'analytics_tab_bloc.dart';
sealed class AnalyticsTabEvent extends Equatable {
const AnalyticsTabEvent();
@override
List<Object> get props => [];
}
class UpdateAnalyticsTabEvent extends AnalyticsTabEvent {
const UpdateAnalyticsTabEvent(this.analyticsTab);
final AnalyticsPageTab analyticsTab;
@override
List<Object> get props => [analyticsTab];
}

View File

@ -0,0 +1,22 @@
import 'package:flutter/material.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';
enum AnalyticsPageTab {
energyManagement(
title: 'Energy Management',
child: AnalyticsEnergyManagementView(),
),
occupancy(
title: 'Occupancy',
child: AnalyticsOccupancyView(),
);
const AnalyticsPageTab({
required this.title,
required this.child,
});
final Widget child;
final String title;
}

View File

@ -0,0 +1,22 @@
import 'package:flutter/material.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';
abstract class AnalyticsDataLoadingStrategy {
void onCommunitySelected(
BuildContext context,
CommunityModel community,
List<SpaceModel> spaces,
);
void onSpaceSelected(
BuildContext context,
CommunityModel community,
SpaceModel space,
);
void onChildSpaceSelected(
BuildContext context,
CommunityModel community,
SpaceModel child,
);
void clearData(BuildContext context);
}

View File

@ -0,0 +1,14 @@
import 'package:syncrow_web/pages/analytics/modules/analytics/enums/analytics_page_tab.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';
abstract final class AnalyticsDataLoadingStrategyFactory {
const AnalyticsDataLoadingStrategyFactory._();
static AnalyticsDataLoadingStrategy getStrategy(AnalyticsPageTab tab) {
return switch (tab) {
AnalyticsPageTab.energyManagement => EnergyManagementDataLoadingStrategy(),
AnalyticsPageTab.occupancy => OccupancyDataLoadingStrategy(),
};
}
}

View File

@ -0,0 +1,65 @@
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:syncrow_web/pages/analytics/modules/analytics/strategies/analytics_data_loading_strategy.dart';
import 'package:syncrow_web/pages/analytics/modules/energy_management/helpers/fetch_energy_management_data_helper.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';
class EnergyManagementDataLoadingStrategy implements AnalyticsDataLoadingStrategy {
@override
void onCommunitySelected(
BuildContext context,
CommunityModel community,
List<SpaceModel> spaces,
) {
context.read<SpaceTreeBloc>().add(
OnCommunitySelected(
community.uuid,
spaces,
),
);
FetchEnergyManagementDataHelper.loadEnergyManagementData(
context,
communityId: community.uuid,
spaceId: spaces.isNotEmpty ? spaces.first.uuid ?? '' : '',
);
}
@override
void onSpaceSelected(
BuildContext context,
CommunityModel community,
SpaceModel space,
) {
context.read<SpaceTreeBloc>().add(
OnSpaceSelected(
community,
space.uuid ?? '',
space.children,
),
);
FetchEnergyManagementDataHelper.loadEnergyManagementData(
context,
communityId: community.uuid,
spaceId: space.uuid ?? '',
);
}
@override
void onChildSpaceSelected(
BuildContext context,
CommunityModel community,
SpaceModel child,
) {
// Do nothing
}
@override
void clearData(BuildContext context) {
context.read<SpaceTreeBloc>().add(const SpaceTreeClearSelectionEvent());
FetchEnergyManagementDataHelper.clearAllData(context);
}
}

View File

@ -0,0 +1,71 @@
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:syncrow_web/pages/analytics/modules/analytics/strategies/analytics_data_loading_strategy.dart';
import 'package:syncrow_web/pages/analytics/modules/occupancy/helpers/fetch_occupancy_data_helper.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';
class OccupancyDataLoadingStrategy implements AnalyticsDataLoadingStrategy {
@override
void onCommunitySelected(
BuildContext context,
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 ?? '' : '',
);
}
@override
void onSpaceSelected(
BuildContext context,
CommunityModel community,
SpaceModel space,
) {
final spaceTreeBloc = context.read<SpaceTreeBloc>();
final selectedSpacesIds = spaceTreeBloc.state.selectedSpaces;
final isSpaceSelected = selectedSpacesIds.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 ?? '', []));
}
FetchOccupancyDataHelper.loadOccupancyData(
context,
communityId: community.uuid,
spaceId: space.uuid ?? '',
);
}
@override
void onChildSpaceSelected(
BuildContext context,
CommunityModel community,
SpaceModel child,
) {
// Do nothing
}
@override
void clearData(BuildContext context) {
context.read<SpaceTreeBloc>().add(const SpaceTreeClearSelectionEvent());
// FetchOccupancyDataHelper.clearAllData(context);
}
}

View File

@ -1,19 +1,91 @@
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_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_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';
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/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/power_clamp_info/remote_power_clamp_info_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';
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 {
const AnalyticsPage({super.key});
@override
Widget build(BuildContext context) {
return MultiBlocProvider(
providers: [
BlocProvider<AnalyticsTabBloc>(
create: (context) => AnalyticsTabBloc(),
),
BlocProvider(
create: (context) => TotalEnergyConsumptionBloc(
RemoteTotalEnergyConsumptionService(HTTPService()),
),
),
BlocProvider(
create: (context) => EnergyConsumptionByPhasesBloc(
FakeEnergyConsumptionByPhasesService(),
),
),
BlocProvider(
create: (context) => EnergyConsumptionPerDeviceBloc(
FakeEnergyConsumptionPerDeviceService(),
),
),
BlocProvider(
create: (context) => PowerClampInfoBloc(
RemotePowerClampInfoService(HTTPService()),
),
),
BlocProvider<RealtimeDeviceChangesBloc>(
create: (context) => RealtimeDeviceChangesBloc(
FirebaseRealtimeDeviceService(),
),
),
BlocProvider(create: (context) => OccupancyBloc(FakeOccupacyService())),
BlocProvider(
create: (context) => OccupancyHeatMapBloc(FakeOccupancyHeatMapService()),
),
BlocProvider(create: (context) => AnalyticsDatePickerBloc()),
],
child: const AnalyticsPageForm(),
);
}
}
class AnalyticsPageForm extends StatelessWidget {
const AnalyticsPageForm({super.key});
@override
Widget build(BuildContext context) {
return WebScaffold(
rightBody: const NavigateHomeGridView(),
appBarTitle: Text(
'Syncrow Analytics',
style: ResponsiveTextTheme.of(context).deviceManagementTitle,
),
scaffoldBody: const Center(
child: Text('AnalyticsPage is Working!'),
enableMenuSidebar: false,
scaffoldBody: const Row(
children: [
AnalyticsCommunitiesSidebar(),
Expanded(flex: 5, child: AnalyticsPageTabsAndChildren()),
],
),
);
}

View File

@ -0,0 +1,29 @@
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_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/strategies/analytics_data_loading_strategy_factory.dart';
import 'package:syncrow_web/pages/analytics/modules/analytics/widgets/sidebar/analytics_space_tree_view.dart';
class AnalyticsCommunitiesSidebar extends StatelessWidget {
const AnalyticsCommunitiesSidebar({super.key});
@override
Widget build(BuildContext context) {
final selectedTab = context.watch<AnalyticsTabBloc>().state;
final strategy = AnalyticsDataLoadingStrategyFactory.getStrategy(selectedTab);
return Expanded(
child: AnalyticsSpaceTreeView(
onSelectCommunity: (community, spaces) {
strategy.onCommunitySelected(context, community, spaces);
},
onSelectSpace: (community, space) {
strategy.onSpaceSelected(context, community, space);
},
onSelectChildSpace: (community, child) {
strategy.onChildSpaceSelected(context, community, child);
},
),
);
}
}

View File

@ -0,0 +1,96 @@
import 'package:flutter/material.dart';
import 'package:flutter_svg/svg.dart';
import 'package:intl/intl.dart';
import 'package:syncrow_web/pages/analytics/modules/analytics/widgets/month_picker_widget.dart';
import 'package:syncrow_web/pages/analytics/modules/analytics/widgets/year_picker_widget.dart';
import 'package:syncrow_web/utils/color_manager.dart';
import 'package:syncrow_web/utils/constants/assets.dart';
enum DatePickerType { month, year }
class AnalyticsDateFilterButton extends StatefulWidget {
const AnalyticsDateFilterButton({
required this.selectedDate,
required this.onDateSelected,
this.datePickerType = DatePickerType.month,
super.key,
});
final DateTime selectedDate;
final void Function(DateTime)? onDateSelected;
final DatePickerType datePickerType;
static final _color = ColorsManager.blackColor.withValues(alpha: 0.8);
@override
State<AnalyticsDateFilterButton> createState() =>
_AnalyticsDateFilterButtonState();
}
class _AnalyticsDateFilterButtonState extends State<AnalyticsDateFilterButton> {
@override
Widget build(BuildContext context) {
return TextButton.icon(
style: TextButton.styleFrom(
foregroundColor: AnalyticsDateFilterButton._color,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(16),
side: const BorderSide(
color: ColorsManager.greyColor,
width: 1,
),
),
backgroundColor: ColorsManager.transparentColor,
padding: const EdgeInsets.symmetric(
horizontal: 32,
vertical: 16,
),
),
icon: SvgPicture.asset(
Assets.blankCalendar,
height: 20,
width: 20,
colorFilter:
ColorFilter.mode(AnalyticsDateFilterButton._color, BlendMode.srcIn),
),
label: Text(
_formatDate(widget.selectedDate),
style: const TextStyle(
fontWeight: FontWeight.w700,
),
),
onPressed: () {
showDialog(
context: context,
builder: (_) {
return switch (widget.datePickerType) {
DatePickerType.month => MonthPickerWidget(
selectedDate: widget.selectedDate,
onDateSelected: (value) {
widget.onDateSelected?.call(value);
},
),
DatePickerType.year => YearPickerWidget(
selectedDate: widget.selectedDate,
onDateSelected: (value) {
widget.onDateSelected?.call(value);
},
),
};
},
);
},
);
}
String _formatDate(DateTime? date) {
final formatterBasedOnDatePickerType = switch (widget.datePickerType) {
DatePickerType.month => DateFormat('MMMM yyyy'),
DatePickerType.year => DateFormat('yyyy'),
};
final formattedDate = formatterBasedOnDatePickerType.format(
date ?? DateTime.now(),
);
return formattedDate;
}
}

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/analytics/blocs/analytics_tab/analytics_tab_bloc.dart';
import 'package:syncrow_web/pages/analytics/modules/analytics/enums/analytics_page_tab.dart';
import 'package:syncrow_web/pages/analytics/modules/analytics/strategies/analytics_data_loading_strategy_factory.dart';
import 'package:syncrow_web/utils/color_manager.dart';
class AnalyticsPageTabButton extends StatelessWidget {
const AnalyticsPageTabButton({
super.key,
required this.tab,
required this.isSelected,
});
final AnalyticsPageTab tab;
final bool isSelected;
@override
Widget build(BuildContext context) {
return TextButton(
onPressed: () {
AnalyticsDataLoadingStrategyFactory.getStrategy(tab).clearData(context);
context.read<AnalyticsTabBloc>().add(
UpdateAnalyticsTabEvent(tab),
);
},
child: Text(
tab.title,
textAlign: TextAlign.center,
maxLines: 1,
softWrap: false,
overflow: TextOverflow.ellipsis,
style: TextStyle(
fontWeight: isSelected ? FontWeight.w700 : FontWeight.w400,
fontSize: 16,
color:
isSelected ? ColorsManager.slidingBlueColor : ColorsManager.textGray,
),
),
);
}
}

View File

@ -0,0 +1,109 @@
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_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_tab/analytics_tab_bloc.dart';
import 'package:syncrow_web/pages/analytics/modules/analytics/enums/analytics_page_tab.dart';
import 'package:syncrow_web/pages/analytics/modules/analytics/widgets/analytics_date_filter_button.dart';
import 'package:syncrow_web/pages/analytics/modules/analytics/widgets/analytics_page_tab_button.dart';
import 'package:syncrow_web/pages/analytics/modules/energy_management/helpers/fetch_energy_management_data_helper.dart';
import 'package:syncrow_web/pages/space_tree/bloc/space_tree_bloc.dart';
import 'package:syncrow_web/utils/style.dart';
class AnalyticsPageTabsAndChildren extends StatelessWidget {
const AnalyticsPageTabsAndChildren({super.key});
@override
Widget build(BuildContext context) {
final spaceTreeState = context.read<SpaceTreeBloc>().state;
return BlocBuilder<AnalyticsTabBloc, AnalyticsPageTab>(
buildWhen: (previous, current) => previous != current,
builder: (context, selectedTab) => Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: [
Expanded(
child: Container(
decoration: subSectionContainerDecoration,
padding: const EdgeInsets.symmetric(horizontal: 32),
child: Row(
mainAxisSize: MainAxisSize.min,
mainAxisAlignment: MainAxisAlignment.start,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Expanded(
flex: 4,
child: FittedBox(
alignment: AlignmentDirectional.centerStart,
fit: BoxFit.scaleDown,
child: Row(
spacing: 32,
mainAxisSize: MainAxisSize.min,
mainAxisAlignment: MainAxisAlignment.start,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
...AnalyticsPageTab.values.map(
(tab) => _buildAnimation(
child: AnalyticsPageTabButton(
key: ValueKey(selectedTab),
tab: tab,
isSelected: tab == selectedTab,
),
),
),
],
),
),
),
const Spacer(),
Visibility(
key: ValueKey(selectedTab),
visible: selectedTab == AnalyticsPageTab.energyManagement,
child: Expanded(
flex: 2,
child: FittedBox(
fit: BoxFit.scaleDown,
alignment: AlignmentDirectional.centerEnd,
child: AnalyticsDateFilterButton(
onDateSelected: (DateTime value) {
context.read<AnalyticsDatePickerBloc>().add(
UpdateAnalyticsDatePickerEvent(montlyDate: value),
);
FetchEnergyManagementDataHelper.loadEnergyManagementData(
context,
selectedDate: value,
communityId:
spaceTreeState.selectedCommunities.firstOrNull ??
'',
spaceId:
spaceTreeState.selectedSpaces.firstOrNull ?? '',
);
},
selectedDate: context
.watch<AnalyticsDatePickerBloc>()
.state
.monthlyDate,
),
),
),
),
],
),
),
),
Expanded(
flex: 8,
child: _buildAnimation(child: selectedTab.child),
),
],
),
);
}
Widget _buildAnimation({required Widget child}) {
return AnimatedSwitcher(
switchInCurve: Curves.easeIn,
duration: const Duration(milliseconds: 200),
child: child,
);
}
}

View File

@ -0,0 +1,194 @@
import 'package:flutter/material.dart';
import 'package:syncrow_web/utils/color_manager.dart';
import 'package:syncrow_web/utils/extension/build_context_x.dart';
class MonthPickerWidget extends StatefulWidget {
const MonthPickerWidget({
super.key,
required this.selectedDate,
required this.onDateSelected,
});
final DateTime selectedDate;
final ValueChanged<DateTime>? onDateSelected;
@override
State<MonthPickerWidget> createState() => _MonthPickerWidgetState();
}
class _MonthPickerWidgetState extends State<MonthPickerWidget> {
late int _currentYear;
int? _selectedMonth;
static const _monthNames = [
"January",
"February",
"March",
"April",
"May",
"June",
"July",
"August",
"September",
"October",
"November",
"December",
];
@override
void initState() {
super.initState();
_currentYear = widget.selectedDate.year;
_selectedMonth = widget.selectedDate.month - 1;
}
@override
Widget build(BuildContext context) {
return Dialog(
backgroundColor: Theme.of(context).colorScheme.surface,
child: Container(
padding: const EdgeInsetsDirectional.all(20),
width: 320,
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
_buildYearSelector(),
_buildMonthsGrid(),
const SizedBox(height: 20),
Row(
spacing: 12,
mainAxisAlignment: MainAxisAlignment.end,
children: [
FilledButton(
onPressed: () => Navigator.pop(context),
style: FilledButton.styleFrom(
fixedSize: const Size(106, 40),
backgroundColor: const Color(0xFFEDF2F7),
padding: const EdgeInsetsDirectional.symmetric(
vertical: 10,
horizontal: 16,
),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(20),
),
),
child: Text(
'Cancel',
style: context.textTheme.titleSmall?.copyWith(
fontSize: 14,
fontWeight: FontWeight.w600,
color: ColorsManager.grey700,
),
),
),
FilledButton(
onPressed: () {
Navigator.pop(context);
final date = DateTime(
_currentYear,
_selectedMonth! + 1,
);
widget.onDateSelected?.call(date);
},
style: FilledButton.styleFrom(
fixedSize: const Size(106, 40),
backgroundColor: ColorsManager.vividBlue.withValues(
alpha: 0.7,
),
padding: const EdgeInsetsDirectional.symmetric(
vertical: 10,
horizontal: 16,
),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(20),
),
),
child: Text(
'Done',
style: context.textTheme.titleSmall?.copyWith(
fontSize: 14,
fontWeight: FontWeight.w600,
color: ColorsManager.whiteColors,
),
),
),
],
),
],
),
),
);
}
Row _buildYearSelector() {
return Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(
'$_currentYear',
style: context.textTheme.titleSmall?.copyWith(
fontSize: 14,
fontWeight: FontWeight.w500,
color: ColorsManager.grey700,
),
),
const Spacer(),
IconButton(
onPressed: () => setState(() => _currentYear = _currentYear - 1),
icon: const Icon(
Icons.chevron_left,
color: ColorsManager.grey700,
),
),
IconButton(
onPressed: () => setState(() => _currentYear = _currentYear + 1),
icon: const Icon(
Icons.chevron_right,
color: ColorsManager.grey700,
),
),
],
);
}
Widget _buildMonthsGrid() {
return GridView.builder(
shrinkWrap: true,
itemCount: 12,
physics: const NeverScrollableScrollPhysics(),
padding: const EdgeInsets.all(8),
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 3,
childAspectRatio: 2.5,
mainAxisSpacing: 8,
mainAxisExtent: 30,
),
itemBuilder: (context, index) {
final isSelected = _selectedMonth == index;
return InkWell(
onTap: () => setState(() => _selectedMonth = index),
child: Container(
alignment: Alignment.center,
decoration: BoxDecoration(
color: isSelected
? ColorsManager.vividBlue.withValues(alpha: 0.7)
: 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
: ColorsManager.blackColor.withValues(alpha: 0.8),
fontWeight: FontWeight.w500,
),
),
),
);
},
);
}
}

View File

@ -0,0 +1,188 @@
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:syncrow_web/common/widgets/search_bar.dart';
import 'package:syncrow_web/common/widgets/sidebar_communities_list.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/space_tree/bloc/space_tree_state.dart';
import 'package:syncrow_web/pages/space_tree/view/custom_expansion.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';
import 'package:syncrow_web/utils/color_manager.dart';
import 'package:syncrow_web/utils/extension/build_context_x.dart';
class AnalyticsSpaceTreeView extends StatefulWidget {
const AnalyticsSpaceTreeView({
super.key,
this.onSelectCommunity,
this.onSelectSpace,
this.onSelectChildSpace,
});
final void Function(
CommunityModel community,
List<SpaceModel> spaces,
)? onSelectCommunity;
final void Function(
CommunityModel community,
SpaceModel space,
)? onSelectSpace;
final void Function(
CommunityModel community,
SpaceModel child,
)? onSelectChildSpace;
@override
State<AnalyticsSpaceTreeView> createState() => _AnalyticsSpaceTreeViewState();
}
class _AnalyticsSpaceTreeViewState extends State<AnalyticsSpaceTreeView> {
late final ScrollController _scrollController;
@override
void initState() {
_scrollController = ScrollController();
super.initState();
}
@override
void dispose() {
_scrollController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return BlocBuilder<SpaceTreeBloc, SpaceTreeState>(builder: (context, state) {
final communities = state.searchQuery.isNotEmpty
? state.filteredCommunity
: state.communityList;
return Container(
height: MediaQuery.sizeOf(context).height,
decoration: const BoxDecoration(color: ColorsManager.whiteColors),
child: state is SpaceTreeLoadingState
? const Center(child: CircularProgressIndicator())
: Column(
children: [
Container(
alignment: AlignmentDirectional.centerStart,
padding: const EdgeInsets.all(24),
child: DefaultTextStyle(
style: context.textTheme.titleMedium!.copyWith(
color: ColorsManager.blackColor,
fontSize: 20,
),
child: const Text('Communities'),
),
),
CustomSearchBar(
onSearchChanged: (query) => context.read<SpaceTreeBloc>().add(
SearchQueryEvent(query),
),
),
const SizedBox(height: 16),
Expanded(
child: state.isSearching
? const Center(child: CircularProgressIndicator())
: SidebarCommunitiesList(
onScrollToEnd: () {
if (!state.paginationIsLoading) {
context.read<SpaceTreeBloc>().add(
PaginationEvent(
state.paginationModel,
state.communityList,
),
);
}
},
scrollController: _scrollController,
communities: communities,
itemBuilder: (context, index) {
return CustomExpansionTileSpaceTree(
title: communities[index].name,
isSelected: state.selectedCommunities
.contains(communities[index].uuid),
isSoldCheck: state.selectedCommunities
.contains(communities[index].uuid),
onExpansionChanged: () =>
context.read<SpaceTreeBloc>().add(
OnCommunityExpanded(
communities[index].uuid,
),
),
isExpanded: state.expandedCommunities.contains(
communities[index].uuid,
),
onItemSelected: () => widget.onSelectCommunity?.call(
communities[index],
communities[index].spaces,
),
children: communities[index].spaces.map(
(space) {
return CustomExpansionTileSpaceTree(
title: space.name,
isExpanded:
state.expandedSpaces.contains(space.uuid),
onItemSelected: () =>
widget.onSelectSpace?.call(
communities[index],
space,
),
onExpansionChanged: () =>
context.read<SpaceTreeBloc>().add(
OnSpaceExpanded(
communities[index].uuid,
space.uuid ?? '',
),
),
isSelected: state.selectedSpaces
.contains(space.uuid) ||
state.soldCheck.contains(space.uuid),
isSoldCheck:
state.soldCheck.contains(space.uuid),
children: _buildNestedSpaces(
context,
state,
space,
communities[index],
),
);
},
).toList(),
);
},
),
),
if (state.paginationIsLoading) const CircularProgressIndicator(),
],
),
);
});
}
List<Widget> _buildNestedSpaces(
BuildContext context,
SpaceTreeState state,
SpaceModel space,
CommunityModel community,
) {
return space.children.map((child) {
return CustomExpansionTileSpaceTree(
isSelected: state.selectedSpaces.contains(child.uuid) ||
state.soldCheck.contains(child.uuid),
isSoldCheck: state.soldCheck.contains(child.uuid),
title: child.name,
isExpanded: state.expandedSpaces.contains(child.uuid),
onItemSelected: () {
widget.onSelectChildSpace?.call(community, child);
},
onExpansionChanged: () {
context.read<SpaceTreeBloc>().add(
OnSpaceExpanded(community.uuid, child.uuid ?? ''),
);
},
children: _buildNestedSpaces(context, state, child, community),
);
}).toList();
}
}

View File

@ -0,0 +1,147 @@
import 'package:flutter/material.dart';
import 'package:syncrow_web/utils/color_manager.dart';
import 'package:syncrow_web/utils/extension/build_context_x.dart';
class YearPickerWidget extends StatefulWidget {
const YearPickerWidget({
super.key,
required this.selectedDate,
required this.onDateSelected,
});
final DateTime selectedDate;
final ValueChanged<DateTime>? onDateSelected;
@override
State<YearPickerWidget> createState() => _YearPickerWidgetState();
}
class _YearPickerWidgetState extends State<YearPickerWidget> {
late int _currentYear;
static final years = List.generate(
DateTime.now().year - 2020 + 1,
(index) => (2020 + index),
);
@override
void initState() {
super.initState();
_currentYear = widget.selectedDate.year;
}
@override
Widget build(BuildContext context) {
return Dialog(
backgroundColor: Theme.of(context).colorScheme.surface,
child: Container(
padding: const EdgeInsetsDirectional.all(20),
width: 320,
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
_buildMonthsGrid(),
const SizedBox(height: 20),
Row(
spacing: 12,
mainAxisAlignment: MainAxisAlignment.end,
children: [
FilledButton(
onPressed: () => Navigator.pop(context),
style: FilledButton.styleFrom(
fixedSize: const Size(106, 40),
backgroundColor: const Color(0xFFEDF2F7),
padding: const EdgeInsetsDirectional.symmetric(
vertical: 10,
horizontal: 16,
),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(20),
),
),
child: Text(
'Cancel',
style: context.textTheme.titleSmall?.copyWith(
fontSize: 14,
fontWeight: FontWeight.w600,
color: ColorsManager.grey700,
),
),
),
FilledButton(
onPressed: () {
Navigator.pop(context);
final date = DateTime(_currentYear);
widget.onDateSelected?.call(date);
},
style: FilledButton.styleFrom(
fixedSize: const Size(106, 40),
backgroundColor: ColorsManager.vividBlue.withValues(
alpha: 0.7,
),
padding: const EdgeInsetsDirectional.symmetric(
vertical: 10,
horizontal: 16,
),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(20),
),
),
child: Text(
'Done',
style: context.textTheme.titleSmall?.copyWith(
fontSize: 14,
fontWeight: FontWeight.w600,
color: ColorsManager.whiteColors,
),
),
),
],
),
],
),
),
);
}
Widget _buildMonthsGrid() {
return GridView.builder(
shrinkWrap: true,
itemCount: years.length,
physics: const NeverScrollableScrollPhysics(),
padding: const EdgeInsets.all(8),
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 3,
childAspectRatio: 2.5,
mainAxisSpacing: 8,
mainAxisExtent: 30,
),
itemBuilder: (context, index) {
final isSelected = _currentYear == years[index];
return InkWell(
onTap: () => setState(() => _currentYear = years[index]),
child: Container(
alignment: Alignment.center,
decoration: BoxDecoration(
color: isSelected
? 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

@ -0,0 +1,50 @@
import 'package:bloc/bloc.dart';
import 'package:equatable/equatable.dart';
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';
part 'energy_consumption_by_phases_event.dart';
part 'energy_consumption_by_phases_state.dart';
class EnergyConsumptionByPhasesBloc
extends Bloc<EnergyConsumptionByPhasesEvent, EnergyConsumptionByPhasesState> {
EnergyConsumptionByPhasesBloc(
this._energyConsumptionByPhasesService,
) : super(const EnergyConsumptionByPhasesState()) {
on<LoadEnergyConsumptionByPhasesEvent>(_onLoadEnergyConsumptionByPhasesEvent);
on<ClearEnergyConsumptionByPhasesEvent>(_onClearEnergyConsumptionByPhasesEvent);
}
final EnergyConsumptionByPhasesService _energyConsumptionByPhasesService;
Future<void> _onLoadEnergyConsumptionByPhasesEvent(
LoadEnergyConsumptionByPhasesEvent event,
Emitter<EnergyConsumptionByPhasesState> emit,
) async {
emit(state.copyWith(status: EnergyConsumptionByPhasesStatus.loading));
try {
final chartData = await _energyConsumptionByPhasesService.load(event.param);
emit(
state.copyWith(
status: EnergyConsumptionByPhasesStatus.loaded,
chartData: chartData,
),
);
} catch (e) {
emit(
state.copyWith(
status: EnergyConsumptionByPhasesStatus.failure,
errorMessage: e.toString(),
),
);
}
}
void _onClearEnergyConsumptionByPhasesEvent(
ClearEnergyConsumptionByPhasesEvent event,
Emitter<EnergyConsumptionByPhasesState> emit,
) async {
emit(const EnergyConsumptionByPhasesState());
}
}

View File

@ -0,0 +1,23 @@
part of 'energy_consumption_by_phases_bloc.dart';
sealed class EnergyConsumptionByPhasesEvent extends Equatable {
const EnergyConsumptionByPhasesEvent();
@override
List<Object> get props => [];
}
class LoadEnergyConsumptionByPhasesEvent extends EnergyConsumptionByPhasesEvent {
const LoadEnergyConsumptionByPhasesEvent({
required this.param,
});
final GetEnergyConsumptionByPhasesParam param;
@override
List<Object> get props => [param];
}
final class ClearEnergyConsumptionByPhasesEvent extends EnergyConsumptionByPhasesEvent {
const ClearEnergyConsumptionByPhasesEvent();
}

View File

@ -0,0 +1,35 @@
part of 'energy_consumption_by_phases_bloc.dart';
enum EnergyConsumptionByPhasesStatus {
initial,
loading,
loaded,
failure,
}
final class EnergyConsumptionByPhasesState extends Equatable {
const EnergyConsumptionByPhasesState({
this.status = EnergyConsumptionByPhasesStatus.initial,
this.chartData = const <PhasesEnergyConsumption>[],
this.errorMessage,
});
final List<PhasesEnergyConsumption> chartData;
final EnergyConsumptionByPhasesStatus status;
final String? errorMessage;
EnergyConsumptionByPhasesState copyWith({
List<PhasesEnergyConsumption>? chartData,
EnergyConsumptionByPhasesStatus? status,
String? errorMessage,
}) {
return EnergyConsumptionByPhasesState(
chartData: chartData ?? this.chartData,
status: status ?? this.status,
errorMessage: errorMessage ?? this.errorMessage,
);
}
@override
List<Object?> get props => [chartData, status, errorMessage];
}

View File

@ -0,0 +1,50 @@
import 'package:bloc/bloc.dart';
import 'package:equatable/equatable.dart';
import 'package:syncrow_web/pages/analytics/models/device_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';
part 'energy_consumption_per_device_event.dart';
part 'energy_consumption_per_device_state.dart';
class EnergyConsumptionPerDeviceBloc
extends Bloc<EnergyConsumptionPerDeviceEvent, EnergyConsumptionPerDeviceState> {
EnergyConsumptionPerDeviceBloc(
this._energyConsumptionPerDeviceService,
) : super(const EnergyConsumptionPerDeviceState()) {
on<LoadEnergyConsumptionPerDeviceEvent>(_onLoadEnergyConsumptionPerDeviceEvent);
on<ClearEnergyConsumptionPerDeviceEvent>(_onClearEnergyConsumptionPerDeviceEvent);
}
final EnergyConsumptionPerDeviceService _energyConsumptionPerDeviceService;
Future<void> _onLoadEnergyConsumptionPerDeviceEvent(
LoadEnergyConsumptionPerDeviceEvent event,
Emitter<EnergyConsumptionPerDeviceState> emit,
) async {
emit(state.copyWith(status: EnergyConsumptionPerDeviceStatus.loading));
try {
final chartData = await _energyConsumptionPerDeviceService.load(event.param);
emit(
state.copyWith(
status: EnergyConsumptionPerDeviceStatus.loaded,
chartData: chartData,
),
);
} catch (e) {
emit(
state.copyWith(
status: EnergyConsumptionPerDeviceStatus.failure,
errorMessage: e.toString(),
),
);
}
}
void _onClearEnergyConsumptionPerDeviceEvent(
ClearEnergyConsumptionPerDeviceEvent event,
Emitter<EnergyConsumptionPerDeviceState> emit,
) async {
emit(const EnergyConsumptionPerDeviceState());
}
}

View File

@ -0,0 +1,23 @@
part of 'energy_consumption_per_device_bloc.dart';
sealed class EnergyConsumptionPerDeviceEvent extends Equatable {
const EnergyConsumptionPerDeviceEvent();
@override
List<Object> get props => [];
}
final class LoadEnergyConsumptionPerDeviceEvent
extends EnergyConsumptionPerDeviceEvent {
const LoadEnergyConsumptionPerDeviceEvent(this.param);
final GetEnergyConsumptionPerDeviceParam param;
@override
List<Object> get props => [param];
}
final class ClearEnergyConsumptionPerDeviceEvent
extends EnergyConsumptionPerDeviceEvent {
const ClearEnergyConsumptionPerDeviceEvent();
}

View File

@ -0,0 +1,30 @@
part of 'energy_consumption_per_device_bloc.dart';
enum EnergyConsumptionPerDeviceStatus { initial, loading, loaded, failure }
final class EnergyConsumptionPerDeviceState extends Equatable {
const EnergyConsumptionPerDeviceState({
this.status = EnergyConsumptionPerDeviceStatus.initial,
this.chartData = const <DeviceEnergyDataModel>[],
this.errorMessage,
});
final List<DeviceEnergyDataModel> chartData;
final EnergyConsumptionPerDeviceStatus status;
final String? errorMessage;
EnergyConsumptionPerDeviceState copyWith({
List<DeviceEnergyDataModel>? chartData,
EnergyConsumptionPerDeviceStatus? status,
String? errorMessage,
}) {
return EnergyConsumptionPerDeviceState(
chartData: chartData ?? this.chartData,
status: status ?? this.status,
errorMessage: errorMessage ?? this.errorMessage,
);
}
@override
List<Object?> get props => [chartData, status, errorMessage];
}

View File

@ -0,0 +1,63 @@
import 'package:bloc/bloc.dart';
import 'package:equatable/equatable.dart';
import 'package:syncrow_web/pages/analytics/services/power_clamp_info/power_clamp_info_service.dart';
import 'package:syncrow_web/pages/device_managment/all_devices/models/device_status.dart';
import 'package:syncrow_web/pages/device_managment/power_clamp/models/power_clamp_model.dart';
part 'power_clamp_info_event.dart';
part 'power_clamp_info_state.dart';
class PowerClampInfoBloc extends Bloc<PowerClampInfoEvent, PowerClampInfoState> {
PowerClampInfoBloc(
this._powerClampInfoService,
) : super(const PowerClampInfoState()) {
on<LoadPowerClampInfoEvent>(_onLoadPowerClampInfoEvent);
on<UpdatePowerClampStatusEvent>(_onUpdatePowerClampStatusEvent);
on<ClearPowerClampInfoEvent>(_onClearPowerClampInfoEvent);
}
final PowerClampInfoService _powerClampInfoService;
Future<void> _onLoadPowerClampInfoEvent(
LoadPowerClampInfoEvent event,
Emitter<PowerClampInfoState> emit,
) async {
emit(state.copyWith(status: PowerClampInfoStatus.loading));
try {
final powerClampModel = await _powerClampInfoService.getInfo(event.deviceId);
emit(
state.copyWith(
status: PowerClampInfoStatus.loaded,
powerClampModel: powerClampModel,
),
);
} catch (e) {
emit(
state.copyWith(
status: PowerClampInfoStatus.error,
errorMessage: e.toString(),
),
);
}
}
void _onUpdatePowerClampStatusEvent(
UpdatePowerClampStatusEvent event,
Emitter<PowerClampInfoState> emit,
) async {
final currentModel = state.powerClampModel;
if (currentModel == null) return;
final updatedStatus = PowerStatus.fromStatusList(event.statusList);
final updatedModel = currentModel.copyWith(statusPower: updatedStatus);
emit(state.copyWith(powerClampModel: updatedModel));
}
void _onClearPowerClampInfoEvent(
ClearPowerClampInfoEvent event,
Emitter<PowerClampInfoState> emit,
) {
emit(const PowerClampInfoState());
}
}

View File

@ -0,0 +1,31 @@
part of 'power_clamp_info_bloc.dart';
sealed class PowerClampInfoEvent extends Equatable {
const PowerClampInfoEvent();
@override
List<Object> get props => [];
}
final class LoadPowerClampInfoEvent extends PowerClampInfoEvent {
const LoadPowerClampInfoEvent(this.deviceId);
final String deviceId;
@override
List<Object> get props => [deviceId];
}
final class UpdatePowerClampStatusEvent extends PowerClampInfoEvent {
const UpdatePowerClampStatusEvent(this.statusList);
final List<Status> statusList;
@override
List<Object> get props => [statusList];
}
final class ClearPowerClampInfoEvent extends PowerClampInfoEvent {
const ClearPowerClampInfoEvent();
}

View File

@ -0,0 +1,30 @@
part of 'power_clamp_info_bloc.dart';
enum PowerClampInfoStatus { initial, loading, loaded, error }
final class PowerClampInfoState extends Equatable {
const PowerClampInfoState({
this.status = PowerClampInfoStatus.initial,
this.powerClampModel,
this.errorMessage,
});
final PowerClampInfoStatus status;
final PowerClampModel? powerClampModel;
final String? errorMessage;
PowerClampInfoState copyWith({
PowerClampInfoStatus? status,
PowerClampModel? powerClampModel,
String? errorMessage,
}) {
return PowerClampInfoState(
status: status ?? this.status,
powerClampModel: powerClampModel ?? this.powerClampModel,
errorMessage: errorMessage ?? this.errorMessage,
);
}
@override
List<Object?> get props => [status, powerClampModel, errorMessage];
}

View File

@ -0,0 +1,80 @@
import 'dart:async';
import 'package:bloc/bloc.dart';
import 'package:equatable/equatable.dart';
import 'package:syncrow_web/pages/analytics/services/realtime_device_service/realtime_device_service.dart';
import 'package:syncrow_web/pages/device_managment/all_devices/models/device_status.dart';
part 'realtime_device_changes_event.dart';
part 'realtime_device_changes_state.dart';
class RealtimeDeviceChangesBloc
extends Bloc<RealtimeDeviceChangesEvent, RealtimeDeviceChangesState> {
RealtimeDeviceChangesBloc(
this._realtimeDeviceService,
) : super(const RealtimeDeviceChangesState()) {
on<RealtimeDeviceChangesStarted>(_onRealtimeDeviceChangesStarted);
on<RealtimeDeviceChangesClosed>(_onRealtimeDeviceChangesClosed);
on<_RealtimeDeviceChangesUpdated>(_onRealtimeDeviceChangesUpdated);
}
final RealtimeDeviceService _realtimeDeviceService;
StreamSubscription<List<Status>>? _subscription;
Future<void> _onRealtimeDeviceChangesStarted(
RealtimeDeviceChangesStarted event,
Emitter<RealtimeDeviceChangesState> emit,
) async {
await _subscription?.cancel();
_subscription = _realtimeDeviceService.subscribe(event.deviceId).listen(
(data) {
add(_RealtimeDeviceChangesUpdated(data));
},
onError: (error) {
emit(
state.copyWith(
status: RealtimeDeviceChangesStatus.failure,
errorMessage: '$error',
),
);
},
);
}
Future<void> _onRealtimeDeviceChangesClosed(
RealtimeDeviceChangesClosed event,
Emitter<RealtimeDeviceChangesState> emit,
) async {
add(const _RealtimeDeviceChangesUpdated([]));
await _subscription?.cancel();
_subscription = null;
emit(const RealtimeDeviceChangesState());
}
void _onRealtimeDeviceChangesUpdated(
_RealtimeDeviceChangesUpdated event,
Emitter<RealtimeDeviceChangesState> emit,
) {
final currentState = state;
final updatedList = [
...currentState.deviceStatusList.where(
(device) => !event.deviceStatusList
.any((newDevice) => newDevice.code == device.code),
),
...event.deviceStatusList,
];
emit(
state.copyWith(
status: RealtimeDeviceChangesStatus.loaded,
deviceStatusList: updatedList,
),
);
}
@override
Future<void> close() async {
await _subscription?.cancel();
return super.close();
}
}

View File

@ -0,0 +1,27 @@
part of 'realtime_device_changes_bloc.dart';
sealed class RealtimeDeviceChangesEvent extends Equatable {
const RealtimeDeviceChangesEvent();
@override
List<Object> get props => [];
}
final class RealtimeDeviceChangesStarted extends RealtimeDeviceChangesEvent {
const RealtimeDeviceChangesStarted(this.deviceId);
final String deviceId;
@override
List<Object> get props => [deviceId];
}
final class RealtimeDeviceChangesClosed extends RealtimeDeviceChangesEvent {
const RealtimeDeviceChangesClosed();
}
class _RealtimeDeviceChangesUpdated extends RealtimeDeviceChangesEvent {
final List<Status> deviceStatusList;
const _RealtimeDeviceChangesUpdated(this.deviceStatusList);
}

View File

@ -0,0 +1,30 @@
part of 'realtime_device_changes_bloc.dart';
enum RealtimeDeviceChangesStatus { initial, loaded, failure }
final class RealtimeDeviceChangesState extends Equatable {
const RealtimeDeviceChangesState({
this.status = RealtimeDeviceChangesStatus.initial,
this.deviceStatusList = const <Status>[],
this.errorMessage,
});
final RealtimeDeviceChangesStatus status;
final List<Status> deviceStatusList;
final String? errorMessage;
RealtimeDeviceChangesState copyWith({
RealtimeDeviceChangesStatus? status,
List<Status>? deviceStatusList,
String? errorMessage,
}) {
return RealtimeDeviceChangesState(
status: status ?? this.status,
deviceStatusList: deviceStatusList ?? this.deviceStatusList,
errorMessage: errorMessage ?? this.errorMessage,
);
}
@override
List<Object?> get props => [status, deviceStatusList, errorMessage];
}

View File

@ -0,0 +1,50 @@
import 'package:bloc/bloc.dart';
import 'package:equatable/equatable.dart';
import 'package:syncrow_web/pages/analytics/models/energy_data_model.dart';
import 'package:syncrow_web/pages/analytics/params/get_total_energy_consumption_param.dart';
import 'package:syncrow_web/pages/analytics/services/total_energy_consumption/total_energy_consumption_service.dart';
part 'total_energy_consumption_event.dart';
part 'total_energy_consumption_state.dart';
class TotalEnergyConsumptionBloc
extends Bloc<TotalEnergyConsumptionEvent, TotalEnergyConsumptionState> {
TotalEnergyConsumptionBloc(
this._totalEnergyConsumptionService,
) : super(const TotalEnergyConsumptionState()) {
on<TotalEnergyConsumptionLoadEvent>(_onTotalEnergyConsumptionLoadEvent);
on<ClearTotalEnergyConsumptionEvent>(_onClearTotalEnergyConsumptionEvent);
}
final TotalEnergyConsumptionService _totalEnergyConsumptionService;
Future<void> _onTotalEnergyConsumptionLoadEvent(
TotalEnergyConsumptionLoadEvent event,
Emitter<TotalEnergyConsumptionState> emit,
) async {
try {
emit(state.copyWith(status: TotalEnergyConsumptionStatus.loading));
final chartData = await _totalEnergyConsumptionService.load(event.param);
emit(
state.copyWith(
chartData: chartData,
status: TotalEnergyConsumptionStatus.loaded,
),
);
} catch (e) {
emit(
state.copyWith(
errorMessage: e.toString(),
status: TotalEnergyConsumptionStatus.failure,
),
);
}
}
void _onClearTotalEnergyConsumptionEvent(
ClearTotalEnergyConsumptionEvent event,
Emitter<TotalEnergyConsumptionState> emit,
) async {
emit(const TotalEnergyConsumptionState());
}
}

View File

@ -0,0 +1,21 @@
part of 'total_energy_consumption_bloc.dart';
sealed class TotalEnergyConsumptionEvent extends Equatable {
const TotalEnergyConsumptionEvent();
@override
List<Object?> get props => [];
}
final class TotalEnergyConsumptionLoadEvent extends TotalEnergyConsumptionEvent {
const TotalEnergyConsumptionLoadEvent({required this.param});
final GetTotalEnergyConsumptionParam param;
@override
List<Object?> get props => [param];
}
final class ClearTotalEnergyConsumptionEvent extends TotalEnergyConsumptionEvent {
const ClearTotalEnergyConsumptionEvent();
}

View File

@ -0,0 +1,35 @@
part of 'total_energy_consumption_bloc.dart';
enum TotalEnergyConsumptionStatus {
initial,
loading,
loaded,
failure,
}
final class TotalEnergyConsumptionState extends Equatable {
const TotalEnergyConsumptionState({
this.status = TotalEnergyConsumptionStatus.initial,
this.chartData = const <EnergyDataModel>[],
this.errorMessage,
});
final List<EnergyDataModel> chartData;
final TotalEnergyConsumptionStatus status;
final String? errorMessage;
TotalEnergyConsumptionState copyWith({
List<EnergyDataModel>? chartData,
TotalEnergyConsumptionStatus? status,
String? errorMessage,
}) {
return TotalEnergyConsumptionState(
chartData: chartData ?? this.chartData,
status: status ?? this.status,
errorMessage: errorMessage ?? this.errorMessage,
);
}
@override
List<Object?> get props => [chartData, status, errorMessage];
}

View File

@ -0,0 +1,20 @@
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

@ -0,0 +1,122 @@
import 'package:fl_chart/fl_chart.dart';
import 'package:flutter/material.dart';
import 'package:syncrow_web/pages/analytics/helpers/format_number_to_kwh.dart';
import 'package:syncrow_web/utils/color_manager.dart';
import 'package:syncrow_web/utils/extension/build_context_x.dart';
abstract final class EnergyManagementChartsHelper {
const EnergyManagementChartsHelper._();
static FlTitlesData titlesData(
BuildContext context, {
double? leftTitlesInterval,
}) {
const emptyTitle = AxisTitles(sideTitles: SideTitles(showTitles: false));
return FlTitlesData(
show: true,
bottomTitles: AxisTitles(
drawBelowEverything: true,
sideTitles: SideTitles(
interval: 1,
reservedSize: 32,
showTitles: true,
maxIncluded: true,
getTitlesWidget: (value, meta) => Padding(
padding: const EdgeInsetsDirectional.only(top: 20.0),
child: Text(
(value + 1).toString(),
style: context.textTheme.bodySmall?.copyWith(
color: ColorsManager.greyColor,
fontSize: 12,
),
),
),
),
),
leftTitles: AxisTitles(
sideTitles: SideTitles(
showTitles: true,
maxIncluded: true,
interval: leftTitlesInterval,
reservedSize: 110,
getTitlesWidget: (value, meta) => Padding(
padding: const EdgeInsetsDirectional.only(end: 12),
child: FittedBox(
alignment: AlignmentDirectional.centerStart,
fit: BoxFit.scaleDown,
child: Text(
value.formatNumberToKwh,
style: context.textTheme.bodySmall?.copyWith(
fontSize: 12,
color: ColorsManager.greyColor,
),
),
),
),
),
),
rightTitles: emptyTitle,
topTitles: emptyTitle,
);
}
static String getToolTipLabel(num month, double value) {
final monthLabel = month.toString();
final valueLabel = value.formatNumberToKwh;
final labels = [monthLabel, valueLabel];
return labels.where((element) => element.isNotEmpty).join(', ');
}
static List<LineTooltipItem?> getTooltipItems(List<LineBarSpot> touchedSpots) {
return touchedSpots.map((spot) {
return LineTooltipItem(
getToolTipLabel(spot.x + 1, spot.y),
const TextStyle(
color: ColorsManager.textPrimaryColor,
fontWeight: FontWeight.w600,
fontSize: 12,
),
);
}).toList();
}
static LineTouchTooltipData lineTouchTooltipData() {
return LineTouchTooltipData(
getTooltipColor: (touchTooltipItem) => ColorsManager.whiteColors,
tooltipBorder: const BorderSide(color: ColorsManager.semiTransparentBlack),
tooltipRoundedRadius: 16,
showOnTopOfTheChartBoxArea: false,
tooltipPadding: const EdgeInsets.all(8),
getTooltipItems: getTooltipItems,
);
}
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(
show: true,
drawVerticalLine: false,
drawHorizontalLine: true,
);
}
static LineTouchData lineTouchData() {
return LineTouchData(
handleBuiltInTouches: true,
touchSpotThreshold: 2,
touchTooltipData: EnergyManagementChartsHelper.lineTouchTooltipData(),
);
}
}

View File

@ -0,0 +1,115 @@
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_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/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_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';
abstract final class FetchEnergyManagementDataHelper {
const FetchEnergyManagementDataHelper._();
static const String _powerClampId = 'cb71d6ad-6e29-4eaa-ae3e-1a0d1c5f60fa';
static void loadEnergyManagementData(
BuildContext context, {
required String communityId,
required String spaceId,
DateTime? selectedDate,
}) {
if (communityId.isEmpty && spaceId.isEmpty) {
clearAllData(context);
return;
}
final datePickerState = context.read<AnalyticsDatePickerBloc>().state;
final selectedDate0 = selectedDate ?? datePickerState.monthlyDate;
loadTotalEnergyConsumption(
context,
selectedDate: selectedDate0,
communityId: communityId,
spaceId: spaceId,
);
loadEnergyConsumptionByPhases(context, selectedDate: selectedDate);
loadEnergyConsumptionPerDevice(context);
loadRealtimeDeviceChanges(context);
loadPowerClampInfo(context);
}
static void loadEnergyConsumptionByPhases(
BuildContext context, {
DateTime? selectedDate,
}) {
final param = GetEnergyConsumptionByPhasesParam(
startDate: selectedDate,
spaceId: '',
);
context.read<EnergyConsumptionByPhasesBloc>().add(
LoadEnergyConsumptionByPhasesEvent(param: param),
);
}
static void loadTotalEnergyConsumption(
BuildContext context, {
DateTime? selectedDate,
required String communityId,
required String spaceId,
}) {
final param = GetTotalEnergyConsumptionParam(
spaceId: spaceId,
communityId: communityId,
monthDate: selectedDate,
);
context.read<TotalEnergyConsumptionBloc>().add(
TotalEnergyConsumptionLoadEvent(param: param),
);
}
static void loadEnergyConsumptionPerDevice(BuildContext context) {
const param = GetEnergyConsumptionPerDeviceParam();
context.read<EnergyConsumptionPerDeviceBloc>().add(
const LoadEnergyConsumptionPerDeviceEvent(param),
);
}
static void loadPowerClampInfo(BuildContext context) {
context.read<PowerClampInfoBloc>().add(
const LoadPowerClampInfoEvent(_powerClampId),
);
}
static void loadRealtimeDeviceChanges(BuildContext context) {
context.read<RealtimeDeviceChangesBloc>().add(
const RealtimeDeviceChangesStarted(_powerClampId),
);
}
static void clearAllData(BuildContext context) {
context.read<RealtimeDeviceChangesBloc>().add(
const RealtimeDeviceChangesClosed(),
);
context.read<PowerClampInfoBloc>().add(
const ClearPowerClampInfoEvent(),
);
context.read<EnergyConsumptionPerDeviceBloc>().add(
const ClearEnergyConsumptionPerDeviceEvent(),
);
context.read<TotalEnergyConsumptionBloc>().add(
const ClearTotalEnergyConsumptionEvent(),
);
context.read<EnergyConsumptionByPhasesBloc>().add(
const ClearEnergyConsumptionByPhasesEvent(),
);
}
}

View File

@ -0,0 +1,92 @@
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_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/energy_consumption_per_device_chart_box.dart';
import 'package:syncrow_web/pages/analytics/modules/energy_management/widgets/power_clamp_energy_data_widget.dart';
import 'package:syncrow_web/pages/analytics/modules/energy_management/widgets/total_energy_consumption_chart_box.dart';
import 'package:syncrow_web/pages/space_tree/bloc/space_tree_bloc.dart';
class AnalyticsEnergyManagementView extends StatefulWidget {
const AnalyticsEnergyManagementView({super.key});
@override
State<AnalyticsEnergyManagementView> createState() =>
_AnalyticsEnergyManagementViewState();
}
class _AnalyticsEnergyManagementViewState
extends State<AnalyticsEnergyManagementView> {
@override
void initState() {
final spaceTreeBloc = context.read<SpaceTreeBloc>();
final communityId = spaceTreeBloc.state.selectedCommunities.firstOrNull;
final spaceId = spaceTreeBloc.state.selectedSpaces.firstOrNull;
FetchEnergyManagementDataHelper.loadEnergyManagementData(
context,
communityId: communityId ?? '',
spaceId: spaceId ?? '',
);
super.initState();
}
static const _padding = EdgeInsetsDirectional.all(32);
@override
Widget build(BuildContext context) {
return LayoutBuilder(
builder: (context, constraints) {
final isMediumOrLess = constraints.maxWidth <= 900;
if (isMediumOrLess) {
return SingleChildScrollView(
padding: _padding,
child: Column(
spacing: 32,
children: [
SizedBox(
height: MediaQuery.sizeOf(context).height * 1.2,
child: const PowerClampEnergyDataWidget(),
),
SizedBox(
height: MediaQuery.sizeOf(context).height * 0.5,
child: const TotalEnergyConsumptionChartBox(),
),
SizedBox(
height: MediaQuery.sizeOf(context).height * 0.5,
child: const EnergyConsumptionPerDeviceChartBox(),
),
],
),
);
}
return SingleChildScrollView(
child: Container(
padding: _padding,
height: MediaQuery.sizeOf(context).height * 1,
child: const Column(
children: [
Expanded(
child: Row(
spacing: 32,
children: [
Expanded(
flex: 2,
child: Column(
spacing: 20,
children: [
Expanded(child: TotalEnergyConsumptionChartBox()),
Expanded(child: EnergyConsumptionPerDeviceChartBox()),
],
),
),
Expanded(child: PowerClampEnergyDataWidget()),
],
),
),
],
),
),
);
},
);
}
}

View File

@ -0,0 +1,21 @@
import 'package:flutter/material.dart';
import 'package:syncrow_web/utils/color_manager.dart';
import 'package:syncrow_web/utils/extension/build_context_x.dart';
class ChartTitle extends StatelessWidget {
const ChartTitle({super.key, required this.title});
final Widget title;
@override
Widget build(BuildContext context) {
return DefaultTextStyle(
style: context.textTheme.titleLarge!.copyWith(
fontSize: 22,
fontWeight: FontWeight.w700,
color: ColorsManager.blackColor,
),
child: title,
);
}
}

View File

@ -0,0 +1,172 @@
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: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';
import 'package:syncrow_web/utils/extension/build_context_x.dart';
class EnergyConsumptionByPhasesChart extends StatelessWidget {
const EnergyConsumptionByPhasesChart({
super.key,
required this.energyData,
});
final List<PhasesEnergyConsumption> energyData;
@override
Widget build(BuildContext context) {
return BarChart(
BarChartData(
gridData: EnergyManagementChartsHelper.gridData(),
borderData: EnergyManagementChartsHelper.borderData(),
barTouchData: _barTouchData(context),
titlesData: _titlesData(context),
barGroups: energyData.asMap().entries.map((entry) {
final index = entry.key;
final data = entry.value;
return BarChartGroupData(
x: index,
barRods: [
BarChartRodData(
color: ColorsManager.vividBlue.withValues(alpha: 0.1),
toY: data.phaseA + data.phaseB + data.phaseC,
rodStackItems: [
BarChartRodStackItem(
0,
data.phaseA,
ColorsManager.vividBlue.withValues(alpha: 0.8),
),
BarChartRodStackItem(
data.phaseA,
data.phaseA + data.phaseB,
ColorsManager.vividBlue.withValues(alpha: 0.4),
),
BarChartRodStackItem(
data.phaseA + data.phaseB,
data.phaseA + data.phaseB + data.phaseC,
ColorsManager.vividBlue.withValues(alpha: 0.15),
),
],
width: 16,
borderRadius: const BorderRadius.only(
topLeft: Radius.circular(8),
topRight: Radius.circular(8),
),
),
],
);
}).toList(),
),
);
}
BarTouchData _barTouchData(BuildContext context) {
return BarTouchData(
touchTooltipData: BarTouchTooltipData(
getTooltipColor: (touchTooltipItem) => ColorsManager.whiteColors,
tooltipBorder: const BorderSide(
color: ColorsManager.semiTransparentBlack,
),
tooltipRoundedRadius: 16,
tooltipPadding: const EdgeInsets.all(8),
getTooltipItem: (group, groupIndex, rod, rodIndex) => getTooltipItem(
context: context,
group: group,
groupIndex: groupIndex,
rod: rod,
rodIndex: rodIndex,
),
),
);
}
BarTooltipItem? getTooltipItem({
required BuildContext context,
required BarChartGroupData group,
required int groupIndex,
required BarChartRodData rod,
required int rodIndex,
}) {
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;
return BarTooltipItem(
'$month\n',
context.textTheme.bodyMedium!.copyWith(
color: ColorsManager.blackColor,
fontSize: 14,
),
children: [
TextSpan(
text: 'Phase A: $phaseA\n',
style: context.textTheme.bodySmall?.copyWith(
color: ColorsManager.blackColor,
fontSize: 12,
),
),
TextSpan(
text: 'Phase B: $phaseB\n',
style: context.textTheme.bodySmall?.copyWith(
color: ColorsManager.blackColor,
fontSize: 12,
),
),
TextSpan(
text: 'Phase C: $phaseC',
style: context.textTheme.bodySmall?.copyWith(
color: ColorsManager.blackColor,
fontSize: 12,
),
),
],
);
}
FlTitlesData _titlesData(BuildContext context) {
final titlesData = EnergyManagementChartsHelper.titlesData(
context,
leftTitlesInterval: 250,
);
final leftTitles = titlesData.leftTitles.copyWith(
sideTitles: titlesData.leftTitles.sideTitles.copyWith(
reservedSize: 70,
),
);
final bottomTitles = AxisTitles(
sideTitles: SideTitles(
showTitles: true,
getTitlesWidget: (value, _) {
final month = energyData[value.toInt()].month.getMonthName;
return FittedBox(
alignment: AlignmentDirectional.bottomCenter,
fit: BoxFit.scaleDown,
child: RotatedBox(
quarterTurns: 3,
child: Text(
month,
style: context.textTheme.bodySmall?.copyWith(
color: ColorsManager.greyColor,
fontSize: 11,
),
),
),
);
},
reservedSize: 36,
),
);
return titlesData.copyWith(
leftTitles: leftTitles,
bottomTitles: bottomTitles,
);
}
}

View File

@ -0,0 +1,37 @@
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_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/widgets/energy_consumption_by_phases_chart.dart';
import 'package:syncrow_web/pages/analytics/modules/energy_management/widgets/energy_consumption_by_phases_title.dart';
import 'package:syncrow_web/pages/analytics/widgets/analytics_error_widget.dart';
import 'package:syncrow_web/utils/style.dart';
class EnergyConsumptionByPhasesChartBox extends StatelessWidget {
const EnergyConsumptionByPhasesChartBox({super.key});
@override
Widget build(BuildContext context) {
return BlocBuilder<EnergyConsumptionByPhasesBloc,
EnergyConsumptionByPhasesState>(
builder: (context, state) {
return Container(
padding: const EdgeInsetsDirectional.all(20),
decoration: secondarySection,
child: Column(
mainAxisSize: MainAxisSize.min,
spacing: 20,
children: [
AnalyticsErrorWidget(state.errorMessage),
EnergyConsumptionByPhasesTitle(isLoading: state.status == EnergyConsumptionByPhasesStatus.loading,),
Expanded(
child: EnergyConsumptionByPhasesChart(
energyData: state.chartData,
),
),
],
),
);
},
);
}
}

View File

@ -0,0 +1,80 @@
import 'package:flutter/material.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/utils/color_manager.dart';
import 'package:syncrow_web/utils/extension/build_context_x.dart';
class EnergyConsumptionByPhasesTitle extends StatelessWidget {
const EnergyConsumptionByPhasesTitle({super.key, required this.isLoading});
final bool isLoading;
@override
Widget build(BuildContext context) {
return Row(
crossAxisAlignment: CrossAxisAlignment.center,
mainAxisAlignment: MainAxisAlignment.center,
children: [
ChartsLoadingWidget(isLoading: isLoading),
Expanded(
flex: 4,
child: FittedBox(
alignment: AlignmentDirectional.centerStart,
fit: BoxFit.scaleDown,
child: ChartTitle(
title: Text(
'Energy Consumption by Phases',
style: context.textTheme.titleLarge?.copyWith(
fontSize: 14,
fontWeight: FontWeight.w400,
color: ColorsManager.textPrimaryColor,
),
),
),
),
),
const Spacer(),
...<(String title, double opacity)>[
('A', 0.8),
('B', 0.4),
('C', 0.15),
].map((phase) => _buildPhaseCell(context, phase)),
],
);
}
Widget _buildPhaseCell(
BuildContext context,
(String title, double colorOpacity) phase,
) {
final (title, colorOpacity) = phase;
return Expanded(
child: FittedBox(
fit: BoxFit.scaleDown,
child: Row(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.center,
mainAxisAlignment: MainAxisAlignment.center,
spacing: 4,
children: [
CircleAvatar(
backgroundColor: ColorsManager.vividBlue.withValues(
alpha: colorOpacity,
),
radius: 4,
),
Text(
'Phase $title',
style: context.textTheme.labelSmall?.copyWith(
fontSize: 8,
fontWeight: FontWeight.w400,
color: ColorsManager.lightGreyColor,
),
),
const SizedBox(width: 4),
],
),
),
);
}
}

View File

@ -0,0 +1,55 @@
import 'package:fl_chart/fl_chart.dart';
import 'package:flutter/material.dart';
import 'package:syncrow_web/pages/analytics/models/device_energy_data_model.dart';
import 'package:syncrow_web/pages/analytics/modules/energy_management/helpers/energy_management_charts_helper.dart';
class EnergyConsumptionPerDeviceChart extends StatelessWidget {
const EnergyConsumptionPerDeviceChart({super.key, required this.chartData});
final List<DeviceEnergyDataModel> chartData;
@override
Widget build(BuildContext context) {
return LineChart(
LineChartData(
titlesData: EnergyManagementChartsHelper.titlesData(
context,
leftTitlesInterval: 250,
),
gridData: EnergyManagementChartsHelper.gridData(),
borderData: EnergyManagementChartsHelper.borderData(),
lineTouchData: EnergyManagementChartsHelper.lineTouchData(),
lineBarsData: chartData.map((e) {
return _buildChartBar(
color: e.color,
spots: e.energy
.map(
(energy) => FlSpot(
energy.date.day.toDouble(),
energy.value,
),
)
.toList(),
);
}).toList(),
),
duration: Durations.extralong1,
curve: Curves.easeIn,
);
}
LineChartBarData _buildChartBar({
required Color color,
required List<FlSpot> spots,
}) {
return LineChartBarData(
spots: spots,
dashArray: [12, 18],
isCurved: true,
color: color,
barWidth: 3,
isStrokeCapRound: true,
dotData: const FlDotData(show: false),
);
}
}

View File

@ -0,0 +1,63 @@
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_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';
import 'package:syncrow_web/pages/analytics/modules/energy_management/widgets/energy_consumption_per_device_devices_list.dart';
import 'package:syncrow_web/pages/analytics/widgets/analytics_error_widget.dart';
import 'package:syncrow_web/pages/analytics/widgets/charts_loading_widget.dart';
import 'package:syncrow_web/utils/style.dart';
class EnergyConsumptionPerDeviceChartBox extends StatelessWidget {
const EnergyConsumptionPerDeviceChartBox({super.key});
@override
Widget build(BuildContext context) {
return BlocBuilder<EnergyConsumptionPerDeviceBloc,
EnergyConsumptionPerDeviceState>(
builder: (context, state) {
return Container(
decoration: subSectionContainerDecoration.copyWith(
borderRadius: BorderRadius.circular(30),
),
padding: const EdgeInsets.all(30),
child: Column(
spacing: 20,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
AnalyticsErrorWidget(state.errorMessage),
Row(
spacing: 32,
children: [
if (state.status == EnergyConsumptionPerDeviceStatus.loading)
const ChartsLoadingWidget(isLoading: true),
const Expanded(
flex: 3,
child: FittedBox(
fit: BoxFit.scaleDown,
alignment: AlignmentDirectional.centerStart,
child: ChartTitle(
title: Text('Energy Consumption per Device'),
),
),
),
const Spacer(),
Expanded(
flex: 2,
child: EnergyConsumptionPerDeviceDevicesList(
chartData: state.chartData,
),
),
],
),
const Divider(height: 0),
Expanded(
child: EnergyConsumptionPerDeviceChart(chartData: state.chartData),
),
],
),
);
},
);
}
}

View File

@ -0,0 +1,62 @@
import 'package:flutter/material.dart';
import 'package:syncrow_web/pages/analytics/models/device_energy_data_model.dart';
import 'package:syncrow_web/utils/color_manager.dart';
class EnergyConsumptionPerDeviceDevicesList extends StatelessWidget {
const EnergyConsumptionPerDeviceDevicesList({required this.chartData, super.key});
final List<DeviceEnergyDataModel> chartData;
@override
Widget build(BuildContext context) {
return SingleChildScrollView(
scrollDirection: Axis.horizontal,
child: Row(
spacing: 16,
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.end,
mainAxisAlignment: MainAxisAlignment.end,
children: chartData.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,
),
),
],
),
),
);
}
}

View File

@ -0,0 +1,55 @@
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

@ -0,0 +1,142 @@
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/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/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';
import 'package:syncrow_web/pages/device_managment/power_clamp/models/power_clamp_model.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';
class PowerClampEnergyDataWidget extends StatelessWidget {
const PowerClampEnergyDataWidget({super.key});
@override
Widget build(BuildContext context) {
return BlocListener<RealtimeDeviceChangesBloc, RealtimeDeviceChangesState>(
listenWhen: (previous, current) =>
previous.deviceStatusList != current.deviceStatusList ||
previous.status != current.status,
listener: (context, state) => context.read<PowerClampInfoBloc>().add(
UpdatePowerClampStatusEvent(state.deviceStatusList),
),
child: BlocBuilder<PowerClampInfoBloc, PowerClampInfoState>(
builder: (context, state) {
final generalDataPoints =
state.powerClampModel?.status.general.dataPoints ?? [];
return Container(
decoration: subSectionContainerDecoration.copyWith(
borderRadius: BorderRadius.circular(30),
),
padding: const EdgeInsetsDirectional.all(32),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
AnalyticsErrorWidget(state.errorMessage),
_buildHeader(context),
Text(
'Device ID:',
style: context.textTheme.bodySmall?.copyWith(
color: ColorsManager.textPrimaryColor,
fontWeight: FontWeight.w400,
fontSize: 12,
),
),
const SizedBox(height: 6),
SelectableText(
state.powerClampModel?.productUuid ?? 'N/A',
style: context.textTheme.bodySmall?.copyWith(
color: ColorsManager.blackColor,
fontWeight: FontWeight.w400,
fontSize: 12,
),
),
const Divider(),
Expanded(
flex: 2,
child: PowerClampEnergyStatusWidget(
status: [
PowerClampEnergyStatus(
iconPath: Assets.powerActiveIcon,
title: 'Active',
value: _valueFromCode('EnergyConsumed', generalDataPoints),
unit: 'W',
),
PowerClampEnergyStatus(
iconPath: Assets.voltMeterIcon,
title: 'Current',
value: _valueFromCode('Current', generalDataPoints),
unit: 'A',
),
PowerClampEnergyStatus(
iconPath: Assets.frequencyIcon,
title: 'Frequency',
value: _valueFromCode('Frequency', generalDataPoints),
unit: 'Hz',
),
],
),
),
const SizedBox(height: 14),
Expanded(
flex: 4,
child: PowerClampPhasesDataWidget(
phaseA: state.powerClampModel?.status.phaseA,
phaseB: state.powerClampModel?.status.phaseB,
phaseC: state.powerClampModel?.status.phaseC,
),
),
const SizedBox(height: 14),
const Expanded(flex: 3, child: EnergyConsumptionByPhasesChartBox()),
],
),
);
},
),
);
}
Widget _buildHeader(BuildContext context) {
return Row(
mainAxisSize: MainAxisSize.min,
children: [
Expanded(
flex: 2,
child: FittedBox(
fit: BoxFit.scaleDown,
alignment: AlignmentDirectional.centerStart,
child: SelectableText(
'Smart Power Clamp',
style: context.textTheme.headlineSmall?.copyWith(
fontWeight: FontWeight.w700,
color: ColorsManager.vividBlue.withValues(alpha: 0.6),
fontSize: 18,
),
),
),
),
const Spacer(),
const Expanded(
child: FittedBox(
fit: BoxFit.scaleDown,
alignment: AlignmentDirectional.centerEnd,
child: PowerClampEnergyDataDeviceDropdown(),
),
),
],
);
}
String _valueFromCode(String code, List<DataPoint> points) {
return points
.firstWhere((e) => e.code == code, orElse: () => DataPoint(value: '--'))
.value
.toString();
}
}

View File

@ -0,0 +1,95 @@
import 'package:flutter/material.dart';
import 'package:flutter_svg/flutter_svg.dart';
import 'package:syncrow_web/pages/analytics/models/power_clamp_energy_status.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 PowerClampEnergyStatusWidget extends StatelessWidget {
const PowerClampEnergyStatusWidget({
super.key,
required this.status,
});
final List<PowerClampEnergyStatus> status;
@override
Widget build(BuildContext context) {
return Container(
decoration: secondarySection.copyWith(boxShadow: const []),
child: Column(
mainAxisSize: MainAxisSize.min,
children: List.generate(
status.length * 2 - 1,
(index) => index.isEven
? Expanded(child: _buildItem(context, status[index ~/ 2]))
: _buildDivider(),
),
),
);
}
Widget _buildItem(BuildContext context, PowerClampEnergyStatus item) {
return Center(
child: ListTile(
titleAlignment: ListTileTitleAlignment.center,
leading: SvgPicture.asset(
item.iconPath,
fit: BoxFit.scaleDown,
alignment: AlignmentDirectional.centerEnd,
height: 18,
width: 18,
),
contentPadding: const EdgeInsets.symmetric(horizontal: 20),
title: Text(
item.title,
style: context.textTheme.bodySmall?.copyWith(
color: ColorsManager.textPrimaryColor.withValues(alpha: 0.83),
fontWeight: FontWeight.w400,
fontSize: 16,
),
),
trailing: Text.rich(
TextSpan(
style: context.textTheme.bodySmall?.copyWith(
color: ColorsManager.textPrimaryColor.withValues(alpha: 0.83),
fontWeight: FontWeight.w700,
fontSize: 16,
),
children: [
TextSpan(text: '${item.value} '),
TextSpan(
text: item.unit,
style: context.textTheme.bodySmall?.copyWith(
color: ColorsManager.textPrimaryColor.withValues(alpha: 0.83),
fontWeight: FontWeight.w700,
fontSize: 8,
),
),
],
),
),
),
);
}
Widget _buildDivider() {
return Container(
height: 1,
decoration: const BoxDecoration(
boxShadow: [
BoxShadow(
color: Color.fromARGB(20, 0, 0, 0),
offset: Offset(0, 1),
blurRadius: 1,
),
BoxShadow(
color: Color.fromARGB(30, 0, 0, 0),
offset: Offset(0, -2),
blurRadius: 3,
),
],
),
);
}
}

View File

@ -0,0 +1,115 @@
import 'package:flutter/material.dart';
import 'package:flutter_svg/svg.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 PowerClampPhase extends StatelessWidget {
const PowerClampPhase({
super.key,
required this.iconPath,
required this.title,
required this.value,
this.unit,
});
final String iconPath;
final String title;
final String value;
final String? unit;
@override
Widget build(BuildContext context) {
return Expanded(
child: Container(
margin: const EdgeInsets.only(bottom: 4),
decoration: containerWhiteDecoration.copyWith(boxShadow: const []),
padding: const EdgeInsetsDirectional.all(8),
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.center,
spacing: 10,
children: [
_buildIcon(),
Expanded(
flex: 3,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
spacing: 4,
children: [
_buildTitle(context),
_buildValue(context),
],
),
),
],
),
),
);
}
Widget _buildValue(BuildContext context) {
final textStyle = context.textTheme.bodySmall?.copyWith(
color: ColorsManager.textPrimaryColor.withValues(
alpha: 0.83,
),
fontWeight: FontWeight.w700,
fontSize: 15,
);
return Expanded(
flex: 2,
child: FittedBox(
alignment: AlignmentDirectional.topCenter,
fit: BoxFit.scaleDown,
child: Text.rich(
TextSpan(
style: textStyle,
children: [
TextSpan(text: '$value '),
if (unit != null)
TextSpan(
text: unit,
style: textStyle?.copyWith(
color: ColorsManager.textPrimaryColor.withValues(
alpha: 0.83,
),
fontWeight: FontWeight.w700,
fontSize: 8,
),
),
],
),
),
),
);
}
Widget _buildTitle(BuildContext context) {
return Expanded(
flex: 2,
child: FittedBox(
alignment: AlignmentDirectional.centerStart,
fit: BoxFit.scaleDown,
child: Text(
title,
style: context.textTheme.titleSmall?.copyWith(
color: ColorsManager.lightGreyColor,
fontWeight: FontWeight.w400,
fontSize: 8,
),
),
),
);
}
Widget _buildIcon() {
return Expanded(
child: FittedBox(
fit: BoxFit.scaleDown,
child: SvgPicture.asset(iconPath),
),
);
}
}

View File

@ -0,0 +1,131 @@
import 'package:flutter/material.dart';
import 'package:syncrow_web/pages/analytics/modules/energy_management/widgets/power_clamp_phase.dart';
import 'package:syncrow_web/pages/device_managment/power_clamp/models/power_clamp_model.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';
class PowerClampPhasesDataWidget extends StatelessWidget {
const PowerClampPhasesDataWidget({
required this.phaseA,
required this.phaseB,
required this.phaseC,
super.key,
});
final Phase? phaseA;
final Phase? phaseB;
final Phase? phaseC;
@override
Widget build(BuildContext context) {
final phases = [phaseA, phaseB, phaseC];
return Container(
width: double.infinity,
decoration: secondarySection.copyWith(boxShadow: const []),
child: Row(
children: List.generate(5, (index) {
if (index.isOdd) return _buildSeparator();
final phaseIndex = index ~/ 2;
final phase = phases[phaseIndex];
final phaseSuffix = ['A', 'B', 'C'][phaseIndex];
return Expanded(
child: Padding(
padding: const EdgeInsetsDirectional.symmetric(horizontal: 14),
child: Column(
spacing: 4,
children: [
const SizedBox(height: 8),
FittedBox(
fit: BoxFit.scaleDown,
alignment: AlignmentDirectional.center,
child: Text(
'Phase ${phaseIndex + 1}',
style: context.textTheme.titleLarge?.copyWith(
color: ColorsManager.textPrimaryColor,
fontWeight: FontWeight.w700,
fontSize: 20,
),
),
),
PowerClampPhase(
iconPath: Assets.powerActiveIcon,
title: 'Active Power',
value: _valueFromCode(
code: 'ReactivePower$phaseSuffix',
points: phase?.dataPoints,
),
unit: 'W',
),
PowerClampPhase(
iconPath: Assets.voltageIcon,
title: 'Voltage',
value: _valueFromCode(
code: 'Voltage$phaseSuffix',
points: phase?.dataPoints,
),
unit: 'V',
),
PowerClampPhase(
iconPath: Assets.voltMeterIcon,
title: 'Current',
value: _valueFromCode(
code: 'Current$phaseSuffix',
points: phase?.dataPoints,
),
unit: 'A',
),
PowerClampPhase(
iconPath: Assets.speedoMeter,
title: 'Power Factor',
value: _valueFromCode(
code: 'PowerFactor$phaseSuffix',
points: phase?.dataPoints,
),
),
const SizedBox(height: 8),
],
),
),
);
}),
),
);
}
Widget _buildSeparator() {
return Container(
height: double.infinity,
width: 1,
decoration: const BoxDecoration(
boxShadow: [
BoxShadow(
color: Color.fromARGB(20, 0, 0, 0),
offset: Offset(1, 0),
blurRadius: 1,
),
BoxShadow(
color: Color.fromARGB(30, 0, 0, 0),
offset: Offset(-2, 0),
blurRadius: 1,
),
],
),
);
}
String _valueFromCode({
required String code,
required List<DataPoint>? points,
}) {
final element = points?.firstWhere(
(e) => e.code == code,
orElse: () => DataPoint(value: '--'),
);
return element?.value.toString() ?? '--';
}
}

View File

@ -0,0 +1,77 @@
import 'package:fl_chart/fl_chart.dart';
import 'package:flutter/material.dart';
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});
final List<EnergyDataModel> chartData;
@override
Widget build(BuildContext context) {
return Expanded(
child: LineChart(
LineChartData(
titlesData: EnergyManagementChartsHelper.titlesData(context),
gridData: EnergyManagementChartsHelper.gridData(),
borderData: EnergyManagementChartsHelper.borderData(),
lineTouchData: EnergyManagementChartsHelper.lineTouchData(),
lineBarsData: _lineBarsData,
),
duration: Durations.extralong1,
curve: Curves.easeIn,
),
);
}
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.value,
),
)
.toList(),
color: ColorsManager.blueColor.withValues(alpha: 0.6),
shadow: const Shadow(color: Colors.black12),
show: true,
isCurved: true,
belowBarData: BarAreaData(
show: true,
gradient: LinearGradient(
colors: [
ColorsManager.vividBlue.withValues(alpha: 0.3),
ColorsManager.vividBlue.withValues(alpha: 0.2),
ColorsManager.vividBlue.withValues(alpha: 0.1),
Colors.transparent,
],
begin: Alignment.center,
end: Alignment.bottomCenter,
),
),
dotData: const FlDotData(show: false),
isStrokeCapRound: true,
barWidth: 3,
),
];
}
}

View File

@ -0,0 +1,49 @@
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_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/modules/energy_management/widgets/chart_title.dart';
import 'package:syncrow_web/pages/analytics/modules/energy_management/widgets/total_energy_consumption_chart.dart';
import 'package:syncrow_web/pages/analytics/widgets/analytics_error_widget.dart';
import 'package:syncrow_web/pages/analytics/widgets/charts_loading_widget.dart';
import 'package:syncrow_web/utils/style.dart';
class TotalEnergyConsumptionChartBox extends StatelessWidget {
const TotalEnergyConsumptionChartBox({super.key});
@override
Widget build(BuildContext context) {
return BlocBuilder<TotalEnergyConsumptionBloc, TotalEnergyConsumptionState>(
builder: (context, state) => Container(
decoration: subSectionContainerDecoration.copyWith(
borderRadius: BorderRadius.circular(30),
),
padding: const EdgeInsets.all(30),
child: Column(
spacing: 20,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
AnalyticsErrorWidget(state.errorMessage),
Row(
children: [
ChartsLoadingWidget(
isLoading: state.status == TotalEnergyConsumptionStatus.loading,
),
const Expanded(
flex: 3,
child: FittedBox(
alignment: AlignmentDirectional.centerStart,
fit: BoxFit.scaleDown,
child: ChartTitle(title: Text('Total Energy Consumption')),
),
),
const Spacer(flex: 4),
],
),
const Divider(),
TotalEnergyConsumptionChart(chartData: state.chartData),
],
),
),
);
}
}

View File

@ -0,0 +1,37 @@
import 'package:bloc/bloc.dart';
import 'package:equatable/equatable.dart';
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';
part 'occupancy_event.dart';
part 'occupancy_state.dart';
class OccupancyBloc extends Bloc<OccupancyEvent, OccupancyState> {
OccupancyBloc(this._occupacyService) : super(const OccupancyState()) {
on<LoadOccupancyEvent>(_onLoadOccupancyEvent);
on<ClearOccupancyEvent>(_onClearOccupancyEvent);
}
final OccupacyService _occupacyService;
Future<void> _onLoadOccupancyEvent(
LoadOccupancyEvent event,
Emitter<OccupancyState> emit,
) async {
emit(state.copyWith(status: OccupancyStatus.loading));
try {
final chartData = await _occupacyService.load(event.param);
emit(state.copyWith(chartData: chartData, status: OccupancyStatus.loaded));
} catch (e) {
emit(state.copyWith(status: OccupancyStatus.failure, errorMessage: '$e'));
}
}
void _onClearOccupancyEvent(
ClearOccupancyEvent event,
Emitter<OccupancyState> emit,
) {
emit(const OccupancyState());
}
}

View File

@ -0,0 +1,21 @@
part of 'occupancy_bloc.dart';
sealed class OccupancyEvent extends Equatable {
const OccupancyEvent();
@override
List<Object> get props => [];
}
final class LoadOccupancyEvent extends OccupancyEvent {
const LoadOccupancyEvent(this.param);
final GetOccupancyParam param;
@override
List<Object> get props => [param];
}
final class ClearOccupancyEvent extends OccupancyEvent {
const ClearOccupancyEvent();
}

View File

@ -0,0 +1,30 @@
part of 'occupancy_bloc.dart';
enum OccupancyStatus { initial, loading, loaded, failure }
final class OccupancyState extends Equatable {
const OccupancyState({
this.chartData = const [],
this.status = OccupancyStatus.initial,
this.errorMessage,
});
final List<Occupacy> chartData;
final OccupancyStatus status;
final String? errorMessage;
OccupancyState copyWith({
List<Occupacy>? chartData,
OccupancyStatus? status,
String? errorMessage,
}) {
return OccupancyState(
chartData: chartData ?? this.chartData,
status: status ?? this.status,
errorMessage: errorMessage ?? this.errorMessage,
);
}
@override
List<Object?> get props => [chartData, status, errorMessage];
}

View File

@ -0,0 +1,49 @@
import 'package:bloc/bloc.dart';
import 'package:equatable/equatable.dart';
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';
part 'occupancy_heat_map_event.dart';
part 'occupancy_heat_map_state.dart';
class OccupancyHeatMapBloc
extends Bloc<OccupancyHeatMapEvent, OccupancyHeatMapState> {
OccupancyHeatMapBloc(
this._occupancyHeatMapService,
) : super(const OccupancyHeatMapState()) {
on<LoadOccupancyHeatMapEvent>(_onLoadOccupancyHeatMapEvent);
on<ClearOccupancyHeatMapEvent>(_onClearOccupancyHeatMapEvent);
}
final OccupancyHeatMapService _occupancyHeatMapService;
Future<void> _onLoadOccupancyHeatMapEvent(
LoadOccupancyHeatMapEvent event,
Emitter<OccupancyHeatMapState> emit,
) async {
emit(state.copyWith(status: OccupancyHeatMapStatus.loading));
try {
final occupancyHeatMap = await _occupancyHeatMapService.load(event.param);
emit(
state.copyWith(
status: OccupancyHeatMapStatus.loaded,
heatMapData: occupancyHeatMap,
),
);
} catch (e) {
emit(
state.copyWith(
status: OccupancyHeatMapStatus.failure,
errorMessage: e.toString(),
),
);
}
}
void _onClearOccupancyHeatMapEvent(
ClearOccupancyHeatMapEvent event,
Emitter<OccupancyHeatMapState> emit,
) {
emit(const OccupancyHeatMapState());
}
}

View File

@ -0,0 +1,21 @@
part of 'occupancy_heat_map_bloc.dart';
sealed class OccupancyHeatMapEvent extends Equatable {
const OccupancyHeatMapEvent();
@override
List<Object> get props => [];
}
final class LoadOccupancyHeatMapEvent extends OccupancyHeatMapEvent {
const LoadOccupancyHeatMapEvent(this.param);
final GetOccupancyHeatMapParam param;
@override
List<Object> get props => [param];
}
final class ClearOccupancyHeatMapEvent extends OccupancyHeatMapEvent {
const ClearOccupancyHeatMapEvent();
}

View File

@ -0,0 +1,30 @@
part of 'occupancy_heat_map_bloc.dart';
enum OccupancyHeatMapStatus { initial, loading, loaded, failure }
final class OccupancyHeatMapState extends Equatable {
const OccupancyHeatMapState({
this.status = OccupancyHeatMapStatus.initial,
this.heatMapData = const [],
this.errorMessage,
});
final OccupancyHeatMapStatus status;
final String? errorMessage;
final List<OccupancyHeatMapModel> heatMapData;
OccupancyHeatMapState copyWith({
OccupancyHeatMapStatus? status,
List<OccupancyHeatMapModel>? heatMapData,
String? errorMessage,
}) {
return OccupancyHeatMapState(
status: status ?? this.status,
heatMapData: heatMapData ?? this.heatMapData,
errorMessage: errorMessage ?? this.errorMessage,
);
}
@override
List<Object?> get props => [status, errorMessage, heatMapData];
}

View File

@ -0,0 +1,63 @@
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_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/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_occupancy_heat_map_param.dart';
import 'package:syncrow_web/pages/analytics/params/get_occupancy_param.dart';
abstract final class FetchOccupancyDataHelper {
const FetchOccupancyDataHelper._();
static void loadOccupancyData(
BuildContext context, {
required String communityId,
required String spaceId,
}) {
if (communityId.isEmpty && spaceId.isEmpty) {
clearAllData(context);
}
final datePickerState = context.read<AnalyticsDatePickerBloc>().state;
context.read<OccupancyBloc>().add(
LoadOccupancyEvent(
GetOccupancyParam(
monthDate:
'${datePickerState.monthlyDate.year}-${datePickerState.monthlyDate.month}',
spaceUuid: spaceId,
communityUuid: communityId,
),
),
);
context.read<OccupancyHeatMapBloc>().add(
LoadOccupancyHeatMapEvent(
GetOccupancyHeatMapParam(
spaceId: spaceId,
communityId: communityId,
year: datePickerState.yearlyDate,
),
),
);
context.read<RealtimeDeviceChangesBloc>()
..add(const RealtimeDeviceChangesClosed())
..add(
const RealtimeDeviceChangesStarted('14fe6e7e-47af-4a07-ae0a-7c4a26ef8135'),
);
}
static void clearAllData(BuildContext context) {
context.read<OccupancyBloc>().add(
const ClearOccupancyEvent(),
);
context.read<OccupancyHeatMapBloc>().add(
const ClearOccupancyHeatMapEvent(),
);
context.read<RealtimeDeviceChangesBloc>().add(
const RealtimeDeviceChangesClosed(),
);
}
}

View File

@ -0,0 +1,56 @@
import 'package:flutter/material.dart';
import 'package:syncrow_web/pages/analytics/modules/occupancy/widgets/occupancy_chart_box.dart';
import 'package:syncrow_web/pages/analytics/modules/occupancy/widgets/occupancy_end_side_bar.dart';
import 'package:syncrow_web/pages/analytics/modules/occupancy/widgets/occupancy_heat_map_box.dart';
class AnalyticsOccupancyView extends StatelessWidget {
const AnalyticsOccupancyView({super.key});
static const _padding = EdgeInsetsDirectional.all(32);
@override
Widget build(BuildContext context) {
final height = MediaQuery.sizeOf(context).height;
return LayoutBuilder(
builder: (context, constraints) {
final isMediumOrLess = constraints.maxWidth <= 900;
if (isMediumOrLess) {
return SingleChildScrollView(
padding: _padding,
child: Column(
spacing: 32,
children: [
SizedBox(height: height * 0.45, child: const OccupancyEndSideBar()),
SizedBox(height: height * 0.5, child: const OccupancyChartBox()),
SizedBox(height: height * 0.5, child: const Placeholder()),
],
),
);
}
return SingleChildScrollView(
child: Container(
padding: _padding,
height: height * 0.9,
child: const Row(
spacing: 32,
children: [
Expanded(
flex: 5,
child: Column(
spacing: 20,
children: [
Expanded(child: OccupancyChartBox()),
Expanded(child: OccupancyHeatMapBox()),
],
),
),
Expanded(flex: 2, child: OccupancyEndSideBar()),
],
),
),
);
},
);
}
}

View File

@ -0,0 +1,145 @@
import 'package:fl_chart/fl_chart.dart';
import 'package:flutter/material.dart';
import 'package:syncrow_web/pages/analytics/models/occupacy.dart';
import 'package:syncrow_web/pages/analytics/modules/energy_management/helpers/energy_management_charts_helper.dart';
import 'package:syncrow_web/utils/color_manager.dart';
import 'package:syncrow_web/utils/extension/build_context_x.dart';
class OccupancyChart extends StatelessWidget {
const OccupancyChart({required this.chartData, super.key});
final List<Occupacy> chartData;
static const _chartWidth = 16.0;
@override
Widget build(BuildContext context) {
return BarChart(
BarChartData(
maxY: 1.0,
gridData: EnergyManagementChartsHelper.gridData().copyWith(
checkToShowHorizontalLine: (value) => true,
horizontalInterval: 0.25,
),
borderData: EnergyManagementChartsHelper.borderData(),
barTouchData: _barTouchData(context),
titlesData: _titlesData(context),
barGroups: List.generate(chartData.length, (index) {
final actual = chartData[index];
return BarChartGroupData(
x: index,
barsSpace: 0,
groupVertically: true,
barRods: [
BarChartRodData(
toY: 1.0,
fromY: double.parse(actual.occupancy) + 0.025,
color: ColorsManager.graysColor,
width: _chartWidth,
borderRadius: BorderRadius.circular(10),
),
BarChartRodData(
toY: double.parse(actual.occupancy),
color: ColorsManager.vividBlue.withValues(alpha: 0.8),
width: _chartWidth,
borderRadius: BorderRadius.circular(10),
),
],
);
}),
),
);
}
BarTouchData _barTouchData(BuildContext context) {
return BarTouchData(
touchTooltipData: BarTouchTooltipData(
getTooltipColor: (touchTooltipItem) => ColorsManager.whiteColors,
tooltipBorder: const BorderSide(
color: ColorsManager.semiTransparentBlack,
),
tooltipRoundedRadius: 16,
tooltipPadding: const EdgeInsets.all(8),
getTooltipItem: (group, groupIndex, rod, rodIndex) => getTooltipItem(
context: context,
group: group,
groupIndex: groupIndex,
rod: rod,
rodIndex: rodIndex,
),
),
);
}
BarTooltipItem? getTooltipItem({
required BuildContext context,
required BarChartGroupData group,
required int groupIndex,
required BarChartRodData rod,
required int rodIndex,
}) {
final data = chartData;
final occupancyValue = double.parse(data[group.x.toInt()].occupancy);
final percentage = '${(occupancyValue * 100).toStringAsFixed(0)}%';
return BarTooltipItem(
percentage,
context.textTheme.bodyMedium!.copyWith(
color: ColorsManager.blackColor,
fontSize: 14,
),
);
}
FlTitlesData _titlesData(BuildContext context) {
final titlesData = EnergyManagementChartsHelper.titlesData(
context,
leftTitlesInterval: 250,
);
final leftTitles = titlesData.leftTitles.copyWith(
sideTitles: titlesData.leftTitles.sideTitles.copyWith(
reservedSize: 70,
interval: 0.25,
getTitlesWidget: (value, meta) => Padding(
padding: const EdgeInsetsDirectional.only(end: 12),
child: FittedBox(
alignment: AlignmentDirectional.centerStart,
fit: BoxFit.scaleDown,
child: Text(
'${(value * 100).toStringAsFixed(0)}%',
style: context.textTheme.bodySmall?.copyWith(
fontSize: 12,
color: ColorsManager.greyColor,
),
),
),
),
),
);
final bottomTitles = AxisTitles(
sideTitles: SideTitles(
showTitles: true,
getTitlesWidget: (value, _) => FittedBox(
alignment: AlignmentDirectional.bottomCenter,
fit: BoxFit.scaleDown,
child: Text(
(value + 1).toString(),
style: context.textTheme.bodySmall?.copyWith(
color: ColorsManager.greyColor,
fontSize: 8,
),
),
),
reservedSize: 36,
),
);
return titlesData.copyWith(
leftTitles: leftTitles,
bottomTitles: bottomTitles,
);
}
}

View File

@ -0,0 +1,73 @@
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_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/widgets/analytics_date_filter_button.dart';
import 'package:syncrow_web/pages/analytics/modules/energy_management/widgets/chart_title.dart';
import 'package:syncrow_web/pages/analytics/modules/occupancy/blocs/occupancy/occupancy_bloc.dart';
import 'package:syncrow_web/pages/analytics/modules/occupancy/helpers/fetch_occupancy_data_helper.dart';
import 'package:syncrow_web/pages/analytics/modules/occupancy/widgets/occupancy_chart.dart';
import 'package:syncrow_web/pages/analytics/widgets/analytics_error_widget.dart';
import 'package:syncrow_web/pages/space_tree/bloc/space_tree_bloc.dart';
import 'package:syncrow_web/utils/style.dart';
class OccupancyChartBox extends StatelessWidget {
const OccupancyChartBox({super.key});
@override
Widget build(BuildContext context) {
final spaceTreeState = context.watch<SpaceTreeBloc>().state;
return BlocBuilder<OccupancyBloc, OccupancyState>(
builder: (context, state) {
return Container(
padding: const EdgeInsets.all(30),
decoration: containerWhiteDecoration,
child: Column(
spacing: 20,
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
AnalyticsErrorWidget(state.errorMessage),
Row(
children: [
const Expanded(
flex: 3,
child: FittedBox(
alignment: AlignmentDirectional.centerStart,
fit: BoxFit.scaleDown,
child: ChartTitle(title: Text('Occupancy')),
),
),
const Spacer(),
Expanded(
child: FittedBox(
fit: BoxFit.scaleDown,
alignment: AlignmentDirectional.centerEnd,
child: AnalyticsDateFilterButton(
onDateSelected: (DateTime value) {
context.read<AnalyticsDatePickerBloc>().add(
UpdateAnalyticsDatePickerEvent(montlyDate: value),
);
FetchOccupancyDataHelper.loadOccupancyData(
context,
communityId: spaceTreeState.selectedCommunities.firstOrNull ?? '',
spaceId: spaceTreeState.selectedSpaces.firstOrNull ?? '',
);
},
selectedDate: context
.watch<AnalyticsDatePickerBloc>()
.state
.monthlyDate,
),
),
),
],
),
const Divider(height: 0),
Expanded(child: OccupancyChart(chartData: state.chartData)),
],
),
);
},
);
}
}

View File

@ -0,0 +1,133 @@
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/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/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});
@override
Widget build(BuildContext context) {
return BlocBuilder<RealtimeDeviceChangesBloc, RealtimeDeviceChangesState>(
builder: (context, state) {
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(
(const Uuid().v4()),
style: context.textTheme.bodySmall?.copyWith(
color: ColorsManager.blackColor,
fontWeight: FontWeight.w400,
fontSize: 12,
),
),
const SizedBox(height: 10),
const Divider(height: 1, color: ColorsManager.greyColor),
const SizedBox(height: 50),
SizedBox(
height: MediaQuery.sizeOf(context).height * 0.2,
child: PowerClampEnergyStatusWidget(
status: [
PowerClampEnergyStatus(
iconPath: Assets.presenceState,
title: 'Presence Status',
value: _valueFromCode(
'presence_state',
state.deviceStatusList,
),
unit: '',
),
PowerClampEnergyStatus(
iconPath: Assets.presenceTimeIcon,
title: 'Presence Time',
value:
'${_valueFromCode('none_body_time', state.deviceStatusList)} Min',
unit: '',
),
PowerClampEnergyStatus(
iconPath: Assets.currentDistanceIcon,
title: 'Detection Distance',
value:
'${_valueFromCode('space_move_val', state.deviceStatusList)} M',
unit: '',
),
],
),
),
const SizedBox(height: 20),
],
),
);
},
);
}
String _valueFromCode(
String code,
List<Status> status, {
String? defaultValue,
}) {
final value = status
.firstWhere(
(e) => e.code == code,
orElse: () => Status(code: '--', value: '--'),
)
.value
.toString();
return value == 'null' ? defaultValue ?? '--' : value;
}
Widget _buildHeader(BuildContext context) {
return Row(
mainAxisSize: MainAxisSize.min,
children: [
Expanded(
flex: 2,
child: FittedBox(
fit: BoxFit.scaleDown,
alignment: AlignmentDirectional.centerStart,
child: SelectableText(
'Presnce Sensor',
style: context.textTheme.headlineSmall?.copyWith(
fontWeight: FontWeight.w700,
color: ColorsManager.vividBlue.withValues(alpha: 0.6),
fontSize: 18,
),
),
),
),
const Spacer(),
const Expanded(
child: FittedBox(
fit: BoxFit.scaleDown,
alignment: AlignmentDirectional.centerEnd,
child: PowerClampEnergyDataDeviceDropdown(),
),
),
],
);
}
}

View File

@ -0,0 +1,84 @@
import 'dart:math' as math show max;
import 'package:flutter/material.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';
import 'package:syncrow_web/pages/analytics/modules/occupancy/widgets/occupancy_painter.dart';
import 'package:syncrow_web/utils/color_manager.dart';
class OccupancyHeatMap extends StatelessWidget {
const OccupancyHeatMap({required this.heatMapData, super.key});
final Map<DateTime, int> heatMapData;
static const _cellSize = 16.0;
static const _totalWeeks = 53;
int get _maxValue => heatMapData.isNotEmpty
? heatMapData.keys.map((key) => heatMapData[key]!).reduce(math.max)
: 0;
DateTime _getStartingDate() {
final jan1 = DateTime(DateTime.now().year, 1, 1);
final startOfWeek = jan1.subtract(Duration(days: jan1.weekday - 1));
return startOfWeek;
}
List<OccupancyPaintItem> _generatePaintItems(DateTime startDate) {
return List.generate(_totalWeeks * 7, (index) {
final date = startDate.add(Duration(days: index));
final value = heatMapData[date] ?? 0;
return OccupancyPaintItem(index: index, value: value);
});
}
@override
Widget build(BuildContext context) {
final startDate = _getStartingDate();
final paintItems = _generatePaintItems(startDate);
return Column(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
OccupancyHeatMapMonths(startDate: startDate, cellSize: _cellSize),
Container(
decoration: const BoxDecoration(
border: Border(
bottom: BorderSide(color: ColorsManager.grayBorder),
top: BorderSide(color: ColorsManager.grayBorder),
),
),
width: double.infinity,
child: Row(
children: [
Expanded(
child: FittedBox(
fit: BoxFit.fill,
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,
),
),
),
],
),
),
),
],
),
),
const SizedBox(height: 20),
OccupancyHeatMapGradient(maxValue: _maxValue),
],
);
}
}

View File

@ -0,0 +1,81 @@
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_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/widgets/analytics_date_filter_button.dart';
import 'package:syncrow_web/pages/analytics/modules/energy_management/widgets/chart_title.dart';
import 'package:syncrow_web/pages/analytics/modules/occupancy/blocs/occupancy_heat_map/occupancy_heat_map_bloc.dart';
import 'package:syncrow_web/pages/analytics/modules/occupancy/helpers/fetch_occupancy_data_helper.dart';
import 'package:syncrow_web/pages/analytics/modules/occupancy/widgets/occupancy_heat_map.dart';
import 'package:syncrow_web/pages/analytics/widgets/analytics_error_widget.dart';
import 'package:syncrow_web/pages/space_tree/bloc/space_tree_bloc.dart';
import 'package:syncrow_web/utils/style.dart';
class OccupancyHeatMapBox extends StatelessWidget {
const OccupancyHeatMapBox({super.key});
@override
Widget build(BuildContext context) {
final spaceTreeState = context.watch<SpaceTreeBloc>().state;
return BlocBuilder<OccupancyHeatMapBloc, OccupancyHeatMapState>(
builder: (context, state) {
return Container(
padding: const EdgeInsets.all(30),
decoration: containerWhiteDecoration,
child: Column(
spacing: 20,
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
AnalyticsErrorWidget(state.errorMessage),
Row(
children: [
const Expanded(
flex: 3,
child: FittedBox(
alignment: AlignmentDirectional.centerStart,
fit: BoxFit.scaleDown,
child: ChartTitle(title: Text('Occupancy Heat Map')),
),
),
const Spacer(),
Expanded(
child: FittedBox(
fit: BoxFit.scaleDown,
alignment: AlignmentDirectional.centerEnd,
child: AnalyticsDateFilterButton(
onDateSelected: (DateTime value) {
context.read<AnalyticsDatePickerBloc>().add(
UpdateAnalyticsDatePickerEvent(yearlyDate: value),
);
FetchOccupancyDataHelper.loadOccupancyData(
context,
communityId:
spaceTreeState.selectedCommunities.firstOrNull ?? '',
spaceId: spaceTreeState.selectedSpaces.firstOrNull ?? '',
);
},
datePickerType: DatePickerType.year,
selectedDate: context
.watch<AnalyticsDatePickerBloc>()
.state
.yearlyDate,
),
),
),
],
),
const Divider(height: 0),
Expanded(
child: OccupancyHeatMap(
heatMapData: state.heatMapData.asMap().map(
(_, value) => MapEntry(value.date, value.occupancy),
),
),
),
],
),
);
},
);
}
}

View File

@ -0,0 +1,51 @@
import 'package:flutter/material.dart';
import 'package:syncrow_web/utils/color_manager.dart';
import 'package:syncrow_web/utils/extension/build_context_x.dart';
class OccupancyHeatMapDays extends StatelessWidget {
const OccupancyHeatMapDays({
required this.cellSize,
this.textColor = ColorsManager.blackColor,
super.key,
});
final double cellSize;
final Color textColor;
static const _weekDayLabels = [
'Monday',
'Tuesday',
'Wednesday',
'Thursday',
'Friday',
'Saturday',
'Sunday',
];
@override
Widget build(BuildContext context) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: List.generate(7, (i) {
final dayLabel = _weekDayLabels[i];
return Container(
height: cellSize,
alignment: AlignmentDirectional.centerStart,
margin: const EdgeInsetsDirectional.all(0.5).add(
const EdgeInsetsDirectional.only(end: 4),
),
padding: const EdgeInsets.only(right: 6),
child: Text(
dayLabel,
textAlign: TextAlign.start,
style: context.textTheme.bodySmall?.copyWith(
color: textColor,
fontSize: 8,
fontWeight: FontWeight.w500,
),
),
);
}),
);
}
}

View File

@ -0,0 +1,47 @@
import 'package:flutter/material.dart';
import 'package:syncrow_web/utils/color_manager.dart';
class OccupancyHeatMapGradient extends StatelessWidget {
const OccupancyHeatMapGradient({super.key, required this.maxValue});
final int maxValue;
List<Color> _heatMapColors() {
if (maxValue == 0) {
return [
ColorsManager.vividBlue.withValues(alpha: 0),
ColorsManager.vividBlue.withValues(alpha: 0),
];
}
return List.generate(
maxValue + 1,
(index) => ColorsManager.vividBlue.withValues(alpha: index / maxValue),
);
}
@override
Widget build(BuildContext context) {
return Row(
children: [
const Spacer(),
Tooltip(
message: 'Min: 0 - Max: $maxValue',
child: Container(
width: 150,
height: 20,
decoration: BoxDecoration(
border: Border.all(
color: ColorsManager.grayBorder,
width: 1,
),
gradient: LinearGradient(
begin: AlignmentDirectional.centerEnd,
end: AlignmentDirectional.centerStart,
colors: _heatMapColors(),
),
),
),
),
],
);
}
}

View File

@ -0,0 +1,60 @@
import 'package:flutter/material.dart';
import 'package:intl/intl.dart';
import 'package:syncrow_web/pages/analytics/modules/occupancy/widgets/occupancy_heat_map_days.dart';
import 'package:syncrow_web/utils/color_manager.dart';
class OccupancyHeatMapMonths extends StatelessWidget {
const OccupancyHeatMapMonths({
required this.startDate,
required this.cellSize,
super.key,
});
final DateTime startDate;
final double cellSize;
@override
Widget build(BuildContext context) {
return Container(
height: 48,
width: double.infinity,
color: Colors.transparent,
child: Row(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.end,
children: [
OccupancyHeatMapDays(
cellSize: cellSize / 3,
textColor: Colors.transparent,
),
...List.generate(12, (monthIndex) {
final monthStartDate = DateTime(startDate.year, monthIndex + 1, 1);
final monthName = DateFormat.MMM().format(monthStartDate);
return Expanded(
child: RotatedBox(
quarterTurns: 3,
child: Container(
padding: EdgeInsetsDirectional.zero,
margin: EdgeInsetsDirectional.zero,
decoration: const BoxDecoration(
border: Border(
top: BorderSide(color: ColorsManager.borderColor),
),
),
width: cellSize * 4,
child: Padding(
padding: const EdgeInsets.only(left: 4, top: 2),
child: Text(
monthName,
style: const TextStyle(fontSize: 8),
),
),
),
),
);
}),
],
),
);
}
}

View File

@ -0,0 +1,84 @@
import 'package:flutter/material.dart';
import 'package:syncrow_web/utils/color_manager.dart';
class OccupancyPaintItem {
final int index;
final int value;
const OccupancyPaintItem({required this.index, required this.value});
}
class OccupancyPainter extends CustomPainter {
OccupancyPainter({
required this.items,
required this.maxValue,
});
final List<OccupancyPaintItem> items;
final int maxValue;
static const double cellSize = 16.0;
@override
void paint(Canvas canvas, Size size) {
final Paint fillPaint = Paint();
final Paint borderPaint = Paint()
..color = ColorsManager.grayBorder.withValues(alpha: 0.4)
..style = PaintingStyle.stroke;
for (final item in items) {
final column = item.index ~/ 7;
final row = item.index % 7;
final x = column * cellSize;
final y = row * cellSize;
fillPaint.color = _getColor(item.value);
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,
);
canvas.drawLine(Offset(x, y), Offset(x, y + cellSize), borderPaint);
canvas.drawLine(
Offset(x + cellSize, y), Offset(x + cellSize, y + cellSize), borderPaint);
}
}
void _drawDashedLine(Canvas canvas, Offset start, Offset end, Paint paint) {
const double dashWidth = 2.0;
const double dashSpace = 4.0;
final double totalLength = (end - start).distance;
final Offset direction = (end - start) / (end - start).distance;
double currentLength = 0.0;
while (currentLength < totalLength) {
final Offset dashStart = start + direction * currentLength;
final double nextLength = currentLength + dashWidth;
final Offset dashEnd =
start + direction * (nextLength < totalLength ? nextLength : totalLength);
canvas.drawLine(dashStart, dashEnd, paint);
currentLength = nextLength + dashSpace;
}
}
Color _getColor(int value) {
if (maxValue == 0) return ColorsManager.vividBlue.withValues(alpha: 0);
final opacity = value.clamp(0, maxValue) / maxValue;
return ColorsManager.vividBlue.withValues(alpha: opacity);
}
@override
bool shouldRepaint(covariant CustomPainter oldDelegate) => false;
}

View File

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

View File

@ -0,0 +1,3 @@
class GetEnergyConsumptionPerDeviceParam {
const GetEnergyConsumptionPerDeviceParam();
}

View File

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

View File

@ -0,0 +1,19 @@
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,
};
}
}

View File

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

View File

@ -0,0 +1,8 @@
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';
abstract interface class EnergyConsumptionByPhasesService {
Future<List<PhasesEnergyConsumption>> load(
GetEnergyConsumptionByPhasesParam param,
);
}

View File

@ -0,0 +1,29 @@
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

@ -0,0 +1,34 @@
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';
import 'package:syncrow_web/services/api/http_service.dart';
final class RemoteEnergyConsumptionByPhasesService
implements EnergyConsumptionByPhasesService {
const RemoteEnergyConsumptionByPhasesService(this._httpService);
final HTTPService _httpService;
@override
Future<List<PhasesEnergyConsumption>> load(
GetEnergyConsumptionByPhasesParam param,
) async {
try {
final response = await _httpService.get(
path: 'endpoint',
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 PhasesEnergyConsumption.fromJson(jsonData);
}).toList();
},
);
return response;
} catch (e) {
throw Exception('Failed to load energy consumption per device: $e');
}
}
}

View File

@ -0,0 +1,8 @@
import 'package:syncrow_web/pages/analytics/models/device_energy_data_model.dart';
import 'package:syncrow_web/pages/analytics/params/get_energy_consumption_per_device_param.dart';
abstract interface class EnergyConsumptionPerDeviceService {
Future<List<DeviceEnergyDataModel>> load(
GetEnergyConsumptionPerDeviceParam param,
);
}

View File

@ -0,0 +1,39 @@
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

@ -0,0 +1,34 @@
import 'package:syncrow_web/pages/analytics/models/device_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';
class RemoteEnergyConsumptionPerDeviceService
implements EnergyConsumptionPerDeviceService {
const RemoteEnergyConsumptionPerDeviceService(this._httpService);
final HTTPService _httpService;
@override
Future<List<DeviceEnergyDataModel>> load(
GetEnergyConsumptionPerDeviceParam param,
) async {
try {
final response = await _httpService.get(
path: 'endpoint',
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();
},
);
return response;
} catch (e) {
throw Exception('Failed to load energy consumption per device: $e');
}
}
}

View File

@ -0,0 +1,19 @@
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,6 @@
import 'package:syncrow_web/pages/analytics/models/occupacy.dart';
import 'package:syncrow_web/pages/analytics/params/get_occupancy_param.dart';
abstract interface class OccupacyService {
Future<List<Occupacy>> load(GetOccupancyParam param);
}

View File

@ -0,0 +1,25 @@
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,6 @@
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';
abstract interface class OccupancyHeatMapService {
Future<List<OccupancyHeatMapModel>> load(GetOccupancyHeatMapParam param);
}

View File

@ -0,0 +1,5 @@
import 'package:syncrow_web/pages/device_managment/power_clamp/models/power_clamp_model.dart';
abstract interface class PowerClampInfoService {
Future<PowerClampModel> getInfo(String deviceId);
}

View File

@ -0,0 +1,27 @@
import 'package:syncrow_web/pages/analytics/services/power_clamp_info/power_clamp_info_service.dart';
import 'package:syncrow_web/pages/device_managment/power_clamp/models/power_clamp_model.dart';
import 'package:syncrow_web/services/api/http_service.dart';
final class RemotePowerClampInfoService implements PowerClampInfoService {
const RemotePowerClampInfoService(this._httpService);
final HTTPService _httpService;
@override
Future<PowerClampModel> getInfo(String deviceId) async {
try {
final response = await _httpService.get(
path: '/devices/$deviceId/functions/status',
showServerMessage: true,
expectedResponseModel: (data) {
final json = data as Map<String, Object?>? ?? {};
final mappedData = json['data'] as Map<String, Object?>? ?? {};
return PowerClampModel.fromJson(mappedData);
},
);
return response;
} catch (e) {
throw Exception('Failed to fetch power clamp info: $e');
}
}
}

View File

@ -0,0 +1,34 @@
import 'package:firebase_database/firebase_database.dart';
import 'package:syncrow_web/pages/analytics/services/realtime_device_service/realtime_device_service.dart';
import 'package:syncrow_web/pages/device_managment/all_devices/models/device_status.dart';
class FirebaseRealtimeDeviceService implements RealtimeDeviceService {
@override
Stream<List<Status>> subscribe(String deviceId) {
try {
final ref = FirebaseDatabase.instance.ref('device-status/$deviceId');
return ref.onValue.asyncMap((event) {
final data = event.snapshot.value as Map<dynamic, dynamic>?;
if (data == null || data['status'] == null) {
throw Exception('Invalid data received from Firebase');
}
final statusMap = data['status'] as List<dynamic>;
return statusMap.map((status) {
if (status is! Map<dynamic, dynamic>) {
throw Exception('Invalid status format');
}
return Status(
code: status['code']?.toString() ?? '',
value: status['value']?.toString() ?? '',
);
}).toList();
});
} catch (e) {
throw Exception('Error subscribing to device status: $e');
}
}
}

View File

@ -0,0 +1,5 @@
import 'package:syncrow_web/pages/device_managment/all_devices/models/device_status.dart';
abstract interface class RealtimeDeviceService {
Stream<List<Status>> subscribe(String deviceId);
}

View File

@ -0,0 +1,50 @@
import 'package:syncrow_web/pages/analytics/models/energy_data_model.dart';
import 'package:syncrow_web/pages/analytics/params/get_total_energy_consumption_param.dart';
import 'package:syncrow_web/pages/analytics/services/total_energy_consumption/total_energy_consumption_service.dart';
import 'package:syncrow_web/services/api/http_service.dart';
class RemoteTotalEnergyConsumptionService implements TotalEnergyConsumptionService {
const RemoteTotalEnergyConsumptionService(this._httpService);
final HTTPService _httpService;
@override
Future<List<EnergyDataModel>> load(
GetTotalEnergyConsumptionParam param,
) async {
try {
final response = await _httpService.get(
path: '/power-clamp/historical',
showServerMessage: true,
queryParameters: param.toJson(),
expectedResponseModel: _TotalEnergyConsumptionResponseMapper.map,
);
return response;
} catch (e) {
throw Exception('Failed to load total energy consumption: $e');
}
}
}
abstract final class _TotalEnergyConsumptionResponseMapper {
const _TotalEnergyConsumptionResponseMapper._();
static List<EnergyDataModel> map(dynamic data) {
final json = data as Map<String, dynamic>? ?? {};
final dailyData = json['data'] as List<dynamic>? ?? [];
return dailyData.map((dayData) {
final date = dayData['date'] as String;
final energyValue = double.tryParse(
dayData['total_energy_consumed_kw'] as String? ?? '0',
) ??
0.0;
return EnergyDataModel(
date: DateTime.parse(date),
value: energyValue,
);
}).toList();
}
}

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