Compare commits

..

245 Commits

Author SHA1 Message Date
f19120c754 enhanced ci/cd by not running the deply jobs on the PR itself, and now we only deploy when we merged a PR to dev or main, and created a separate GitHub action that only builds and install dependencies, which only runs on the PR itself. 2025-06-04 10:12:19 +03:00
6b3eca23af Update pull_request_template.md 2025-05-28 16:46:24 +03:00
4f4f11c330 Merge branch 'main' of https://github.com/SyncrowIOT/web 2025-05-28 14:26:36 +03:00
8a25fa798c Created pull_request_template.md. 2025-05-28 14:26:33 +03:00
6612e91430 Merge pull request #177 from SyncrowIOT/merge_sprint_19_bugfixes
merged DEV into staging.
2025-05-08 14:32:54 +03:00
56c613fb0c Disabled Syncrow Analytics feature for release purposes. 2025-05-08 14:32:08 +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
c114161357 SP-1492. 2025-04-29 14:17:08 +03:00
8d2d9dd0bb Merge branch 'dev' of https://github.com/SyncrowIOT/web 2025-04-29 10:39:54 +03:00
fe1dbb66ac Merge pull request #168 from SyncrowIOT/flush-presence-sensor-routines
fix real time garage door and add flush sensor to routines
2025-04-29 10:25:17 +03:00
ea88f54d20 Refactor garage door control view to use 'doorcontact_state' code for fetching records 2025-04-29 10:22:00 +03:00
ccce7bb671 fix real time garage door and add flush sensor to routines 2025-04-29 10:06:17 +03:00
b4de07de2f Merge pull request #167 from SyncrowIOT:bugfix/fix-repeated-duplication
fixed repeated duplication
2025-04-29 10:14:03 +04:00
acefe6b433 fixed repeated duplication 2025-04-29 10:13:11 +04:00
63bc7a56de - Refactor the code in _RoutinesViewState to improve readability and maintainability.
- Update the indentation and add padding to the child widgets in the Column.
- Add a bottom padding to the empty state text in _buildEmptyState.
2025-04-28 16:49:22 +03:00
7b3635deae Merge pull request #166 from SyncrowIOT/bugfix/clear-search
fixed community search caching
2025-04-28 16:27:38 +04:00
58755eafe1 Merge branch 'dev' of https://github.com/SyncrowIOT/web into bugfix/clear-search 2025-04-28 16:26:47 +04:00
ce225818fb fixed community search caching 2025-04-28 16:25:43 +04:00
8762a7aaa8 Merge pull request #165 from SyncrowIOT/bugfix/white-page-rendering 2025-04-28 15:22:37 +04:00
8dc833b2c3 fixed blank page rendering 2025-04-28 15:21:28 +04:00
13cef151aa Merge pull request #164 from SyncrowIOT/bugfix/space-model-with-tags 2025-04-28 14:37:26 +04:00
ab23be9828 fixed the issue in selecting space model and tag 2025-04-28 14:36:27 +04:00
687b68ab22 Merge pull request #163 from SyncrowIOT/fix/duplication-flatten
Fix/duplication-flatten
2025-04-28 13:00:17 +04:00
25614c3dd0 fix the flatten 2025-04-28 12:59:16 +04:00
7cbe20ae88 remove unused code 2025-04-28 12:56:29 +04:00
349fe6c555 realignment 2025-04-28 11:58:41 +04:00
9779f3783c Merge branch 'dev' of https://github.com/SyncrowIOT/web into dev 2025-04-28 10:03:31 +04:00
fe3db663b6 realign initially 2025-04-28 10:03:27 +04:00
888d444752 Merge pull request #160 from SyncrowIOT/SP-1478-FE-On-devices-management-page-when-we-open-power-clamp-device-loading-indicator-remains-loading-and-no-data-is-displayed
Sp 1478 fe on devices management page when we open power clamp device loading indicator remains loading and no data is displayed
2025-04-28 08:59:17 +03:00
bab5e06968 Merge pull request #159 from SyncrowIOT/SP-1463-rework
Sp 1463 rework
2025-04-28 08:58:39 +03:00
d7b6174dee Merge pull request #162 from SyncrowIOT:bugfix/save-spaces
fixed the save issue
2025-04-28 00:37:33 +04:00
6ef0b2f9d1 fixed the save issue 2025-04-28 00:36:58 +04:00
3ceb03826e Merge pull request #161 from SyncrowIOT/bugfix/searchquery 2025-04-27 22:44:57 +04:00
52608b1f35 fixed search query 2025-04-27 22:42:57 +04:00
ac2996629e resolved an exception that was thrown when resizing the DeviceSearchFilters. 2025-04-27 15:42:50 +03:00
51c52c66cb SP-1478-FE-On-devices-management-page-when-we-open-power-clamp-device-loading-indicator-remains-loading-and-no-data-is-displayed 2025-04-27 15:18:19 +03:00
0f56273d99 SP-1408 2025-04-27 12:10:38 +03:00
34d4d892d9 refactor: streamline value calculations in FlushMountedPresenceSensorControlView 2025-04-27 11:11:38 +03:00
3193fd26fe refactor: update presence delay value and enhance request handling in DebouncedBatchControlDevicesService 2025-04-27 11:04:54 +03:00
43802a9fad refactor: update detection value calculations and adjust parameters for presence delay 2025-04-27 10:57:44 +03:00
6e0b1775f0 fix: reorder constructor parameters for consistency in FlushMountedPresenceSensorControlView 2025-04-27 10:55:18 +03:00
233fb2ee2c refactor: improve formatting of clamp method for near and far detection values 2025-04-27 10:55:03 +03:00
b26928b3d5 fixed ui bugs. 2025-04-27 10:14:35 +03:00
6fc35a7b9a enhanced device debouncing to accomodate for multiple calls from the different devices functions calls. 2025-04-27 10:14:19 +03:00
756457927c removed unnecessary isBatch flag from FlushMountedPresenceSensorChangeValueEvent. 2025-04-27 10:13:53 +03:00
f30d7c0117 Merge pull request #158 from SyncrowIOT:bugfix/duplicate-space
Bugfix/duplicate-space
2025-04-27 11:13:07 +04:00
976d6e385a duplicated space 2025-04-27 11:12:03 +04:00
ff07e7509d fixed the issue in aligning child space 2025-04-26 16:04:41 +04:00
17a582ab99 Merge pull request #157 from SyncrowIOT/SP-1415 2025-04-25 10:50:06 +04:00
09fb604acc added filtering 2025-04-25 10:49:25 +04:00
2068df173d Merge pull request #155 from SyncrowIOT/SP-1441-rework-FE-On-routine-creation-page-When-the-user-drags-a-card-that-has-signs-and-selects-a-sign-without-a-number-then-confirms-the-value-appears-to-be-Null
Sp 1441 rework fe on routine creation page when the user drags a card that has signs and selects a sign without a number then confirms the value appears to be null
2025-04-24 16:25:39 +03:00
bfc2a381d2 Merge pull request #156 from SyncrowIOT/SP-1464-FE-implement-Batch-Control-Dialog
Sp 1464 fe implement batch control dialog
2025-04-24 16:25:17 +03:00
778257644d reduced debounce duration. 2025-04-24 15:13:10 +03:00
c8e540e938 Remove unnecessary event dispatch in FlushMountedPresenceSensorBlocFactory creation 2025-04-24 14:29:18 +03:00
ba20998067 disabled realtime on batch control. 2025-04-24 14:21:38 +03:00
75b0b24543 Add Flush Mounted Presence Sensor support and update event handling 2025-04-24 14:13:13 +03:00
c03b8f290c refactor function tap handlers to use RoutineTapFunctionHelper for improved code reuse and readability, and to remove code duplication. 2025-04-24 10:25:41 +03:00
2c684a9495 SP-1441 rework. 2025-04-23 16:58:50 +03:00
fbc45b465f Merge pull request #153 from SyncrowIOT/SP-1344-FE-Real-Time-Issues-Ceiling-Sensor-AC-and-Garage-Door-Sensor
Refactor widget lifecycle methods for temperature control and presenc…
2025-04-23 16:29:36 +03:00
c9c939c59f Merge pull request #154 from SyncrowIOT/Add-Flush-Mounted-Presence-Sensor-Single-Control
Add flush mounted presence sensor single control
2025-04-23 16:21:17 +03:00
e1a2465130 Fix deviceId assignment in FlushMountedPresenceSensorBlocFactory and update _listenToChanges method for async handling 2025-04-23 13:11:25 +03:00
86164e746a Refactor FlushMountedPresenceSensorBloc creation to use factory method and streamline dependency injection 2025-04-23 12:15:50 +03:00
4a5176cf22 Refactor presence update data handling for improved precision and scaling 2025-04-23 12:11:13 +03:00
2bb7a6950a Refactor debounce duration handling in BatchControlDevicesService and ControlDeviceService 2025-04-23 10:52:08 +03:00
46860619a0 Add bloc dependency to pubspec.yaml 2025-04-23 10:48:24 +03:00
d1bb7b129f Refactor widget lifecycle methods for temperature control and presence sensor 2025-04-23 10:46:56 +03:00
7adce3b94c Refactor _onFlushMountedPresenceSensorFetchBatchStatusEvent to use final for response variable 2025-04-23 10:44:44 +03:00
91f93d4395 Refactor FlushMountedPresenceSensorBloc to streamline device control logic and remove redundant code 2025-04-23 10:44:11 +03:00
42d6b64e58 Refactor FlushMountedPresenceSensorBloc to replace _runDeBouncer with _controlDevice for handling device control logic 2025-04-23 10:32:49 +03:00
b11d4186fb Add BatchControlDevicesService integration to FlushMountedPresenceSensorBloc and ControlView 2025-04-23 10:31:26 +03:00
24130be665 organized instances in bloc. 2025-04-23 10:26:55 +03:00
8c960bd5f1 created BatchControlDevicesService interface. 2025-04-23 10:26:17 +03:00
fb8ccdf0a6 Add FlushMountedPresenceSensorControlView for managing presence sensor settings 2025-04-23 10:24:38 +03:00
1975a1b6f4 created FlushMountedPresenceSensorBloc, events, and states for device management. 2025-04-23 10:24:14 +03:00
367d6717e7 Refactor PresenceUpdateData widget to support decimal values. 2025-04-23 10:23:28 +03:00
8c637e40ff Created FlushMountedPresenceSensorModel model. 2025-04-23 10:23:00 +03:00
05b3180510 Created ControlDeviceService interface and its remote and debounced implementation. 2025-04-23 10:22:41 +03:00
177a4711fe Synced main_dev and main_staging with main.dart 2025-04-22 03:39:27 +03:00
d7a37c6519 Removed flavor from Flutter run and build commands 2025-04-22 03:01:43 +03:00
b901791079 Update Flutter run and build commands, and added main_staging file 2025-04-22 02:33:50 +03:00
5d16555748 Merge pull request #151 from SyncrowIOT/SP-1447-FE-Invalid-Time-Limit-24h-Exceeding-Backend-Max-of-12h
SP-1447.
2025-04-21 18:59:57 +03:00
bd15f30b8a Merge pull request #150 from SyncrowIOT/SP-1440-FE-On-routine-creation-edit-Page-When-saving-a-routine-the-confirmation-pop-up-is-not-identical-to-design
Sp 1440 fe on routine creation edit page when saving a routine the confirmation pop up is not identical to design
2025-04-21 18:59:37 +03:00
5f2684bdf4 Merge pull request #152 from SyncrowIOT/SP-702-WEB-Username-Caching-After-Logout-Requires-Screen-Refresh
Sp 702 web username caching after logout requires screen refresh
2025-04-21 18:58:57 +03:00
b4ef22ef0a SP-702/ clears user data on logging out. 2025-04-21 14:23:40 +03:00
d8afb562eb SP-1447, set the limit of gangs countdown to be 12 hours instead of 24 hours. 2025-04-21 12:07:21 +03:00
67a164e6d2 modified divider color to match figma design. 2025-04-21 10:59:09 +03:00
cf20bdcd42 SP-1440 2025-04-21 10:57:46 +03:00
32b45ea5d7 Merge pull request #149 from SyncrowIOT/SP-1441-FE-On-routine-creation-page-When-the-user-drags-a-card-that-has-signs-and-selects-a-sign-without-a-number-then-confirms-the-value-appears-to-be-Null
Sp 1441 fe on routine creation page when the user drags a card that has signs and selects a sign without a number then confirms the value appears to be null
2025-04-21 10:32:38 +03:00
62fb8b3097 SP-1441 2025-04-21 10:28:44 +03:00
065bd33511 SP-1330. 2025-04-21 10:03:02 +03:00
b9ab782c01 Merge pull request #148 from SyncrowIOT/SP-1435-FE-On-routines-page-when-the-screen-height-is-decreased-or-is-small-the-scroll-area-for-routine-cards-is-not-fitting-the-whole-screen-width
Sp 1435 fe on routines page when the screen height is decreased or is small the scroll area for routine cards is not fitting the whole screen width
2025-04-21 09:49:08 +03:00
4d5adf948c Merge pull request #147 from SyncrowIOT/SP-1433-FE-Text-Alignment-Issue-in-UI-Component-in-adding-subspace-in-a-space
Sp 1433 fe text alignment issue in UI component in adding subspace in a space
2025-04-21 09:48:38 +03:00
e45f57ca03 SP-1441 2025-04-17 16:55:44 +03:00
84264391d9 progress towards matching the design of save routine dialog. 2025-04-17 16:14:26 +03:00
2e4f904d3a style: improve code formatting and readability in SaveRoutineHelper 2025-04-17 14:39:33 +03:00
c46cfb48a8 refactor: extract IF and THEN sections into separate methods for better readability 2025-04-17 14:38:04 +03:00
a0dd128557 matched design of Function and action in SaveRoutineHelper.showSaveRoutineDialog 2025-04-17 14:30:30 +03:00
34fa426163 submitting password field in login logs the user in. 2025-04-17 14:17:43 +03:00
1407c173b0 tapping bugfix. 2025-04-17 13:28:00 +03:00
9431eb79c1 Fix loading state handling and refactor scene/automation rendering in FetchRoutineScenesAutomation 2025-04-17 13:18:44 +03:00
70f1f39fce Improve loading state handling and in FetchRoutineScenesAutomation. 2025-04-17 13:13:48 +03:00
f912b41fd8 Refactor visibility handling for scenes and automations in FetchRoutineScenesAutomation 2025-04-17 13:12:09 +03:00
977875f1f2 SP-1435 2025-04-17 13:08:16 +03:00
8136804694 bugfix. 2025-04-17 12:18:09 +03:00
ce253b2034 Remove unused parameters from CreateSubSpaceDialog constructor 2025-04-17 11:27:37 +03:00
024fbcdb83 Refactor CreateSubSpaceDialog to convert to StatefulWidget and manage text controller lifecycle 2025-04-17 11:25:39 +03:00
a8430a7d3d Merge pull request #145 from SyncrowIOT/SP-1330-FE-Side-tree-text-breaks-incorrectly-causing-layout-issues
Sp 1330 fe side tree text breaks incorrectly causing layout issues
2025-04-17 09:49:33 +03:00
7ef6020dd8 Merge pull request #144 from SyncrowIOT/SP-1333-FE-set-barrier-dismissable-to-true
Sp 1333 fe set barrier dismissable to true
2025-04-17 09:49:11 +03:00
2a77483f46 Refactor CreateSubSpaceDialog to improve widget structure and readability 2025-04-17 09:45:14 +03:00
a6fc99443b Refactor CreateSubSpaceDialog layout for improved readability and maintainability 2025-04-17 09:38:54 +03:00
ae95d06482 Fix constructor parameter order in CreateSubSpaceDialog 2025-04-17 09:35:47 +03:00
18c886753d added trailing commas wherever neccessary in CreateSubSpaceDialog. 2025-04-17 09:35:33 +03:00
62bf4f2944 Refactor CreateSubSpaceDialog to use context extension for screen width calculations 2025-04-17 09:34:03 +03:00
726c173a76 SP-1433-FE-Text-Alignment-Issue-in-UI-Component-in-adding-subspace-in-a-space 2025-04-17 09:33:10 +03:00
d538b3667e Merge pull request #146 from SyncrowIOT/Fix-Factory-Reset-Model
Refactor FactoryResetModel and MainDoorSensorBatchView
2025-04-17 09:18:43 +03:00
72ae3b1727 Refactor FactoryResetModel and MainDoorSensorBatchView
- Refactor FactoryResetModel to include 'operationType' in toJson and toMap methods.
- Refactor MainDoorSensorBatchView to use BlocProvider and Builder for better state management.
2025-04-16 15:06:50 +03:00
9044645f95 remove screenWidth parameter from TagChipDisplay and use context.screenWidth instead. 2025-04-15 15:45:47 +03:00
7699453e6d moved styling of _buildChip up, and removed unnecessary SizedBox. 2025-04-15 15:43:50 +03:00
d1a21be983 removed else from TagChipDisplay.build 2025-04-15 15:35:24 +03:00
db8e5a4aa6 Refactor TagChipDisplay._groupedTags to enhance readabaility. 2025-04-15 15:32:31 +03:00
fa5bb350c3 refactor: replace spaceNameController with spaceName in TagChipDisplay. 2025-04-15 15:29:25 +03:00
920827d763 Removed unnecessary SizedBox from TagChipDisplay. 2025-04-15 15:28:12 +03:00
d3902d622e Moved constructor to be the first element in TagChipDisplay. 2025-04-15 15:27:45 +03:00
a4432656ab refactor: extract EditChip into a private method for improved readability 2025-04-15 15:27:04 +03:00
90e0d2f52b Extracted Chip into a private method. 2025-04-15 15:24:36 +03:00
08e5e17910 Extracted Add Devices button into a private method. 2025-04-15 15:23:36 +03:00
f57348e5cd converted to using expressions wherever possible in TagChipDisplay. 2025-04-15 15:13:47 +03:00
be168aed93 refactor: simplify tag checking logic to enhance readability. 2025-04-15 15:08:30 +03:00
a66784473f Replaced conditional with an if statement in TagChipDisplay. 2025-04-15 15:06:01 +03:00
c0a963ded5 refactor: use context extension for text theme. 2025-04-15 15:05:31 +03:00
7945cefe53 added trailing commas wherever necessary. 2025-04-15 15:05:06 +03:00
7d0e50fb1d removed unnecessary comments. 2025-04-15 15:03:27 +03:00
117f6190dd removed unnecessary BuildContext from TagChipDisplay constructor, sorted its properies, and converted to using super.key. 2025-04-15 15:02:58 +03:00
748c67fd8b SP-1333 2025-04-15 14:52:20 +03:00
cfc68f1568 Merge pull request #116 from SyncrowIOT/dev
fix real time listenToChanges
2025-03-12 21:26:45 +03:00
02e08ad92f Merge pull request #115 from SyncrowIOT/dev
Dev
2025-03-12 14:21:13 +03:00
d7899a24f5 Merged with dev 2025-02-20 13:03:38 +03:00
800c0ba47f Merge pull request #101 from SyncrowIOT/bugfix/fix-endpoint
Main
2025-02-20 13:37:28 +04:00
fe4e775902 fixed endpoint 2025-02-20 13:35:59 +04:00
5247856cb4 Merge pull request #99 from SyncrowIOT:bugfix/add-tag-border
added back border of tag list
2025-02-20 11:50:07 +04:00
4a8b8a32ba added back border of tag list 2025-02-20 11:49:35 +04:00
2abce77eb5 Merge pull request #97 from SyncrowIOT/feat/fix-cursor-issue-in-main
Fixed cursor issue
2025-02-20 11:35:35 +04:00
7efd1c3c87 Fixed cursor issue 2025-02-20 11:34:24 +04:00
7a0d9aefb7 Merge pull request #95 from SyncrowIOT/bugfix/change-endpoint-prod
fix endpoints
2025-02-19 18:01:11 +04:00
21cc25cfc4 fix endpoints 2025-02-19 17:58:53 +04:00
e2ec4bbf31 Pulled main changes 2025-02-18 16:27:09 +03:00
51b46ae197 Merged with dev 2025-02-18 16:25:33 +03:00
36ee22603a fixes CommunityId and spaceUuid 2025-02-10 12:44:35 +03:00
b0abd42b0c projectId 2025-02-06 11:28:40 +03:00
ba4da78846 Merge branch 'dev' 2025-02-06 11:20:34 +03:00
dc20d69f20 Merge pull request #88 from SyncrowIOT/dev
Dev
2025-02-06 01:03:32 +03:00
cf6ec231dc Merged with dev 2025-02-06 00:57:29 +03:00
d0530f7fc3 Added staging space and community IDs 2024-12-04 09:57:29 +03:00
175 changed files with 7808 additions and 1529 deletions

26
.github/pull_request_template.md vendored Normal file
View File

@ -0,0 +1,26 @@
<!--
Thanks for contributing!
Provide a description of your changes below and a general summary in the title
Please look at the following checklist to ensure that your PR can be accepted quickly:
-->
## Jira Ticket
[SP-0000](https://syncrow.atlassian.net/browse/SP-0000)
## Description
<!--- Describe your changes in detail -->
## Type of Change
<!--- Put an `x` in all the boxes that apply: -->
- [ ] ✨ New feature (non-breaking change which adds functionality)
- [ ] 🛠️ Bug fix (non-breaking change which fixes an issue)
- [ ] ❌ Breaking change (fix or feature that would cause existing functionality to change)
- [ ] 🧹 Code refactor
- [ ] ✅ Build configuration change
- [ ] 📝 Documentation
- [ ] 🗑️ Chore

View File

@ -4,10 +4,6 @@ on:
push:
branches:
- main
pull_request:
types: [opened, synchronize, reopened, closed]
branches:
- main
jobs:
build_and_deploy_job:
@ -31,7 +27,7 @@ jobs:
run: flutter pub get
- name: Build Flutter Web App
run: flutter build web --release --dart-define=FLAVOR=production
run: flutter build web --web-renderer canvaskit -t lib/main.dart
- name: Build And Deploy
id: builddeploy

View File

@ -4,18 +4,12 @@ on:
push:
branches:
- dev
pull_request:
types: [opened, synchronize, reopened, closed]
branches:
- dev
jobs:
build_and_deploy_job:
if: github.event_name == 'push' || (github.event_name == 'pull_request' && github.event.action != 'closed')
runs-on: ubuntu-latest
name: Build and Deploy Job
steps:
- name: Checkout Code
uses: actions/checkout@v3
with:
@ -31,7 +25,7 @@ jobs:
run: flutter pub get
- name: Build Flutter Web App
run: flutter build web --release --dart-define=FLAVOR=development
run: flutter build web --web-renderer canvaskit -t lib/main_dev.dart
- name: Build And Deploy
id: builddeploy

29
.github/workflows/pr-check.yml vendored Normal file
View File

@ -0,0 +1,29 @@
name: Pull Request Check
on:
pull_request:
branches:
- dev
- main
jobs:
setup_flutter:
runs-on: ubuntu-latest
name: Setup Flutter and Dependencies
steps:
- name: Checkout Code
uses: actions/checkout@v3
with:
submodules: true
lfs: false
- name: Set up Flutter
uses: subosito/flutter-action@v2
with:
flutter-version: '3.27.3'
- name: Install dependencies
run: flutter pub get
- name: Run Flutter Build
run: flutter build web --web-renderer canvaskit -t lib/main_dev.dart

33
.vscode/launch.json vendored
View File

@ -10,11 +10,12 @@
"type": "dart",
"args": [
"--dart-define",
"FLAVOR=development"
"-d",
"chrome",
"--web-port",
"3000",
"-t",
"lib/main_dev.dart",
],
"flutterMode": "debug"
@ -28,11 +29,12 @@
"type": "dart",
"args": [
"--dart-define",
"FLAVOR=staging"
"-d",
"chrome",
"--web-port",
"3000",
"-t",
"lib/main_staging.dart",
],
"flutterMode": "debug"
@ -46,11 +48,12 @@
"type": "dart",
"args": [
"--dart-define",
"FLAVOR=production"
"-d",
"chrome",
"--web-port",
"3000",
"-t",
"lib/main.dart",
],
"flutterMode": "debug"

View File

@ -16,7 +16,12 @@ samples, guidance on mobile development, and a full API reference.
## USEFUL COMMANDS
Run on chrome: flutter run -d chrome --dart-define=FLAVOR='ENV_NAME'
- Building for the Web
- CanvasKit
- `flutter build web --web-renderer canvaskit -t lib/main_dev.dart --output=build/web_dev` - build for DEVELOPMENT.
- `flutter build web --web-renderer canvaskit -t lib/main_staging.dart --output=build/web_stg` - build for STAGING.
- `flutter build web --web-renderer canvaskit -t lib/main.dart --output=build/web` - build for PRODUCTION.
- run command: `flutter run -d chrome --target=lib/main_dev.dart`
Build: flutter build web --release --dart-define=FLAVOR='ENV_NAME'

View File

@ -26,6 +26,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,17 @@
<svg width="22" height="22" viewBox="0 0 22 22" fill="none" xmlns="http://www.w3.org/2000/svg">
<g clip-path="url(#clip0_8194_10048)">
<path d="M11.4732 16.7316C8.31096 16.7316 5.73828 14.1589 5.73828 10.9967C5.73828 7.8344 8.31096 5.26172 11.4732 5.26172C14.6355 5.26172 17.2081 7.8344 17.2081 10.9967C17.2081 14.1589 14.6355 16.7316 11.4732 16.7316Z" fill="#EBF6FF"/>
<path d="M17.2076 10.9967C17.2076 7.8344 14.6349 5.26172 11.4727 5.26172V16.7316C14.6349 16.7316 17.2076 14.1589 17.2076 10.9967Z" fill="#D7E7F8"/>
<path d="M11.4721 17.3699C7.95827 17.3699 5.09961 14.5112 5.09961 10.9974C5.09961 7.48362 7.95827 4.625 11.472 4.625C14.9858 4.625 17.8445 7.48366 17.8445 10.9974C17.8445 14.5112 14.9858 17.3699 11.4721 17.3699ZM11.4721 5.89473C8.6584 5.89473 6.36934 8.18379 6.36934 10.9974C6.36934 13.811 8.6584 16.1001 11.472 16.1001C14.2857 16.1001 16.5748 13.8111 16.5748 10.9974C16.5748 8.18375 14.2857 5.89473 11.4721 5.89473Z" fill="#9FE066"/>
<path d="M11.4728 11.6318H8.28525C7.93464 11.6318 7.65039 11.3475 7.65039 10.9969C7.65039 10.6463 7.93464 10.3621 8.28525 10.3621H10.8379V8.95127C10.8379 8.60066 11.1222 8.31641 11.4728 8.31641C11.8234 8.31641 12.1076 8.60066 12.1076 8.95127V10.9969C12.1077 11.3475 11.8234 11.6318 11.4728 11.6318Z" fill="#394949"/>
<path d="M12.1075 10.9969V8.95127C12.1075 8.60066 11.8233 8.31641 11.4727 8.31641V11.6318C11.8233 11.6318 12.1075 11.3475 12.1075 10.9969Z" fill="#151F1F"/>
<path d="M16.5753 10.9974C16.5753 13.8111 14.2863 16.1001 11.4727 16.1001V17.3698C14.9865 17.3698 17.8451 14.5112 17.8451 10.9974C17.8451 7.48366 14.9864 4.625 11.4727 4.625V5.89473C14.2863 5.89473 16.5753 8.18379 16.5753 10.9974Z" fill="#4ACA7B"/>
<path d="M18.6832 3.78752C16.7572 1.86147 14.1963 0.800781 11.4725 0.800781C8.74869 0.800781 6.18782 1.86147 4.26177 3.78752C2.67775 5.37158 1.67966 7.38516 1.37492 9.56545L1.08378 9.2743C0.835882 9.02641 0.433887 9.02641 0.185951 9.2743C-0.0619838 9.52219 -0.0619838 9.92419 0.185951 10.1721L1.46097 11.4471C1.58494 11.5711 1.74742 11.6331 1.90986 11.6331C2.0723 11.6331 2.23482 11.5711 2.35875 11.4471L3.63377 10.1721C3.8817 9.92423 3.8817 9.52224 3.63377 9.2743C3.38587 9.02641 2.98388 9.02641 2.73594 9.2743L2.70678 9.30346C3.50083 5.18845 7.12907 2.07051 11.4725 2.07051C16.3953 2.07051 20.4003 6.07548 20.4003 10.9983C20.4003 15.921 16.3953 19.926 11.4725 19.926C7.46868 19.926 3.9294 17.234 2.86558 13.3794C2.77234 13.0413 2.42266 12.8429 2.08474 12.9363C1.74674 13.0295 1.54837 13.3792 1.64165 13.7171C2.22378 15.8264 3.50315 17.7286 5.24408 19.0732C7.04184 20.4618 9.19555 21.1957 11.4725 21.1957C14.1963 21.1957 16.7571 20.135 18.6832 18.209C20.6093 16.2829 21.67 13.7221 21.67 10.9983C21.67 8.27439 20.6093 5.71361 18.6832 3.78752Z" fill="#FF4D5B"/>
<path d="M20.4004 10.9983C20.4004 15.9211 16.3955 19.926 11.4727 19.926V21.1958C14.1965 21.1958 16.7573 20.135 18.6834 18.209C20.6095 16.2829 21.6702 13.7221 21.6702 10.9983C21.6702 8.27439 20.6094 5.71361 18.6834 3.78752C16.7573 1.86147 14.1965 0.800781 11.4727 0.800781V2.07051C16.3955 2.07051 20.4004 6.07548 20.4004 10.9983Z" fill="#DE0062"/>
</g>
<defs>
<clipPath id="clip0_8194_10048">
<rect width="21.67" height="21.67" fill="white" transform="translate(0 0.164062)"/>
</clipPath>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 3.2 KiB

View File

@ -0,0 +1,13 @@
<svg width="22" height="22" viewBox="0 0 22 22" fill="none" xmlns="http://www.w3.org/2000/svg">
<g clip-path="url(#clip0_8194_10065)">
<path d="M17.6069 21.8341H4.06313C1.81893 21.8341 0 20.0151 0 17.7709V4.22719C0 1.98299 1.81893 0.164062 4.06313 0.164062H17.6069C19.8511 0.164062 21.67 1.98299 21.67 4.22719V17.7709C21.67 20.0151 19.8511 21.8341 17.6069 21.8341Z" fill="#E3F8FA"/>
<path d="M15.1312 7.18375L10.955 5.60387C10.8771 5.57475 10.7925 5.57475 10.7153 5.60387L6.53904 7.18375C6.40767 7.23386 6.32031 7.35982 6.32031 7.50067V9.30605C6.32031 9.41711 6.37516 9.5214 6.46591 9.58438C6.55868 9.64736 6.67516 9.6609 6.77877 9.6223L10.8351 8.08779L14.8915 9.62298C14.9301 9.6372 14.9707 9.64465 15.0113 9.64465C15.0791 9.64465 15.1461 9.62433 15.2043 9.58438C15.2958 9.5214 15.3499 9.41711 15.3499 9.30605V7.5C15.3499 7.35914 15.2626 7.23318 15.1312 7.18375Z" fill="#8CE1EB"/>
<path d="M15.1312 13.9572L10.955 12.3773C10.8771 12.3482 10.7925 12.3482 10.7153 12.3773L6.53904 13.9572C6.40767 14.0073 6.32031 14.1333 6.32031 14.2741V16.0795C6.32031 16.1906 6.37516 16.2948 6.46591 16.3578C6.55868 16.4215 6.67516 16.435 6.77877 16.3957L10.8351 14.8612L14.8915 16.3964C14.9301 16.4106 14.9707 16.4181 15.0113 16.4181C15.0791 16.4181 15.1461 16.3978 15.2043 16.3578C15.2958 16.2948 15.3499 16.1906 15.3499 16.0795V14.2734C15.3499 14.1326 15.2626 14.0066 15.1312 13.9572Z" fill="#26C6DA"/>
<path d="M15.1312 10.5705L10.955 8.99059C10.8771 8.96147 10.7925 8.96147 10.7153 8.99059L6.53904 10.5705C6.40767 10.6206 6.32031 10.7465 6.32031 10.8874V12.6928C6.32031 12.8038 6.37516 12.9081 6.46591 12.9711C6.55868 13.0348 6.67516 13.0483 6.77877 13.009L10.8351 11.4745L14.8915 13.0097C14.9301 13.0239 14.9707 13.0314 15.0113 13.0314C15.0791 13.0314 15.1461 13.0111 15.2043 12.9711C15.2958 12.9081 15.3499 12.8038 15.3499 12.6928V10.8867C15.3499 10.7459 15.2626 10.6199 15.1312 10.5705Z" fill="#26C6DA"/>
</g>
<defs>
<clipPath id="clip0_8194_10065">
<rect width="21.67" height="21.67" fill="white" transform="translate(0 0.164062)"/>
</clipPath>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 2.0 KiB

View File

@ -0,0 +1,21 @@
<svg width="22" height="22" viewBox="0 0 22 22" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M20.7802 9.0399L20.0667 9.18046C19.7057 9.25157 19.4663 9.5894 19.5103 9.95466C19.6254 10.9106 19.5871 11.9137 19.3549 12.9433C18.5334 16.5849 15.4143 19.3394 11.6982 19.6957C6.22309 20.2206 1.65927 15.6884 2.13085 10.223C2.48684 6.09727 5.78343 2.73681 9.90242 2.3098C12.864 2.00274 15.5631 3.17762 17.3531 5.18175L18.396 4.25096C16.4588 2.08197 13.6036 0.750365 10.4397 0.87082C5.29437 1.06665 1.04625 5.19944 0.720226 10.3382C0.34608 16.2355 5.01846 21.1352 10.8352 21.1352C16.4331 21.1352 20.9712 16.5971 20.9712 10.9992C20.9712 10.3288 20.9044 9.674 20.7802 9.0399Z" fill="#FFF0D2"/>
<path d="M10.8355 12.7451C11.8006 12.7451 12.583 11.9627 12.583 10.9976C12.583 10.0324 11.8006 9.25 10.8355 9.25C9.8703 9.25 9.08789 10.0324 9.08789 10.9976C9.08789 11.9627 9.8703 12.7451 10.8355 12.7451Z" fill="#FFF0D2"/>
<path d="M10.8358 16.5911C10.6426 16.5911 10.4863 16.4346 10.4863 16.2416V10.9989C10.4863 10.9062 10.5232 10.8173 10.5887 10.7517L14.4334 6.90709C14.5699 6.77055 14.7911 6.77055 14.9276 6.90709C15.0642 7.04363 15.0642 7.26482 14.9276 7.40131L11.1854 11.1436V16.2416C11.1854 16.4347 11.029 16.5911 10.8358 16.5911Z" fill="#7A4646"/>
<path d="M10.8356 12.0463C11.4147 12.0463 11.8842 11.5769 11.8842 10.9978C11.8842 10.4187 11.4147 9.94922 10.8356 9.94922C10.2566 9.94922 9.78711 10.4187 9.78711 10.9978C9.78711 11.5769 10.2566 12.0463 10.8356 12.0463Z" fill="#AA6E4D"/>
<path d="M10.8358 4.35899C10.6426 4.35899 10.4863 4.20248 10.4863 4.00948V3.31045C10.4863 3.11745 10.6427 2.96094 10.8358 2.96094C11.0291 2.96094 11.1854 3.11745 11.1854 3.31045V4.00948C11.1854 4.20248 11.029 4.35899 10.8358 4.35899Z" fill="#DE966C"/>
<path d="M10.8358 19.0387C10.6426 19.0387 10.4863 18.8822 10.4863 18.6892V17.9901C10.4863 17.7971 10.6427 17.6406 10.8358 17.6406C11.0291 17.6406 11.1854 17.7971 11.1854 17.9901V18.6892C11.1854 18.8822 11.029 19.0387 10.8358 19.0387Z" fill="#DE966C"/>
<path d="M18.5251 11.3475H17.8261C17.6329 11.3475 17.4766 11.1909 17.4766 10.998C17.4766 10.805 17.6329 10.6484 17.8261 10.6484H18.5251C18.7183 10.6484 18.8746 10.805 18.8746 10.998C18.8746 11.1909 18.7183 11.3475 18.5251 11.3475Z" fill="#DE966C"/>
<path d="M3.84546 11.3475H3.14639C2.95318 11.3475 2.79688 11.1909 2.79688 10.998C2.79688 10.805 2.95322 10.6484 3.14639 10.6484H3.84541C4.03863 10.6484 4.19493 10.805 4.19493 10.998C4.19493 11.1909 4.03863 11.3475 3.84546 11.3475Z" fill="#DE966C"/>
<path d="M4.78099 7.85338C4.72161 7.85338 4.66151 7.83836 4.60657 7.80661L4.00108 7.4571C3.83382 7.36051 3.77651 7.14682 3.87309 6.9796C3.96968 6.81233 4.18371 6.75502 4.35059 6.85161L4.95608 7.20112C5.12335 7.29771 5.18066 7.5114 5.08407 7.67862C5.01923 7.79074 4.90178 7.85338 4.78099 7.85338Z" fill="#F7B97E"/>
<path d="M17.4939 15.1933C17.4345 15.1933 17.3744 15.1782 17.3195 15.1465L16.714 14.797C16.5467 14.7004 16.4894 14.4867 16.586 14.3195C16.6826 14.152 16.8966 14.0949 17.0635 14.1915L17.669 14.541C17.8362 14.6376 17.8935 14.8513 17.797 15.0185C17.7321 15.1306 17.6147 15.1933 17.4939 15.1933Z" fill="#F7B97E"/>
<path d="M14.3298 5.29299C14.2704 5.29299 14.2103 5.27796 14.1554 5.24622C13.9881 5.14964 13.9308 4.93594 14.0274 4.76872L14.3769 4.16323C14.4735 3.99579 14.6875 3.93866 14.8544 4.03524C15.0217 4.13182 15.079 4.34552 14.9824 4.51274L14.6329 5.11823C14.568 5.23035 14.4506 5.29299 14.3298 5.29299Z" fill="#F7B97E"/>
<path d="M6.98995 18.0077C6.93057 18.0077 6.87047 17.9927 6.81553 17.961C6.64827 17.8644 6.59096 17.6507 6.68755 17.4834L7.03706 16.878C7.13331 16.7107 7.34768 16.6536 7.51456 16.75C7.68183 16.8466 7.73913 17.0602 7.64255 17.2275L7.29304 17.833C7.2282 17.9451 7.11079 18.0077 6.98995 18.0077Z" fill="#F7B97E"/>
<path d="M16.8891 7.85337C16.7682 7.85337 16.6508 7.79073 16.586 7.67861C16.4894 7.51135 16.5467 7.2977 16.714 7.20111L17.3195 6.8516C17.486 6.75502 17.7004 6.81236 17.797 6.97959C17.8935 7.14685 17.8362 7.36051 17.669 7.45709L17.0635 7.8066C17.0085 7.83835 16.9485 7.85337 16.8891 7.85337Z" fill="#F7B97E"/>
<path d="M4.17617 15.1933C4.05533 15.1933 3.93792 15.1306 3.87308 15.0185C3.7765 14.8512 3.83385 14.6376 4.00107 14.541L4.60656 14.1915C4.77277 14.0949 4.98714 14.1521 5.08406 14.3195C5.18065 14.4867 5.1233 14.7004 4.95608 14.797L4.35059 15.1465C4.29565 15.1782 4.23555 15.1933 4.17617 15.1933Z" fill="#F7B97E"/>
<path d="M7.34013 5.29298C7.2193 5.29298 7.10189 5.23034 7.03705 5.11822L6.68754 4.51273C6.59095 4.34547 6.6483 4.13181 6.81553 4.03523C6.98207 3.93865 7.1961 3.99583 7.29303 4.16322L7.64254 4.76871C7.73912 4.93597 7.68178 5.14963 7.51455 5.24621C7.45962 5.27795 7.39956 5.29298 7.34013 5.29298Z" fill="#F7B97E"/>
<path d="M14.68 18.0077C14.5591 18.0077 14.4417 17.9451 14.3769 17.833L14.0274 17.2275C13.9308 17.0602 13.9881 16.8465 14.1554 16.75C14.3219 16.6535 14.5363 16.7107 14.6329 16.8779L14.9824 17.4834C15.079 17.6507 15.0216 17.8644 14.8544 17.9609C14.7994 17.9927 14.7394 18.0077 14.68 18.0077Z" fill="#F7B97E"/>
<path d="M10.8358 11.3475C11.0289 11.3475 11.1854 11.191 11.1854 10.998C11.1854 10.8049 11.0289 10.6484 10.8358 10.6484C10.6428 10.6484 10.4863 10.8049 10.4863 10.998C10.4863 11.191 10.6428 11.3475 10.8358 11.3475Z" fill="#7A4646"/>
<path d="M10.8356 1.56212C5.42443 1.56212 1.07697 6.11655 1.41743 11.6012C1.70913 16.3007 5.53397 20.1256 10.2335 20.4173C15.7181 20.7578 20.2726 16.4103 20.2726 10.9991C20.2726 10.2541 20.1854 9.5297 20.0217 8.83474C19.9829 8.66988 20.0623 8.5019 20.2172 8.43346C20.4105 8.34805 20.6773 8.24402 20.8922 8.16267C21.0866 8.08907 21.3059 8.19949 21.3557 8.40125C21.6326 9.52411 21.7356 10.7154 21.6299 11.9463C21.4062 14.551 20.2433 16.8986 18.4888 18.652C18.4888 18.652 14.9619 21.4157 11.4687 21.4157C4.94621 21.4157 0.410156 16.7271 0.410156 10.1916C0.410156 7.40625 3.18409 3.34816 3.18409 3.34816C5.04237 1.49153 7.56422 0.300058 10.3375 0.175287C13.4409 0.035659 16.2657 1.20796 18.3212 3.17781L18.9202 2.71389C19.1388 2.5446 19.4582 2.68418 19.4824 2.95967L19.7199 5.66482C19.7422 5.91826 19.4949 6.10982 19.2551 6.02483L16.6954 5.11791C16.4348 5.02556 16.3795 4.68142 16.5982 4.51212L17.2071 4.04055C15.5283 2.50215 13.2923 1.56212 10.8356 1.56212Z" fill="#FB5F7A"/>
<path d="M18.485 18.6485C18.2212 18.3848 17.7996 18.3723 17.5193 18.6183C15.5456 20.3511 12.893 21.3292 10.0123 21.102C5.09917 20.7145 1.11879 16.7339 0.731479 11.8208C0.50441 8.94025 1.48256 6.28788 3.21519 4.3143C3.46126 4.03403 3.44709 3.61138 3.18336 3.34766C1.37642 5.15274 0.197009 7.58634 0.0232257 10.2799C-0.397562 16.8019 4.95607 22.1858 11.468 21.8155C14.1882 21.6609 16.6595 20.4788 18.4881 18.6515L18.485 18.6485Z" fill="#F74455"/>
</svg>

After

Width:  |  Height:  |  Size: 6.5 KiB

View File

@ -0,0 +1,23 @@
<svg width="22" height="22" viewBox="0 0 22 22" fill="none" xmlns="http://www.w3.org/2000/svg">
<g clip-path="url(#clip0_8194_10077)">
<path d="M17.6 18.673C17.3521 18.4251 17.3521 18.0231 17.6 17.7752C19.4054 15.9698 20.3996 13.5678 20.3996 11.0115C20.3996 8.45525 19.4054 6.05318 17.6 4.24784C17.3521 3.99994 17.3521 3.59795 17.6 3.35001C17.8479 3.10208 18.2499 3.10208 18.4978 3.35001C20.543 5.39516 21.6694 8.11606 21.6694 11.0115C21.6694 13.9069 20.543 16.6278 18.4978 18.673C18.2503 18.9205 17.8484 18.9213 17.6 18.673Z" fill="#7AFFE4"/>
<path d="M15.6781 16.7502C15.4302 16.5023 15.4302 16.1003 15.6781 15.8524C16.9704 14.5602 17.6821 12.8408 17.6821 11.011C17.6821 9.18128 16.9705 7.46194 15.6781 6.1697C15.4302 5.92176 15.4302 5.51981 15.6781 5.27187C15.926 5.02398 16.328 5.02394 16.576 5.27187C18.1081 6.80393 18.9519 8.84213 18.9519 11.0111C18.9519 13.18 18.1081 15.2182 16.576 16.7503C16.3279 16.9982 15.926 16.9981 15.6781 16.7502Z" fill="#00DDC1"/>
<path d="M13.7543 14.8285C13.5064 14.5805 13.5064 14.1785 13.7543 13.9306C14.5331 13.152 14.9619 12.1158 14.9619 11.0131C14.9619 9.91034 14.5331 8.87416 13.7543 8.09548C13.5064 7.84758 13.5064 7.44559 13.7543 7.19765C14.0022 6.94976 14.4042 6.94972 14.6521 7.19765C15.6707 8.21619 16.2317 9.5712 16.2317 11.0131C16.2317 12.455 15.6707 13.81 14.6521 14.8285C14.4042 15.0764 14.0022 15.0763 13.7543 14.8285Z" fill="#7AFFE4"/>
<path d="M17.6 18.6732C17.8484 18.9216 18.2503 18.9207 18.4978 18.6732C20.543 16.6281 21.6694 13.9072 21.6694 11.0117H20.3996C20.3996 13.568 19.4054 15.97 17.6 17.7754C17.3521 18.0233 17.3521 18.4253 17.6 18.6732Z" fill="#00DDC1"/>
<path d="M15.6781 16.7509C15.926 16.9988 16.3279 16.9989 16.5759 16.7509C18.1081 15.2189 18.9518 13.1807 18.9518 11.0117H17.6821C17.6821 12.8415 16.9704 14.5608 15.6781 15.8531C15.4302 16.101 15.4302 16.503 15.6781 16.7509Z" fill="#00B4BC"/>
<path d="M13.7543 14.8272C14.0022 15.0751 14.4041 15.0751 14.6521 14.8272C15.6707 13.8086 16.2316 12.4536 16.2316 11.0117H14.9619C14.9619 12.1144 14.533 13.1506 13.7543 13.9293C13.5064 14.1772 13.5064 14.5792 13.7543 14.8272Z" fill="#00DDC1"/>
<path d="M3.17152 18.673C1.12633 16.6278 0 13.9069 0 11.0115C0 8.11604 1.12633 5.39515 3.17152 3.35C3.41946 3.10206 3.82141 3.10211 4.06935 3.35C4.31728 3.59793 4.31728 3.99989 4.06935 4.24782C2.26396 6.05316 1.26973 8.45523 1.26973 11.0115C1.26973 13.5677 2.26396 15.9698 4.06935 17.7751C4.31728 18.023 4.31728 18.425 4.06935 18.673C3.82141 18.921 3.41937 18.9209 3.17152 18.673Z" fill="#7AFFE4"/>
<path d="M5.09462 16.7503C3.56253 15.2182 2.71875 13.18 2.71875 11.0111C2.71875 8.84212 3.56253 6.80391 5.09466 5.27186C5.3426 5.02396 5.74459 5.02396 5.99249 5.27186C6.24038 5.51979 6.24038 5.92175 5.99249 6.16968C4.70016 7.46197 3.98848 9.1813 3.98848 11.0111C3.98848 12.8408 4.70016 14.5602 5.99249 15.8524C6.24042 16.1003 6.24042 16.5023 5.99249 16.7502C5.74489 16.9978 5.34294 16.9985 5.09462 16.7503Z" fill="#00DDC1"/>
<path d="M7.01704 14.8285C5.99847 13.81 5.4375 12.455 5.4375 11.0131C5.4375 9.5712 5.99847 8.21619 7.01704 7.19765C7.26498 6.94972 7.66697 6.94976 7.91486 7.19765C8.1628 7.44559 8.16276 7.84758 7.91486 8.09548C7.1361 8.87416 6.70723 9.91034 6.70723 11.0131C6.70723 12.1158 7.1361 13.152 7.91486 13.9306C8.1628 14.1785 8.1628 14.5805 7.91486 14.8285C7.66727 15.0761 7.26531 15.0768 7.01704 14.8285Z" fill="#7AFFE4"/>
<path d="M4.06935 18.6732C4.31728 18.4253 4.31728 18.0233 4.06935 17.7754C2.26396 15.97 1.26973 13.568 1.26973 11.0117H0C0 13.9072 1.12633 16.6281 3.17152 18.6732C3.41937 18.9211 3.82141 18.9212 4.06935 18.6732Z" fill="#00DDC1"/>
<path d="M5.99249 16.7509C6.24038 16.5029 6.24038 16.101 5.99249 15.8531C4.70016 14.5609 3.98848 12.8415 3.98848 11.0117H2.71875C2.71875 13.1807 3.56253 15.2189 5.09466 16.7509C5.34294 16.9992 5.74489 16.9985 5.99249 16.7509Z" fill="#00B4BC"/>
<path d="M7.91486 14.8271C8.1628 14.5792 8.16276 14.1772 7.91486 13.9293C7.1361 13.1506 6.70723 12.1144 6.70723 11.0117H5.4375C5.4375 12.4536 5.99846 13.8086 7.01704 14.8272C7.26527 15.0754 7.66722 15.0747 7.91486 14.8271Z" fill="#00DDC1"/>
<path d="M10.835 13.6857C12.3123 13.6857 13.5099 12.4881 13.5099 11.0108C13.5099 9.53353 12.3123 8.33594 10.835 8.33594C9.35775 8.33594 8.16016 9.53353 8.16016 11.0108C8.16016 12.4881 9.35775 13.6857 10.835 13.6857Z" fill="#00DDC1"/>
<path d="M10.835 13.6866C12.31 13.6866 13.5099 12.4866 13.5099 11.0117H8.16016C8.16016 12.4867 9.36009 13.6866 10.835 13.6866Z" fill="#00B4BC"/>
</g>
<defs>
<clipPath id="clip0_8194_10077">
<rect width="21.67" height="21.67" fill="white" transform="translate(0 0.164062)"/>
</clipPath>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 4.5 KiB

View File

@ -16,12 +16,12 @@ import 'package:syncrow_web/pages/visitor_password/bloc/visitor_password_bloc.da
import 'package:syncrow_web/services/locator.dart';
import 'package:syncrow_web/utils/app_routes.dart';
import 'package:syncrow_web/utils/constants/routes_const.dart';
import 'package:syncrow_web/utils/navigation_service.dart';
import 'package:syncrow_web/utils/theme/theme.dart';
Future<void> main() async {
try {
const environment =
String.fromEnvironment('FLAVOR', defaultValue: 'development');
const environment = String.fromEnvironment('FLAVOR', defaultValue: 'development');
await dotenv.load(fileName: '.env.$environment');
WidgetsFlutterBinding.ensureInitialized();
await Firebase.initializeApp(
@ -33,9 +33,7 @@ Future<void> main() async {
}
class MyApp extends StatelessWidget {
MyApp({
super.key,
});
MyApp({super.key});
final GoRouter _router = GoRouter(
initialLocation: RoutesConst.auth,
@ -56,11 +54,10 @@ class MyApp extends StatelessWidget {
Widget build(BuildContext context) {
return MultiBlocProvider(
providers: [
BlocProvider<CreateRoutineBloc>(
BlocProvider<CreateRoutineBloc>(
create: (context) => CreateRoutineBloc(),
),
BlocProvider(
create: (context) => HomeBloc()..add(const FetchUserInfo())),
BlocProvider(create: (context) => HomeBloc()..add(const FetchUserInfo())),
BlocProvider<VisitorPasswordBloc>(
create: (context) => VisitorPasswordBloc(),
),
@ -81,6 +78,8 @@ class MyApp extends StatelessWidget {
PointerDeviceKind.unknown,
},
),
key: NavigationService.navigatorKey,
// scaffoldMessengerKey: NavigationService.snackbarKey,
theme: myTheme,
routerConfig: _router,
));

87
lib/main_staging.dart Normal file
View File

@ -0,0 +1,87 @@
import 'package:firebase_core/firebase_core.dart';
import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_dotenv/flutter_dotenv.dart';
import 'package:go_router/go_router.dart';
import 'package:syncrow_web/firebase_options_prod.dart';
import 'package:syncrow_web/pages/auth/bloc/auth_bloc.dart';
import 'package:syncrow_web/pages/home/bloc/home_bloc.dart';
import 'package:syncrow_web/pages/home/bloc/home_event.dart';
import 'package:syncrow_web/pages/routines/bloc/create_routine_bloc/create_routine_bloc.dart';
import 'package:syncrow_web/pages/routines/bloc/routine_bloc/routine_bloc.dart';
import 'package:syncrow_web/pages/space_tree/bloc/space_tree_bloc.dart';
import 'package:syncrow_web/pages/space_tree/bloc/space_tree_event.dart';
import 'package:syncrow_web/pages/visitor_password/bloc/visitor_password_bloc.dart';
import 'package:syncrow_web/services/locator.dart';
import 'package:syncrow_web/utils/app_routes.dart';
import 'package:syncrow_web/utils/constants/routes_const.dart';
import 'package:syncrow_web/utils/navigation_service.dart';
import 'package:syncrow_web/utils/theme/theme.dart';
Future<void> main() async {
try {
const environment = String.fromEnvironment('FLAVOR', defaultValue: 'staging');
await dotenv.load(fileName: '.env.$environment');
WidgetsFlutterBinding.ensureInitialized();
await Firebase.initializeApp(
options: DefaultFirebaseOptionsStaging.currentPlatform,
);
initialSetup();
} catch (_) {}
runApp(MyApp());
}
class MyApp extends StatelessWidget {
MyApp({super.key});
final GoRouter _router = GoRouter(
initialLocation: RoutesConst.auth,
routes: AppRoutes.getRoutes(),
redirect: (context, state) async {
String checkToken = await AuthBloc.getTokenAndValidate();
final loggedIn = checkToken == 'Success';
final goingToLogin = state.uri.toString() == RoutesConst.auth;
if (!loggedIn && !goingToLogin) return RoutesConst.auth;
if (loggedIn && goingToLogin) return RoutesConst.home;
return null;
},
);
@override
Widget build(BuildContext context) {
return MultiBlocProvider(
providers: [
BlocProvider<CreateRoutineBloc>(
create: (context) => CreateRoutineBloc(),
),
BlocProvider(create: (context) => HomeBloc()..add(const FetchUserInfo())),
BlocProvider<VisitorPasswordBloc>(
create: (context) => VisitorPasswordBloc(),
),
BlocProvider<RoutineBloc>(
create: (context) => RoutineBloc(),
),
BlocProvider<SpaceTreeBloc>(
create: (context) => SpaceTreeBloc()..add(InitialEvent()),
),
],
child: MaterialApp.router(
debugShowCheckedModeBanner: false,
scrollBehavior: const MaterialScrollBehavior().copyWith(
dragDevices: {
PointerDeviceKind.mouse,
PointerDeviceKind.touch,
PointerDeviceKind.stylus,
PointerDeviceKind.unknown,
},
),
key: NavigationService.navigatorKey,
// scaffoldMessengerKey: NavigationService.snackbarKey,
theme: myTheme,
routerConfig: _router,
));
}
}

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,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,17 @@
import 'package:bloc/bloc.dart';
import 'package:equatable/equatable.dart';
part 'analytics_date_picker_event.dart';
class AnalyticsDatePickerBloc extends Bloc<AnalyticsDatePickerEvent, DateTime> {
AnalyticsDatePickerBloc() : super(DateTime.now()) {
on<UpdateAnalyticsDatePickerEvent>(_onUpdateAnalyticsDatePickerEvent);
}
void _onUpdateAnalyticsDatePickerEvent(
UpdateAnalyticsDatePickerEvent event,
Emitter<DateTime> emit,
) {
emit(event.date);
}
}

View File

@ -0,0 +1,17 @@
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.date);
final DateTime date;
@override
List<Object?> get props => [date];
}

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,82 @@
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/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/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/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/fake_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(
FakeTotalEnergyConsumptionService(),
),
),
BlocProvider(
create: (context) => EnergyConsumptionByPhasesBloc(
FakeEnergyConsumptionByPhasesService(),
),
),
BlocProvider(
create: (context) => EnergyConsumptionPerDeviceBloc(
FakeEnergyConsumptionPerDeviceService(),
),
),
BlocProvider(
create: (context) => PowerClampInfoBloc(
RemotePowerClampInfoService(HTTPService()),
),
),
BlocProvider<RealtimeDeviceChangesBloc>(
create: (context) => RealtimeDeviceChangesBloc(
FirebaseRealtimeDeviceService(),
),
),
],
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,
),
enableMenuSidebar: false,
scaffoldBody: const Row(
children: [
AnalyticsCommunitiesSidebar(),
Expanded(flex: 5, child: AnalyticsPageTabsAndChildren()),
],
),
);
}
}

View File

@ -0,0 +1,55 @@
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_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/helpers/fetch_energy_management_data_helper.dart';
import 'package:syncrow_web/pages/space_tree/view/space_tree_view.dart';
class AnalyticsCommunitiesSidebar extends StatelessWidget {
const AnalyticsCommunitiesSidebar({super.key});
@override
Widget build(BuildContext context) {
return Builder(
builder: (context) {
return Expanded(
child: SpaceTreeView(
title: const Text('Communities'),
shouldDisableDeselectingChildrenOfSelectedParent: true,
onSelect: () {
/// Necessary to wait for the state to update before fethcing the data.
Future.delayed(
const Duration(milliseconds: 100),
() {
if (context.mounted) {
FetchEnergyManagementDataHelper.fetchEnergyManagementData(
context,
);
FetchEnergyManagementDataHelper.loadRealtimeDeviceChanges(
context,
);
context.read<PowerClampInfoBloc>().add(
const ClearPowerClampInfoEvent(),
);
final (selectedCommunities, selectedSpaces) =
FetchEnergyManagementDataHelper
.getSelectedCommunitiesAndSpaces(context);
if (selectedCommunities.isEmpty && selectedSpaces.isEmpty) {
context.read<PowerClampInfoBloc>().add(
const ClearPowerClampInfoEvent(),
);
} else {
FetchEnergyManagementDataHelper.loadPowerClampInfo(
context,
);
}
}
},
);
},
isSide: false,
),
);
},
);
}
}

View File

@ -0,0 +1,97 @@
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_svg/svg.dart';
import 'package:intl/intl.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/month_picker_widget.dart';
import 'package:syncrow_web/pages/analytics/modules/energy_management/helpers/fetch_energy_management_data_helper.dart';
import 'package:syncrow_web/utils/color_manager.dart';
import 'package:syncrow_web/utils/constants/assets.dart';
class AnalyticsDateFilterButton extends StatefulWidget {
const AnalyticsDateFilterButton({super.key});
static final _color = ColorsManager.blackColor.withValues(alpha: 0.8);
@override
State<AnalyticsDateFilterButton> createState() =>
_AnalyticsDateFilterButtonState();
}
class _AnalyticsDateFilterButtonState extends State<AnalyticsDateFilterButton> {
late final AnalyticsDatePickerBloc _analyticsDatePickerBloc;
@override
void initState() {
_analyticsDatePickerBloc = AnalyticsDatePickerBloc();
super.initState();
}
@override
void dispose() {
_analyticsDatePickerBloc.close();
super.dispose();
}
@override
Widget build(BuildContext context) {
return BlocProvider.value(
value: _analyticsDatePickerBloc,
child: Builder(builder: (context) {
final selectedDate = context.watch<AnalyticsDatePickerBloc>().state;
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(selectedDate),
style: const TextStyle(
fontWeight: FontWeight.w700,
),
),
onPressed: () {
showDialog(
context: context,
builder: (_) => MonthPickerWidget(
selectedDate: selectedDate,
onDateSelected: (value) {
_analyticsDatePickerBloc.add(
UpdateAnalyticsDatePickerEvent(value),
);
FetchEnergyManagementDataHelper.fetchEnergyManagementData(
context,
selectedDate: value,
);
},
),
);
},
);
}),
);
}
String _formatDate(DateTime? date) {
final formatter = DateFormat('MMMM yyyy');
final formattedDate = formatter.format(date ?? DateTime.now());
return formattedDate;
}
}

View File

@ -0,0 +1,38 @@
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/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: () => 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,80 @@
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/widgets/analytics_date_filter_button.dart';
import 'package:syncrow_web/pages/analytics/modules/analytics/widgets/analytics_page_tab_button.dart';
import 'package:syncrow_web/utils/style.dart';
class AnalyticsPageTabsAndChildren extends StatelessWidget {
const AnalyticsPageTabsAndChildren({super.key});
@override
Widget build(BuildContext context) {
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) => AnimatedSwitcher(
switchInCurve: Curves.easeIn,
duration: const Duration(milliseconds: 200),
child: AnalyticsPageTabButton(
key: ValueKey(selectedTab),
tab: tab,
isSelected: tab == selectedTab,
),
),
),
],
),
),
),
const Spacer(),
const Expanded(
flex: 2,
child: FittedBox(
fit: BoxFit.scaleDown,
alignment: AlignmentDirectional.centerEnd,
child: AnalyticsDateFilterButton(),
),
),
],
),
),
),
Expanded(
flex: 8,
child: AnimatedSwitcher(
switchInCurve: Curves.easeIn,
duration: const Duration(milliseconds: 200),
child: selectedTab.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,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.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, 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,111 @@
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/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';
import 'package:syncrow_web/pages/space_tree/bloc/space_tree_bloc.dart';
abstract final class FetchEnergyManagementDataHelper {
const FetchEnergyManagementDataHelper._();
static void fetchEnergyManagementData(
BuildContext context, {
DateTime? selectedDate,
}) {
final (selectedCommunities, selectedSpaces) =
getSelectedCommunitiesAndSpaces(context);
if (selectedCommunities.isEmpty && selectedSpaces.isEmpty) {
clearAllData(context);
return;
}
loadTotalEnergyConsumption(context);
loadEnergyConsumptionByPhases(context);
loadEnergyConsumptionPerDevice(context);
return;
}
static (List<String> selectedCommunities, List<String> selectedSpaces)
getSelectedCommunitiesAndSpaces(BuildContext context) {
final spaceTreeState = context.read<SpaceTreeBloc>().state;
final selectedCommunities = spaceTreeState.selectedCommunities;
final selectedSpaces = spaceTreeState.selectedSpaces;
return (selectedCommunities, selectedSpaces);
}
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,
}) {
final (selectedCommunities, selectedSpaces) =
getSelectedCommunitiesAndSpaces(context);
final param = GetTotalEnergyConsumptionParam(
spaceId: selectedCommunities.firstOrNull,
startDate: 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('cb71d6ad-6e29-4eaa-ae3e-1a0d1c5f60fa'),
);
}
static void loadRealtimeDeviceChanges(BuildContext context) {
context.read<RealtimeDeviceChangesBloc>().add(
const RealtimeDeviceChangesStarted('cb71d6ad-6e29-4eaa-ae3e-1a0d1c5f60fa'),
);
}
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,70 @@
import 'package:flutter/material.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';
class AnalyticsEnergyManagementView extends StatelessWidget {
const AnalyticsEnergyManagementView({super.key});
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,36 @@
import 'package:flutter/material.dart';
import 'package:syncrow_web/utils/color_manager.dart';
class PowerClampEnergyDataDeviceDropdown extends StatelessWidget {
const PowerClampEnergyDataDeviceDropdown({super.key});
static final _color = ColorsManager.blackColor.withValues(alpha: 0.8);
@override
Widget build(BuildContext context) {
return TextButton(
style: TextButton.styleFrom(
foregroundColor: _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,
),
),
child: const Text(
'Device 1',
style: TextStyle(
fontWeight: FontWeight.w700,
),
),
onPressed: () {},
);
}
}

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,80 @@
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,
leftTitlesInterval: 5000,
),
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,12 @@
import 'package:flutter/material.dart';
class AnalyticsOccupancyView extends StatelessWidget {
const AnalyticsOccupancyView({super.key});
@override
Widget build(BuildContext context) {
return const Center(
child: Text('AnalyticsOccupancyView is Working!'),
);
}
}

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 GetTotalEnergyConsumptionParam {
final DateTime? startDate;
final DateTime? endDate;
final String? spaceId;
const GetTotalEnergyConsumptionParam({
this.startDate,
this.endDate,
this.spaceId,
});
Map<String, dynamic> toJson() {
return {
'startDate': startDate?.toIso8601String(),
'endDate': endDate?.toIso8601String(),
'spaceId': spaceId,
};
}
}

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,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: num.tryParse(status['value']?.toString() ?? '0'),
);
}).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,19 @@
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';
class FakeTotalEnergyConsumptionService implements TotalEnergyConsumptionService {
@override
Future<List<EnergyDataModel>> load(
GetTotalEnergyConsumptionParam param,
) {
return Future.value(
List.generate(30, (index) {
return EnergyDataModel(
date: DateTime(2025, 1, index + 1),
value: 20000 + (index * 1000) % 5000,
);
}),
);
}
}

View File

@ -0,0 +1,33 @@
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: '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 EnergyDataModel.fromJson(jsonData);
}).toList();
},
);
return response;
} catch (e) {
throw Exception('Failed to load total energy consumption: $e');
}
}
}

View File

@ -0,0 +1,8 @@
import 'package:syncrow_web/pages/analytics/models/energy_data_model.dart';
import 'package:syncrow_web/pages/analytics/params/get_total_energy_consumption_param.dart';
abstract interface class TotalEnergyConsumptionService {
Future<List<EnergyDataModel>> load(
GetTotalEnergyConsumptionParam param,
);
}

View File

@ -0,0 +1,26 @@
import 'package:flutter/material.dart';
import 'package:syncrow_web/utils/color_manager.dart';
import 'package:syncrow_web/utils/extension/build_context_x.dart';
class AnalyticsErrorWidget extends StatelessWidget {
const AnalyticsErrorWidget(this.errorMessage, {super.key});
final String? errorMessage;
@override
Widget build(BuildContext context) {
return Visibility(
visible: errorMessage != null || (errorMessage?.isNotEmpty ?? false),
child: Text(
'$errorMessage ?? "Something went wrong"',
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: context.textTheme.bodySmall?.copyWith(
color: ColorsManager.red,
fontWeight: FontWeight.w400,
fontSize: 8,
),
),
);
}
}

View File

@ -0,0 +1,25 @@
import 'package:flutter/material.dart';
class ChartsLoadingWidget extends StatelessWidget {
const ChartsLoadingWidget({
required this.isLoading,
super.key,
});
final bool isLoading;
@override
Widget build(BuildContext context) {
return Visibility(
visible: isLoading,
child: const SizedBox.square(
dimension: 16,
child: FittedBox(
child: Padding(
padding: EdgeInsetsDirectional.only(end: 8),
child: CircularProgressIndicator(),
),
),
),
);
}
}

View File

@ -10,6 +10,7 @@ import 'package:syncrow_web/pages/auth/model/region_model.dart';
import 'package:syncrow_web/pages/auth/model/token.dart';
import 'package:syncrow_web/pages/auth/model/user_model.dart';
import 'package:syncrow_web/pages/common/bloc/project_manager.dart';
import 'package:syncrow_web/pages/home/bloc/home_bloc.dart';
import 'package:syncrow_web/pages/space_tree/bloc/space_tree_bloc.dart';
import 'package:syncrow_web/pages/space_tree/bloc/space_tree_event.dart';
import 'package:syncrow_web/services/auth_api.dart';
@ -432,9 +433,13 @@ class AuthBloc extends Bloc<AuthEvent, AuthState> {
}
static Future<void> logout(BuildContext context) async {
final storage = FlutterSecureStorage();
ProjectManager.clearProjectUUID();
const storage = FlutterSecureStorage();
context.read<SpaceTreeBloc>().add(ClearAllData());
storage.deleteAll();
user = null;
context.read<HomeBloc>().user = null;
await Future.wait<void>([
ProjectManager.clearProjectUUID(),
storage.deleteAll(),
]);
}
}

View File

@ -55,12 +55,12 @@ class _LoginWebPageState extends State<LoginWebPage> with HelperResponsiveLayout
final isSmallScreen = isSmallScreenSize(context);
final isMediumScreen = isMediumScreenSize(context);
Size size = MediaQuery.of(context).size;
late ScrollController _scrollController;
_scrollController = ScrollController();
late ScrollController scrollController;
scrollController = ScrollController();
void _scrollToCenter() {
final double middlePosition = _scrollController.position.maxScrollExtent / 2;
_scrollController.animateTo(
void scrollToCenter() {
final double middlePosition = scrollController.position.maxScrollExtent / 2;
scrollController.animateTo(
middlePosition,
duration: const Duration(seconds: 1),
curve: Curves.easeInOut,
@ -68,7 +68,7 @@ class _LoginWebPageState extends State<LoginWebPage> with HelperResponsiveLayout
}
WidgetsBinding.instance.addPostFrameCallback((_) {
_scrollToCenter();
scrollToCenter();
});
return Stack(
@ -76,7 +76,7 @@ class _LoginWebPageState extends State<LoginWebPage> with HelperResponsiveLayout
FirstLayer(
second: Center(
child: ListView(
controller: _scrollController,
controller: scrollController,
shrinkWrap: true,
children: [
Container(
@ -199,7 +199,7 @@ class _LoginWebPageState extends State<LoginWebPage> with HelperResponsiveLayout
width: size.width * 0.9,
child: DropdownButtonHideUnderline(
child: DropdownButton2<String>(
style: TextStyle(color: Colors.black),
style: const TextStyle(color: Colors.black),
isExpanded: true,
hint: Text(
'Select your region/country',
@ -336,6 +336,16 @@ class _LoginWebPageState extends State<LoginWebPage> with HelperResponsiveLayout
obscureText: loginBloc.obscureText,
keyboardType: TextInputType.visiblePassword,
controller: loginBloc.loginPasswordController,
onFieldSubmitted: (value) {
if (loginBloc.loginFormKey.currentState!.validate()) {
loginBloc.add(LoginButtonPressed(
username: loginBloc.loginEmailController.text,
password: value,
));
} else {
loginBloc.add(ChangeValidateEvent());
}
},
decoration: textBoxDecoration()!.copyWith(
hintText: 'At least 8 characters',
hintStyle: Theme.of(context)
@ -393,7 +403,7 @@ class _LoginWebPageState extends State<LoginWebPage> with HelperResponsiveLayout
Transform.scale(
scale: 1.2,
child: Checkbox(
fillColor: MaterialStateProperty.all<Color>(Colors.white),
fillColor: WidgetStateProperty.all<Color>(Colors.white),
activeColor: Colors.white,
value: loginBloc.isChecked,
checkColor: Colors.black,

View File

@ -1,8 +1,8 @@
import 'package:flutter/material.dart';
import 'package:syncrow_web/pages/common/buttons/default_button.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';
import 'package:syncrow_web/utils/color_manager.dart';
class SearchResetButtons extends StatelessWidget {
const SearchResetButtons({
@ -17,8 +17,10 @@ class SearchResetButtons extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Row(
mainAxisSize: MainAxisSize.min,
children: [
Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
const SizedBox(height: 25),

View File

@ -60,7 +60,15 @@ class _CurrentTempState extends State<CurrentTemp> {
);
});
}
@override
void didUpdateWidget(CurrentTemp oldWidget) {
super.didUpdateWidget(oldWidget);
if (oldWidget.tempSet != widget.tempSet) {
setState(() {
_adjustedValue = _initialAdjustedValue(widget.tempSet);
});
}
}
@override
void dispose() {
_debounce?.cancel();

View File

@ -9,6 +9,8 @@ import 'package:syncrow_web/pages/device_managment/curtain/view/curtain_batch_st
import 'package:syncrow_web/pages/device_managment/curtain/view/curtain_status_view.dart';
import 'package:syncrow_web/pages/device_managment/door_lock/view/door_lock_batch_control_view.dart';
import 'package:syncrow_web/pages/device_managment/door_lock/view/door_lock_control_view.dart';
import 'package:syncrow_web/pages/device_managment/flush_mounted_presence_sensor/views/flush_mounted_presence_sensor_batch_control_view.dart';
import 'package:syncrow_web/pages/device_managment/flush_mounted_presence_sensor/views/flush_mounted_presence_sensor_control_view.dart';
import 'package:syncrow_web/pages/device_managment/garage_door/view/garage_door_batch_control_view.dart';
import 'package:syncrow_web/pages/device_managment/garage_door/view/garage_door_control_view.dart';
import 'package:syncrow_web/pages/device_managment/gateway/view/gateway_batch_control.dart';
@ -104,6 +106,9 @@ mixin RouteControlsBasedCode {
);
case 'SOS':
return SosDeviceControlsView(device: device);
case 'NCPS':
return FlushMountedPresenceSensorControlView(device: device);
default:
return const SizedBox();
}
@ -194,6 +199,10 @@ mixin RouteControlsBasedCode {
return SOSBatchControlView(
deviceIds: devices.where((e) => (e.productType == 'SOS')).map((e) => e.uuid!).toList(),
);
case 'NCPS':
return FlushMountedPresenceSensorBatchControlView(
devicesIds: devices.where((e) => (e.productType == 'NCPS')).map((e) => e.uuid!).toList(),
);
default:
return const SizedBox();
}

View File

@ -7,6 +7,7 @@ import 'package:syncrow_web/pages/device_managment/all_devices/models/room.dart'
import 'package:syncrow_web/pages/device_managment/all_devices/models/unit.dart';
import 'package:syncrow_web/pages/routines/models/ac/ac_function.dart';
import 'package:syncrow_web/pages/routines/models/device_functions.dart';
import 'package:syncrow_web/pages/routines/models/flush/flush_functions.dart';
import 'package:syncrow_web/pages/routines/models/gang_switches/one_gang_switch/one_gang_switch.dart';
import 'package:syncrow_web/pages/routines/models/gang_switches/three_gang_switch/three_gang_switch.dart';
import 'package:syncrow_web/pages/routines/models/gang_switches/two_gang_switch/two_gang_switch.dart';
@ -155,13 +156,17 @@ class AllDevicesModel {
batteryLevel = int.tryParse(json['battery']?.toString() ?? '');
productName = json['productName']?.toString();
deviceTags = json['deviceTag'] != null && json['deviceTag'] is List
? (json['deviceTag'] as List).map((tag) => DeviceTagModel.fromJson(tag)).toList()
? (json['deviceTag'] as List)
.map((tag) => DeviceTagModel.fromJson(tag))
.toList()
: [];
deviceSubSpace = json['subspace'] != null
? DeviceSubSpace.fromJson(json['subspace'])
: DeviceSubSpace(subspaceName: '');
if (json['spaces'] != null && json['spaces'] is List) {
spaces = (json['spaces'] as List).map((space) => DeviceSpaceModel.fromJson(space)).toList();
spaces = (json['spaces'] as List)
.map((space) => DeviceSpaceModel.fromJson(space))
.toList();
}
}
@ -209,7 +214,8 @@ SOS
String tempIcon = '';
if (type == DeviceType.LightBulb) {
tempIcon = Assets.lightBulb;
} else if (type == DeviceType.CeilingSensor || type == DeviceType.WallSensor) {
} else if (type == DeviceType.CeilingSensor ||
type == DeviceType.WallSensor) {
tempIcon = Assets.sensors;
} else if (type == DeviceType.AC) {
tempIcon = Assets.ac;
@ -239,6 +245,8 @@ SOS
// tempIcon = Assets.gang3touch;
} else if (type == DeviceType.WaterLeak) {
tempIcon = Assets.waterLeakNormal;
} else if (type == DeviceType.NCPS) {
tempIcon = Assets.sensors;
} else {
tempIcon = Assets.logoHorizontal;
}
@ -254,51 +262,75 @@ SOS
switch (productType) {
case 'AC':
return [
SwitchFunction(deviceId: uuid ?? '', deviceName: name ?? '', type: 'BOTH'),
ModeFunction(deviceId: uuid ?? '', deviceName: name ?? '', type: 'BOTH'),
TempSetFunction(deviceId: uuid ?? '', deviceName: name ?? '', type: 'BOTH'),
CurrentTempFunction(deviceId: uuid ?? '', deviceName: name ?? '', type: 'IF'),
LevelFunction(deviceId: uuid ?? '', deviceName: name ?? '', type: 'BOTH'),
ChildLockFunction(deviceId: uuid ?? '', deviceName: name ?? '', type: 'BOTH'),
SwitchFunction(
deviceId: uuid ?? '', deviceName: name ?? '', type: 'BOTH'),
ModeFunction(
deviceId: uuid ?? '', deviceName: name ?? '', type: 'BOTH'),
TempSetFunction(
deviceId: uuid ?? '', deviceName: name ?? '', type: 'BOTH'),
CurrentTempFunction(
deviceId: uuid ?? '', deviceName: name ?? '', type: 'IF'),
LevelFunction(
deviceId: uuid ?? '', deviceName: name ?? '', type: 'BOTH'),
ChildLockFunction(
deviceId: uuid ?? '', deviceName: name ?? '', type: 'BOTH'),
];
case '1G':
return [
OneGangSwitchFunction(deviceId: uuid ?? '', deviceName: name ?? ''),
OneGangCountdownFunction(deviceId: uuid ?? '', deviceName: name ?? ''),
OneGangCountdownFunction(
deviceId: uuid ?? '', deviceName: name ?? ''),
];
case '2G':
return [
TwoGangSwitch1Function(deviceId: uuid ?? '', deviceName: name ?? ''),
TwoGangSwitch2Function(deviceId: uuid ?? '', deviceName: name ?? ''),
TwoGangCountdown1Function(deviceId: uuid ?? '', deviceName: name ?? ''),
TwoGangCountdown2Function(deviceId: uuid ?? '', deviceName: name ?? ''),
TwoGangCountdown1Function(
deviceId: uuid ?? '', deviceName: name ?? ''),
TwoGangCountdown2Function(
deviceId: uuid ?? '', deviceName: name ?? ''),
];
case '3G':
return [
ThreeGangSwitch1Function(deviceId: uuid ?? '', deviceName: name ?? '', type: 'BOTH'),
ThreeGangSwitch2Function(deviceId: uuid ?? '', deviceName: name ?? '', type: 'BOTH'),
ThreeGangSwitch3Function(deviceId: uuid ?? '', deviceName: name ?? '', type: 'BOTH'),
ThreeGangCountdown1Function(deviceId: uuid ?? '', deviceName: name ?? '', type: 'BOTH'),
ThreeGangCountdown2Function(deviceId: uuid ?? '', deviceName: name ?? '', type: 'BOTH'),
ThreeGangCountdown3Function(deviceId: uuid ?? '', deviceName: name ?? '', type: 'BOTH'),
ThreeGangSwitch1Function(
deviceId: uuid ?? '', deviceName: name ?? '', type: 'BOTH'),
ThreeGangSwitch2Function(
deviceId: uuid ?? '', deviceName: name ?? '', type: 'BOTH'),
ThreeGangSwitch3Function(
deviceId: uuid ?? '', deviceName: name ?? '', type: 'BOTH'),
ThreeGangCountdown1Function(
deviceId: uuid ?? '', deviceName: name ?? '', type: 'BOTH'),
ThreeGangCountdown2Function(
deviceId: uuid ?? '', deviceName: name ?? '', type: 'BOTH'),
ThreeGangCountdown3Function(
deviceId: uuid ?? '', deviceName: name ?? '', type: 'BOTH'),
];
case 'WPS':
return [
//IF Functions
PresenceStateFunction(deviceId: uuid ?? '', deviceName: name ?? '', type: 'IF'),
CurrentDistanceFunction(deviceId: uuid ?? '', deviceName: name ?? '', type: 'IF'),
IlluminanceValueFunction(deviceId: uuid ?? '', deviceName: name ?? '', type: 'IF'),
PresenceTimeFunction(deviceId: uuid ?? '', deviceName: name ?? '', type: 'IF'),
PresenceStateFunction(
deviceId: uuid ?? '', deviceName: name ?? '', type: 'IF'),
CurrentDistanceFunction(
deviceId: uuid ?? '', deviceName: name ?? '', type: 'IF'),
IlluminanceValueFunction(
deviceId: uuid ?? '', deviceName: name ?? '', type: 'IF'),
PresenceTimeFunction(
deviceId: uuid ?? '', deviceName: name ?? '', type: 'IF'),
//THEN Functions
FarDetectionFunction(deviceId: uuid ?? '', deviceName: name ?? '', type: 'THEN'),
MotionSensitivityFunction(deviceId: uuid ?? '', deviceName: name ?? '', type: 'THEN'),
MotionLessSensitivityFunction(deviceId: uuid ?? '', deviceName: name ?? '', type: 'THEN'),
IndicatorFunction(deviceId: uuid ?? '', deviceName: name ?? '', type: 'BOTH'),
NoOneTimeFunction(deviceId: uuid ?? '', deviceName: name ?? '', type: 'THEN'),
FarDetectionFunction(
deviceId: uuid ?? '', deviceName: name ?? '', type: 'THEN'),
MotionSensitivityFunction(
deviceId: uuid ?? '', deviceName: name ?? '', type: 'THEN'),
MotionLessSensitivityFunction(
deviceId: uuid ?? '', deviceName: name ?? '', type: 'THEN'),
IndicatorFunction(
deviceId: uuid ?? '', deviceName: name ?? '', type: 'BOTH'),
NoOneTimeFunction(
deviceId: uuid ?? '', deviceName: name ?? '', type: 'THEN'),
];
case 'GW':
return [
@ -323,6 +355,30 @@ SOS
uuid: uuid ?? '',
name: name ?? '',
);
case 'NCPS':
return [
FlushPresenceDelayFunction(
deviceId: uuid ?? '', deviceName: name ?? '', type: 'IF',),
FlushIlluminanceFunction(
deviceId: uuid ?? '', deviceName: name ?? '', type: 'IF'),
//THEN Functions
FlushSensitivityFunction(
deviceId: uuid ?? '', deviceName: name ?? '', type: 'THEN'),
FlushNearDetectionFunction(
deviceId: uuid ?? '', deviceName: name ?? '', type: 'THEN'),
FlushMaxDetectDistFunction(
deviceId: uuid ?? '', deviceName: name ?? '', type: 'THEN'),
FlushTargetConfirmTimeFunction(
deviceId: uuid ?? '', deviceName: name ?? '', type: 'THEN'),
FlushDisappeDelayFunction(
deviceId: uuid ?? '', deviceName: name ?? '', type: 'THEN'),
FlushIndentLevelFunction(
deviceId: uuid ?? '', deviceName: name ?? '', type: 'THEN'),
FlushTriggerLevelFunction(
deviceId: uuid ?? '', deviceName: name ?? '', type: 'THEN'),
];
default:
return [];
}
@ -454,5 +510,6 @@ SOS
"3GT": DeviceType.ThreeTouch,
"GD": DeviceType.GarageDoor,
"WL": DeviceType.WaterLeak,
"NCPS": DeviceType.NCPS,
};
}

View File

@ -19,6 +19,7 @@ class FactoryResetModel {
Map<String, dynamic> toJson() {
return {
'devicesUuid': devicesUuid,
'operationType': operationType,
};
}
@ -33,6 +34,7 @@ class FactoryResetModel {
Map<String, dynamic> toMap() {
return {
'devicesUuid': devicesUuid,
'operationType': operationType,
};
}
@ -56,3 +58,4 @@ class FactoryResetModel {
@override
int get hashCode => devicesUuid.hashCode;
}

View File

@ -108,7 +108,7 @@ class DeviceManagementPage extends StatelessWidget with HelperResponsiveLayout {
return DeviceManagementBody(
devices: deviceState.filteredDevices);
} else {
return const Center(child: Text('Error fetching Devices'));
return const DeviceManagementBody(devices: []);
}
},
);

View File

@ -72,6 +72,7 @@ class DeviceManagementBody extends StatelessWidget with HelperResponsiveLayout {
child: state is DeviceManagementLoading
? const Center(child: CircularProgressIndicator())
: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Container(
padding: isLargeScreenSize(context)

View File

@ -14,29 +14,29 @@ class DeviceSearchFilters extends StatefulWidget {
class _DeviceSearchFiltersState extends State<DeviceSearchFilters>
with HelperResponsiveLayout {
final _unitNameController = TextEditingController();
final _productNameController = TextEditingController();
late final TextEditingController _unitNameController;
late final TextEditingController _productNameController;
List<Widget> get _widgets => [
_buildSearchField("Space Name", _unitNameController, 200),
_buildSearchField("Device Name / Product Name", _productNameController, 300),
_buildSearchResetButtons(),
];
@override
void initState() {
_unitNameController = TextEditingController();
_productNameController = TextEditingController();
super.initState();
}
@override
Widget build(BuildContext context) {
if (isExtraLargeScreenSize(context)) {
return Row(
children: _widgets
.map((e) => Padding(padding: const EdgeInsets.all(10), child: e))
.toList(),
);
}
return Wrap(
alignment: WrapAlignment.start,
runAlignment: WrapAlignment.start,
crossAxisAlignment: WrapCrossAlignment.center,
spacing: 20,
runSpacing: 10,
children: _widgets,
children: [
_buildSearchField("Space Name", _unitNameController, 200),
_buildSearchField("Device Name / Product Name", _productNameController, 300),
_buildSearchResetButtons(),
],
);
}

View File

@ -16,7 +16,8 @@ import 'package:syncrow_web/pages/device_managment/shared/table/report_table.dar
import 'package:syncrow_web/utils/constants/assets.dart';
import 'package:syncrow_web/utils/helpers/responsice_layout_helper/responsive_layout_helper.dart';
class CeilingSensorControlsView extends StatelessWidget with HelperResponsiveLayout {
class CeilingSensorControlsView extends StatelessWidget
with HelperResponsiveLayout {
const CeilingSensorControlsView({super.key, required this.device});
final AllDevicesModel device;
@ -31,29 +32,35 @@ class CeilingSensorControlsView extends StatelessWidget with HelperResponsiveLay
..add(CeilingInitialEvent(device.uuid ?? '')),
child: BlocBuilder<CeilingSensorBloc, CeilingSensorState>(
builder: (context, state) {
if (state is CeilingLoadingInitialState || state is CeilingReportsLoadingState) {
if (state is CeilingLoadingInitialState ||
state is CeilingReportsLoadingState) {
return const Center(child: CircularProgressIndicator());
} else if (state is CeilingUpdateState) {
return _buildGridView(
context, state.ceilingSensorModel, isExtraLarge, isLarge, isMedium);
return _buildGridView(context, state.ceilingSensorModel,
isExtraLarge, isLarge, isMedium);
} else if (state is CeilingReportsState) {
return ReportsTable(
report: state.deviceReport,
onRowTap: (index) {},
onClose: () {
context.read<CeilingSensorBloc>().add(BackToCeilingGridViewEvent());
context
.read<CeilingSensorBloc>()
.add(BackToCeilingGridViewEvent());
},
);
} else if (state is ShowCeilingDescriptionState) {
return DescriptionView(
description: state.description,
onClose: () {
context.read<CeilingSensorBloc>().add(BackToCeilingGridViewEvent());
context
.read<CeilingSensorBloc>()
.add(BackToCeilingGridViewEvent());
},
);
} else if (state is CeilingReportsFailedState) {
final model = context.read<CeilingSensorBloc>().deviceStatus;
return _buildGridView(context, model, isExtraLarge, isLarge, isMedium);
return _buildGridView(
context, model, isExtraLarge, isLarge, isMedium);
}
return const Center(child: Text('Error fetching status'));
},
@ -61,8 +68,8 @@ class CeilingSensorControlsView extends StatelessWidget with HelperResponsiveLay
);
}
Widget _buildGridView(BuildContext context, CeilingSensorModel model, bool isExtraLarge,
bool isLarge, bool isMedium) {
Widget _buildGridView(BuildContext context, CeilingSensorModel model,
bool isExtraLarge, bool isLarge, bool isMedium) {
return GridView(
padding: const EdgeInsets.symmetric(horizontal: 50),
shrinkWrap: true,
@ -143,8 +150,8 @@ class CeilingSensorControlsView extends StatelessWidget with HelperResponsiveLay
),
GestureDetector(
onTap: () {
context.read<CeilingSensorBloc>().add(
GetCeilingDeviceReportsEvent(code: 'presence_state', deviceUuid: device.uuid!));
context.read<CeilingSensorBloc>().add(GetCeilingDeviceReportsEvent(
code: 'presence_state', deviceUuid: device.uuid!));
},
child: const PresenceStaticWidget(
icon: Assets.illuminanceRecordIcon,
@ -153,9 +160,8 @@ class CeilingSensorControlsView extends StatelessWidget with HelperResponsiveLay
),
GestureDetector(
onTap: () {
context
.read<CeilingSensorBloc>()
.add(GetCeilingDeviceReportsEvent(code: '', deviceUuid: device.uuid!));
context.read<CeilingSensorBloc>().add(GetCeilingDeviceReportsEvent(
code: '', deviceUuid: device.uuid!));
},
child: const PresenceStaticWidget(
icon: Assets.helpDescriptionIcon,

View File

@ -0,0 +1,250 @@
import 'dart:async';
import 'dart:developer';
import 'package:bloc/bloc.dart';
import 'package:equatable/equatable.dart';
import 'package:firebase_database/firebase_database.dart';
import 'package:syncrow_web/pages/device_managment/all_devices/models/device_reports.dart';
import 'package:syncrow_web/pages/device_managment/all_devices/models/device_status.dart';
import 'package:syncrow_web/pages/device_managment/all_devices/models/factory_reset_model.dart';
import 'package:syncrow_web/pages/device_managment/flush_mounted_presence_sensor/models/flush_mounted_presence_sensor_model.dart';
import 'package:syncrow_web/services/batch_control_devices_service.dart';
import 'package:syncrow_web/services/control_device_service.dart';
import 'package:syncrow_web/services/devices_mang_api.dart';
part 'flush_mounted_presence_sensor_event.dart';
part 'flush_mounted_presence_sensor_state.dart';
class FlushMountedPresenceSensorBloc
extends Bloc<FlushMountedPresenceSensorEvent, FlushMountedPresenceSensorState> {
final String deviceId;
final ControlDeviceService controlDeviceService;
final BatchControlDevicesService batchControlDevicesService;
late FlushMountedPresenceSensorModel deviceStatus;
FlushMountedPresenceSensorBloc({
required this.deviceId,
required this.controlDeviceService,
required this.batchControlDevicesService,
}) : super(FlushMountedPresenceSensorInitialState()) {
on<FlushMountedPresenceSensorFetchStatusEvent>(
_onFlushMountedPresenceSensorFetchStatusEvent,
);
on<FlushMountedPresenceSensorFetchBatchStatusEvent>(
_onFlushMountedPresenceSensorFetchBatchStatusEvent);
on<FlushMountedPresenceSensorChangeValueEvent>(
_onFlushMountedPresenceSensorChangeValueEvent,
);
on<FlushMountedPresenceSensorBatchControlEvent>(
_onFlushMountedPresenceSensorBatchControlEvent,
);
on<FlushMountedPresenceSensorGetDeviceReportsEvent>(
_onFlushMountedPresenceSensorGetDeviceReportsEvent);
on<FlushMountedPresenceSensorShowDescriptionEvent>(
_onFlushMountedPresenceSensorShowDescriptionEvent,
);
on<FlushMountedPresenceSensorBackToGridViewEvent>(
_onFlushMountedPresenceSensorBackToGridViewEvent,
);
on<FlushMountedPresenceSensorFactoryResetEvent>(
_onFlushMountedPresenceSensorFactoryResetEvent,
);
on<FlushMountedPresenceSensorStatusUpdatedEvent>(
_onFlushMountedPresenceSensorStatusUpdatedEvent,
);
}
void _onFlushMountedPresenceSensorFetchStatusEvent(
FlushMountedPresenceSensorFetchStatusEvent event,
Emitter<FlushMountedPresenceSensorState> emit,
) async {
emit(FlushMountedPresenceSensorLoadingInitialState());
try {
final response = await DevicesManagementApi().getDeviceStatus(deviceId);
deviceStatus = FlushMountedPresenceSensorModel.fromJson(response.status);
emit(FlushMountedPresenceSensorUpdateState(model: deviceStatus));
_listenToChanges(deviceId);
} catch (e) {
emit(FlushMountedPresenceSensorFailedState(error: e.toString()));
return;
}
}
Future<void> _onFlushMountedPresenceSensorFetchBatchStatusEvent(
FlushMountedPresenceSensorFetchBatchStatusEvent event,
Emitter<FlushMountedPresenceSensorState> emit,
) async {
emit(FlushMountedPresenceSensorLoadingInitialState());
try {
final response = await DevicesManagementApi().getBatchStatus(event.devicesIds);
deviceStatus = FlushMountedPresenceSensorModel.fromJson(response.status);
emit(FlushMountedPresenceSensorUpdateState(model: deviceStatus));
} catch (e) {
emit(FlushMountedPresenceSensorFailedState(error: e.toString()));
}
}
void _listenToChanges(String deviceId) {
try {
final ref = FirebaseDatabase.instance.ref(
'device-status/$deviceId',
);
ref.onValue.listen((event) {
final eventsMap = event.snapshot.value as Map<dynamic, dynamic>;
List<Status> statusList = [];
eventsMap['status'].forEach((element) {
statusList.add(
Status(code: element['code'], value: element['value']),
);
});
deviceStatus = FlushMountedPresenceSensorModel.fromJson(statusList);
if (!isClosed) {
add(FlushMountedPresenceSensorStatusUpdatedEvent(deviceStatus));
}
});
} catch (_) {
log(
'Error listening to changes',
name: 'FlushMountedPresenceSensorBloc._listenToChanges',
);
}
}
void _onFlushMountedPresenceSensorChangeValueEvent(
FlushMountedPresenceSensorChangeValueEvent event,
Emitter<FlushMountedPresenceSensorState> emit,
) async {
emit(FlushMountedPresenceSensorLoadingNewSate(model: deviceStatus));
_updateDeviceFunctionFromCode(event.code, event.value);
emit(FlushMountedPresenceSensorUpdateState(model: deviceStatus));
try {
await controlDeviceService.controlDevice(
deviceUuid: deviceId,
status: Status(code: event.code, value: event.value),
);
} catch (_) {
await _reloadDeviceStatus();
}
}
Future<void> _onFlushMountedPresenceSensorBatchControlEvent(
FlushMountedPresenceSensorBatchControlEvent event,
Emitter<FlushMountedPresenceSensorState> emit,
) async {
emit(FlushMountedPresenceSensorLoadingNewSate(model: deviceStatus));
_updateDeviceFunctionFromCode(event.code, event.value);
emit(FlushMountedPresenceSensorUpdateState(model: deviceStatus));
try {
await batchControlDevicesService.batchControlDevices(
uuids: event.deviceIds,
code: event.code,
value: event.value,
);
} catch (_) {
await _reloadDeviceStatus();
}
}
void _updateDeviceFunctionFromCode(String code, int value) {
switch (code) {
case FlushMountedPresenceSensorModel.codeFarDetection:
deviceStatus.farDetection = value;
break;
case FlushMountedPresenceSensorModel.codeSensitivity:
deviceStatus.sensitivity = value;
break;
case FlushMountedPresenceSensorModel.codeNoneDelay:
deviceStatus.noneDelay = value;
break;
case FlushMountedPresenceSensorModel.codePresenceDelay:
deviceStatus.presenceDelay = value;
break;
case FlushMountedPresenceSensorModel.codeNearDetection:
deviceStatus.nearDetection = value;
break;
case FlushMountedPresenceSensorModel.codeOccurDistReduce:
deviceStatus.occurDistReduce = value;
break;
case FlushMountedPresenceSensorModel.codeSensiReduce:
deviceStatus.sensiReduce = value;
break;
default:
return;
}
}
Future<void> _reloadDeviceStatus() async {
await Future.delayed(const Duration(milliseconds: 500), () {
add(FlushMountedPresenceSensorFetchStatusEvent());
});
}
Future<void> _onFlushMountedPresenceSensorGetDeviceReportsEvent(
FlushMountedPresenceSensorGetDeviceReportsEvent event,
Emitter<FlushMountedPresenceSensorState> emit,
) async {
emit(FlushMountedPresenceSensorDeviceReportsLoadingState());
try {
await DevicesManagementApi.getDeviceReports(deviceId, event.code)
.then((value) {
emit(FlushMountedPresenceSensorDeviceReportsState(
deviceReport: value, code: event.code));
});
} catch (e) {
emit(FlushMountedPresenceSensorDeviceReportsFailedState(error: e.toString()));
return;
}
}
void _onFlushMountedPresenceSensorShowDescriptionEvent(
FlushMountedPresenceSensorShowDescriptionEvent event,
Emitter<FlushMountedPresenceSensorState> emit,
) {
emit(FlushMountedPresenceSensorShowDescriptionState(
description: event.description));
}
void _onFlushMountedPresenceSensorBackToGridViewEvent(
FlushMountedPresenceSensorBackToGridViewEvent event,
Emitter<FlushMountedPresenceSensorState> emit,
) {
emit(FlushMountedPresenceSensorUpdateState(model: deviceStatus));
}
Future<void> _onFlushMountedPresenceSensorFactoryResetEvent(
FlushMountedPresenceSensorFactoryResetEvent event,
Emitter<FlushMountedPresenceSensorState> emit,
) async {
emit(FlushMountedPresenceSensorLoadingNewSate(model: deviceStatus));
try {
final response = await DevicesManagementApi().factoryReset(
event.factoryReset,
event.deviceId,
);
if (!response) {
emit(
const FlushMountedPresenceSensorFailedState(
error: 'Something went wrong with factory reset, please try again',
),
);
} else {
emit(FlushMountedPresenceSensorUpdateState(model: deviceStatus));
}
} catch (e) {
emit(FlushMountedPresenceSensorFailedState(error: e.toString()));
}
}
void _onFlushMountedPresenceSensorStatusUpdatedEvent(
FlushMountedPresenceSensorStatusUpdatedEvent event,
Emitter<FlushMountedPresenceSensorState> emit,
) {
deviceStatus = event.model;
emit(FlushMountedPresenceSensorUpdateState(model: deviceStatus));
}
}

View File

@ -0,0 +1,93 @@
part of 'flush_mounted_presence_sensor_bloc.dart';
sealed class FlushMountedPresenceSensorEvent extends Equatable {
const FlushMountedPresenceSensorEvent();
@override
List<Object> get props => [];
}
class FlushMountedPresenceSensorFetchStatusEvent
extends FlushMountedPresenceSensorEvent {}
class FlushMountedPresenceSensorStatusUpdatedEvent
extends FlushMountedPresenceSensorEvent {
const FlushMountedPresenceSensorStatusUpdatedEvent(this.model);
final FlushMountedPresenceSensorModel model;
@override
List<Object> get props => [model];
}
class FlushMountedPresenceSensorChangeValueEvent
extends FlushMountedPresenceSensorEvent {
final int value;
final String code;
const FlushMountedPresenceSensorChangeValueEvent({
required this.value,
required this.code,
});
@override
List<Object> get props => [value, code];
}
class FlushMountedPresenceSensorFetchBatchStatusEvent
extends FlushMountedPresenceSensorEvent {
final List<String> devicesIds;
const FlushMountedPresenceSensorFetchBatchStatusEvent(this.devicesIds);
@override
List<Object> get props => [devicesIds];
}
class FlushMountedPresenceSensorGetDeviceReportsEvent
extends FlushMountedPresenceSensorEvent {
final String deviceUuid;
final String code;
const FlushMountedPresenceSensorGetDeviceReportsEvent({
required this.deviceUuid,
required this.code,
});
@override
List<Object> get props => [deviceUuid, code];
}
class FlushMountedPresenceSensorShowDescriptionEvent
extends FlushMountedPresenceSensorEvent {
final String description;
const FlushMountedPresenceSensorShowDescriptionEvent({required this.description});
}
class FlushMountedPresenceSensorBackToGridViewEvent
extends FlushMountedPresenceSensorEvent {}
class FlushMountedPresenceSensorBatchControlEvent
extends FlushMountedPresenceSensorEvent {
final List<String> deviceIds;
final String code;
final dynamic value;
const FlushMountedPresenceSensorBatchControlEvent({
required this.deviceIds,
required this.code,
required this.value,
});
@override
List<Object> get props => [deviceIds, code, value];
}
class FlushMountedPresenceSensorFactoryResetEvent
extends FlushMountedPresenceSensorEvent {
final String deviceId;
final FactoryResetModel factoryReset;
const FlushMountedPresenceSensorFactoryResetEvent({
required this.deviceId,
required this.factoryReset,
});
}

View File

@ -0,0 +1,76 @@
part of 'flush_mounted_presence_sensor_bloc.dart';
sealed class FlushMountedPresenceSensorState extends Equatable {
const FlushMountedPresenceSensorState();
@override
List<Object> get props => [];
}
class FlushMountedPresenceSensorInitialState
extends FlushMountedPresenceSensorState {}
class FlushMountedPresenceSensorLoadingInitialState
extends FlushMountedPresenceSensorState {}
class FlushMountedPresenceSensorUpdateState extends FlushMountedPresenceSensorState {
final FlushMountedPresenceSensorModel model;
const FlushMountedPresenceSensorUpdateState({required this.model});
@override
List<Object> get props => [model];
}
class FlushMountedPresenceSensorLoadingNewSate
extends FlushMountedPresenceSensorState {
final FlushMountedPresenceSensorModel model;
const FlushMountedPresenceSensorLoadingNewSate({required this.model});
@override
List<Object> get props => [model];
}
class FlushMountedPresenceSensorFailedState extends FlushMountedPresenceSensorState {
final String error;
const FlushMountedPresenceSensorFailedState({required this.error});
@override
List<Object> get props => [error];
}
class FlushMountedPresenceSensorDeviceReportsLoadingState
extends FlushMountedPresenceSensorState {}
class FlushMountedPresenceSensorDeviceReportsState
extends FlushMountedPresenceSensorState {
const FlushMountedPresenceSensorDeviceReportsState({
required this.deviceReport,
required this.code,
});
final DeviceReport deviceReport;
final String code;
@override
List<Object> get props => [deviceReport, code];
}
class FlushMountedPresenceSensorDeviceReportsFailedState
extends FlushMountedPresenceSensorState {
const FlushMountedPresenceSensorDeviceReportsFailedState({required this.error});
final String error;
@override
List<Object> get props => [error];
}
class FlushMountedPresenceSensorShowDescriptionState
extends FlushMountedPresenceSensorState {
const FlushMountedPresenceSensorShowDescriptionState({required this.description});
final String description;
@override
List<Object> get props => [description];
}

View File

@ -0,0 +1,21 @@
import 'package:syncrow_web/pages/device_managment/flush_mounted_presence_sensor/bloc/flush_mounted_presence_sensor_bloc.dart';
import 'package:syncrow_web/services/batch_control_devices_service.dart';
import 'package:syncrow_web/services/control_device_service.dart';
abstract final class FlushMountedPresenceSensorBlocFactory {
const FlushMountedPresenceSensorBlocFactory._();
static FlushMountedPresenceSensorBloc create({
required String deviceId,
}) {
return FlushMountedPresenceSensorBloc(
deviceId: deviceId,
controlDeviceService: DebouncedControlDeviceService(
decoratee: RemoteControlDeviceService(),
),
batchControlDevicesService: DebouncedBatchControlDevicesService(
decoratee: RemoteBatchControlDevicesService(),
),
);
}
}

View File

@ -0,0 +1,103 @@
import 'package:syncrow_web/pages/device_managment/all_devices/models/device_status.dart';
class FlushMountedPresenceSensorModel {
FlushMountedPresenceSensorModel({
required this.presenceState,
required this.farDetection,
required this.illuminance,
required this.sensitivity,
required this.occurDistReduce,
required this.noneDelay,
required this.presenceDelay,
required this.nearDetection,
required this.sensiReduce,
required this.checkingResult,
});
static const String codePresenceState = 'presence_state';
static const String codeSensitivity = 'sensitivity';
static const String codeNearDetection = 'near_detection';
static const String codeFarDetection = 'far_detection';
static const String codeCheckingResult = 'checking_result';
static const String codePresenceDelay = 'presence_delay';
static const String codeNoneDelay = 'none_delay';
static const String codeOccurDistReduce = 'occur_dist_reduce';
static const String codeIlluminance = 'illum_value';
static const String codeSensiReduce = 'sensi_reduce';
String presenceState;
int sensitivity;
int nearDetection;
int farDetection;
String checkingResult;
int presenceDelay;
int noneDelay;
int occurDistReduce;
int illuminance;
int sensiReduce;
factory FlushMountedPresenceSensorModel.fromJson(List<Status> jsonList) {
String presenceState = 'none';
int sensitivity = 0;
int nearDetection = 0;
int farDetection = 0;
String checkingResult = 'none';
int presenceDelay = 0;
int noneDelay = 0;
int occurDistReduce = 0;
int illuminance = 0;
int sensiReduce = 0;
for (var status in jsonList) {
switch (status.code) {
case codePresenceState:
presenceState = status.value ?? 'presence';
break;
case codeSensitivity:
sensitivity = status.value ?? 0;
break;
case codeNearDetection:
nearDetection = status.value ?? 0;
break;
case codeFarDetection:
farDetection = status.value ?? 0;
break;
case codeCheckingResult:
checkingResult = status.value ?? 'check_success';
break;
case codePresenceDelay:
presenceDelay = status.value ?? 0;
break;
case codeNoneDelay:
noneDelay = status.value ?? 0;
break;
case codeOccurDistReduce:
occurDistReduce = status.value ?? 0;
break;
case codeIlluminance:
illuminance = status.value ?? 0;
break;
case codeSensiReduce:
sensiReduce = status.value ?? 0;
break;
}
}
return FlushMountedPresenceSensorModel(
presenceState: presenceState,
sensitivity: sensitivity,
nearDetection: nearDetection,
farDetection: farDetection,
checkingResult: checkingResult,
presenceDelay: presenceDelay,
noneDelay: noneDelay,
occurDistReduce: occurDistReduce,
illuminance: illuminance,
sensiReduce: sensiReduce,
);
}
}

View File

@ -0,0 +1,185 @@
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:syncrow_web/pages/device_managment/all_devices/models/factory_reset_model.dart';
import 'package:syncrow_web/pages/device_managment/flush_mounted_presence_sensor/bloc/flush_mounted_presence_sensor_bloc.dart';
import 'package:syncrow_web/pages/device_managment/flush_mounted_presence_sensor/factories/flush_mounted_presence_sensor_bloc_factory.dart';
import 'package:syncrow_web/pages/device_managment/flush_mounted_presence_sensor/models/flush_mounted_presence_sensor_model.dart';
import 'package:syncrow_web/pages/device_managment/shared/batch_control/factory_reset.dart';
import 'package:syncrow_web/pages/device_managment/shared/sensors_widgets/presence_update_data.dart';
import 'package:syncrow_web/utils/helpers/responsice_layout_helper/responsive_layout_helper.dart';
class FlushMountedPresenceSensorBatchControlView extends StatelessWidget
with HelperResponsiveLayout {
const FlushMountedPresenceSensorBatchControlView({
required this.devicesIds,
super.key,
});
final List<String> devicesIds;
@override
Widget build(BuildContext context) {
return BlocProvider(
create: (context) => FlushMountedPresenceSensorBlocFactory.create(
deviceId: devicesIds.first,
)..add(FlushMountedPresenceSensorFetchBatchStatusEvent(devicesIds)),
child: BlocBuilder<FlushMountedPresenceSensorBloc,
FlushMountedPresenceSensorState>(
builder: (context, state) {
if (state is FlushMountedPresenceSensorLoadingInitialState ||
state is FlushMountedPresenceSensorDeviceReportsLoadingState) {
return const Center(child: CircularProgressIndicator());
} else if (state is FlushMountedPresenceSensorUpdateState) {
return _buildGridView(context, state.model);
}
return const Center(child: Text('Error fetching status'));
},
),
);
}
Widget _buildGridView(
BuildContext context,
FlushMountedPresenceSensorModel model,
) {
final isExtraLarge = isExtraLargeScreenSize(context);
final isLarge = isLargeScreenSize(context);
final isMedium = isMediumScreenSize(context);
return GridView(
padding: const EdgeInsets.symmetric(horizontal: 50),
shrinkWrap: true,
physics: const NeverScrollableScrollPhysics(),
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: isLarge || isExtraLarge
? 3
: isMedium
? 2
: 1,
mainAxisExtent: 140,
crossAxisSpacing: 12,
mainAxisSpacing: 12,
),
children: [
PresenceUpdateData(
value: model.sensitivity.toDouble(),
title: 'Sensitivity:',
minValue: 0,
maxValue: 9,
steps: 1,
action: (int value) => context.read<FlushMountedPresenceSensorBloc>().add(
FlushMountedPresenceSensorBatchControlEvent(
deviceIds: devicesIds,
code: FlushMountedPresenceSensorModel.codeSensitivity,
value: value,
),
),
),
PresenceUpdateData(
value: (model.nearDetection / 100).clamp(0.0, double.infinity),
title: 'Nearest Detect Dist:',
description: 'm',
minValue: 0.0,
maxValue: 9.5,
steps: 0.1,
valuesPercision: 1,
action: (double value) =>
context.read<FlushMountedPresenceSensorBloc>().add(
FlushMountedPresenceSensorBatchControlEvent(
deviceIds: devicesIds,
code: FlushMountedPresenceSensorModel.codeNearDetection,
value: (value * 100).toInt(),
),
),
),
PresenceUpdateData(
value: (model.farDetection / 100).clamp(0.0, double.infinity),
title: 'Max Detect Dist:',
description: 'm',
minValue: 0.0,
maxValue: 9.5,
steps: 0.1,
valuesPercision: 1,
action: (double value) =>
context.read<FlushMountedPresenceSensorBloc>().add(
FlushMountedPresenceSensorBatchControlEvent(
deviceIds: devicesIds,
code: FlushMountedPresenceSensorModel.codeFarDetection,
value: (value * 100).toInt(),
),
),
),
PresenceUpdateData(
value: model.sensiReduce.toDouble(),
title: 'Trigger Level:',
minValue: 0,
maxValue: 3,
steps: 1,
action: (int value) => context.read<FlushMountedPresenceSensorBloc>().add(
FlushMountedPresenceSensorBatchControlEvent(
deviceIds: devicesIds,
code: FlushMountedPresenceSensorModel.codeSensiReduce,
value: value,
),
),
),
PresenceUpdateData(
value: model.occurDistReduce.toDouble(),
title: 'Indent Level:',
minValue: 0,
maxValue: 3,
steps: 1,
action: (int value) => context.read<FlushMountedPresenceSensorBloc>().add(
FlushMountedPresenceSensorBatchControlEvent(
deviceIds: devicesIds,
code: FlushMountedPresenceSensorModel.codeOccurDistReduce,
value: value,
),
),
),
PresenceUpdateData(
value: (model.presenceDelay / 10).toDouble(),
title: 'Target Confirm Time:',
description: 's',
minValue: 0.0,
maxValue: 0.5,
steps: 0.1,
valuesPercision: 1,
action: (double value) =>
context.read<FlushMountedPresenceSensorBloc>().add(
FlushMountedPresenceSensorBatchControlEvent(
deviceIds: devicesIds,
code: FlushMountedPresenceSensorModel.codePresenceDelay,
value: (value * 10).toInt(),
),
),
),
PresenceUpdateData(
value: ((model.noneDelay / 10).toDouble()),
description: 's',
title: 'Disappe Delay:',
minValue: 20,
maxValue: 300,
steps: 1,
action: (double value) =>
context.read<FlushMountedPresenceSensorBloc>().add(
FlushMountedPresenceSensorBatchControlEvent(
deviceIds: devicesIds,
code: FlushMountedPresenceSensorModel.codeNoneDelay,
value: (value * 10).round(),
),
),
),
FactoryResetWidget(
callFactoryReset: () {
context.read<FlushMountedPresenceSensorBloc>().add(
FlushMountedPresenceSensorFactoryResetEvent(
deviceId: devicesIds.first,
factoryReset: FactoryResetModel(devicesUuid: devicesIds),
),
);
},
),
],
);
}
}

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