Compare commits

..
Author SHA1 Message Date
Faris Armoush 3fc6964e15 Update TotalEnergyConsumptionChart to adjust Y-axis limits and intervals for better data representation (#302)
<!--
  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
2025-06-27 17:16:49 +03:00
Faris Armoush 4744009cb6 Update TotalEnergyConsumptionChart to adjust Y-axis limits and intervals for better data representation 2025-06-27 17:11:17 +03:00
Faris Armoush 5beae81596 fixed datetimes in occupancy heatmap. (#301)
<!--
  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:
-->

## 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
2025-06-27 16:40:20 +03:00
Faris Armoush f1144762b0 Merge branch 'dev' of https://github.com/SyncrowIOT/web into add_loading_indicator_to_analytics_device_dropdown 2025-06-27 16:37:17 +03:00
Faris Armoush ca41aa6224 all dates in heatmap are utc. 2025-06-27 16:37:09 +03:00
Faris Armoush 2d0019200e manually parse event date for heatmap date object. (#300)
<!--
  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:
-->



## 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
2025-06-27 15:29:39 +03:00
Faris Armoush 475462301f manually parse event date for heatmap date object. 2025-06-27 15:29:11 +03:00
Faris Armoush 731ba7a768 Add loading indicator to analytics device dropdown (#299)
<!--
  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:
-->

## Description

Added a loading indicator to analytics device dropdown, for a better UX,
and since the occupancy devices api takes longer than the other tabs, it
makes more sense to add this feature.

## Type of Change

<!--- Put an `x` in all the boxes that apply: -->

- [x] ✨ 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
2025-06-27 14:08:13 +03:00
Faris Armoush 7fda564ee4 hotfixes. 2025-06-27 14:07:24 +03:00
Faris Armoush 11e2853403 Enhance AnalyticsDeviceDropdown to show loading indicator during loading state. 2025-06-27 11:56:43 +03:00
Faris Armoush 9c02bed4c0 Sp 1669 fe user edit form does not pre fill existing data on user management page (#284)
<!--
  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-1669](https://syncrow.atlassian.net/browse/SP-1669)

## Description

when opening the dialog for use info pass the user to fill the data 

## Type of Change

<!--- Put an `x` in all the boxes that apply: -->

- [ ] ✨ New feature (non-breaking change which adds functionality)
- [x] 🛠️ 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 


[SP-1669]:
https://syncrow.atlassian.net/browse/SP-1669?atlOrigin=eyJpIjoiNWRkNTljNzYxNjVmNDY3MDlhMDU5Y2ZhYzA5YTRkZjUiLCJwIjoiZ2l0aHViLWNvbS1KU1cifQ
2025-06-27 11:20:09 +03:00
Faris Armoush 4f932b8c35 [FE] Disable AC Control Button When AC is Off SP-1387 (#283)
<!--
  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-1387](https://syncrow.atlassian.net/browse/SP-1387)

## 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)
- [x] 🛠️ 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 


[SP-1387]:
https://syncrow.atlassian.net/browse/SP-1387?atlOrigin=eyJpIjoiNWRkNTljNzYxNjVmNDY3MDlhMDU5Y2ZhYzA5YTRkZjUiLCJwIjoiZ2l0aHViLWNvbS1KU1cifQ
2025-06-27 11:16:38 +03:00
Faris Armoush 44ae8386df sp:1740-[FE] On login page when a timeout/connection error occurs the exception is getting out of the container and the message should be handled (#286)
<!--
  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-1740](https://syncrow.atlassian.net/browse/SP-1740)

## Description

handling error from backend on login Error for time out connections or
network issues

## Type of Change

<!--- Put an `x` in all the boxes that apply: -->

- [ ] ✨ New feature (non-breaking change which adds functionality)
- [x] 🛠️ 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 


[SP-1740]:
https://syncrow.atlassian.net/browse/SP-1740?atlOrigin=eyJpIjoiNWRkNTljNzYxNjVmNDY3MDlhMDU5Y2ZhYzA5YTRkZjUiLCJwIjoiZ2l0aHViLWNvbS1KU1cifQ
2025-06-27 11:14:25 +03:00
Faris Armoush 9d4a665547 SP-368-Clarification-on-Default-Value-for-Start-Date-in-Door-Lock-Online-Tile-Limited-Password-repeat-section (#296)
<!--
  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-368](https://syncrow.atlassian.net/browse/SP-368)

## Description

1. Shows an error dialog when the users sets the start date in create
visitor password that is before current time.
2. Initially sets the Start Date time as now.

## Type of Change

<!--- Put an `x` in all the boxes that apply: -->

- [x] ✨ 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 


[SP-368]:
https://syncrow.atlassian.net/browse/SP-368?atlOrigin=eyJpIjoiNWRkNTljNzYxNjVmNDY3MDlhMDU5Y2ZhYzA5YTRkZjUiLCJwIjoiZ2l0aHViLWNvbS1KU1cifQ
2025-06-27 11:13:37 +03:00
mohammadnemer1 8593055923 Add deviceName field to FailedOperation and SuccessOperation models (#295)
<!--
  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:
-->


## Description

<!--- Describe your changes in detail -->
Add deviceName field to FailedOperation and SuccessOperation models

## 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)
- [x] ❌ Breaking change (fix or feature that would cause existing
functionality to change)
- [ ] 🧹 Code refactor
- [ ] ✅ Build configuration change
- [ ] 📝 Documentation
- [ ] 🗑️ Chore
2025-06-25 16:12:42 +03:00
mohammadnemer1 18ab9fd24c Add bloc closure handling and improve device status updates in AcBloc (#298)
<!--
  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:
-->



## Description

<!--- Describe your changes in detail -->
Add bloc closure handling and improve device status updates in AcBloc
## Type of Change

<!--- Put an `x` in all the boxes that apply: -->

- [ ] ✨ New feature (non-breaking change which adds functionality)
- [x] 🛠️ 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
2025-06-25 15:59:41 +03:00
mohammad 3c9494963d Add generated configuration files for Flutter integration across platforms 2025-06-25 15:58:58 +03:00
Faris Armoush 07dd260593 Occupancy analytics devices bug (#297)
<!--
  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:
-->

## Description

1. Supported new device type `NCPS` in analytics occupancy tab.
2. Fixed a bug where the devices weren't being shown.
3. removed old comment.
4. Increased size of `OccupancyEndSideBar` in medium sized screens.
5. Fixed TVOC code to read data.
6. Changed TVOC unit string to match the device. 
7. Fixed data loading bug in AQI.

## Type of Change

<!--- Put an `x` in all the boxes that apply: -->

- [ ] ✨ New feature (non-breaking change which adds functionality)
- [x] 🛠️ Bug fix (non-breaking change which fixes an issue)
- [x] ❌ Breaking change (fix or feature that would cause existing
functionality to change)
- [ ] 🧹 Code refactor
- [ ] ✅ Build configuration change
- [ ] 📝 Documentation
- [ ] 🗑️ Chore
2025-06-25 15:58:41 +03:00
mohammad f38ac58442 Add bloc closure handling and improve device status updates in AcBloc 2025-06-25 14:45:10 +03:00
Faris Armoush 30e940fdfc Reads the correct date to load aqi data. 2025-06-25 14:34:23 +03:00
Faris Armoush 520b73717a Doesnt load devices when date changes. 2025-06-25 14:34:07 +03:00
Faris Armoush e1bb67d7bd reads correct value for TVOC. 2025-06-25 14:26:05 +03:00
Faris Armoush 5e0df09cb6 Changed tvoc unit to match the device. 2025-06-25 14:25:53 +03:00
Faris Armoush 22070ca04a removed unused comment. 2025-06-25 13:26:07 +03:00
Faris Armoush 6d667af7dc increased size of OccupancyEndSideBar in medium sized screens. 2025-06-25 13:25:46 +03:00
Faris Armoush 3b4952db0a fixed thrown exceptions inAnalyticsDevice. 2025-06-25 13:25:30 +03:00
Faris Armoush 5f59583696 Supported NCPS device type in occupancy devices dropdown. 2025-06-25 13:25:12 +03:00
Faris Armoush 7397486e7a SP-368-Clarification-on-Default-Value-for-Start-Date-in-Door-Lock-Online-Tile-Limited-Password-repeat-section 2025-06-25 13:11:49 +03:00
Faris Armoush 487c5a894b Sp 1796 fe set the max on range of aqi chart based on selected pollutant s current highest value (#290)
<!--
  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-1796](https://syncrow.atlassian.net/browse/SP-1796)

## Description

Added day of month labels to all analytics charts
Implemented ranges for each pollutant based on the highest value of each
pollutant in the range of aqi chart.

## Type of Change

<!--- Put an `x` in all the boxes that apply: -->

- [x] ✨ New feature (non-breaking change which adds functionality)
- [ ] 🛠️ Bug fix (non-breaking change which fixes an issue)
- [x] ❌ Breaking change (fix or feature that would cause existing
functionality to change)
- [ ] 🧹 Code refactor
- [ ] ✅ Build configuration change
- [ ] 📝 Documentation
- [ ] 🗑️ Chore 


[SP-1796]:
https://syncrow.atlassian.net/browse/SP-1796?atlOrigin=eyJpIjoiNWRkNTljNzYxNjVmNDY3MDlhMDU5Y2ZhYzA5YTRkZjUiLCJwIjoiZ2l0aHViLWNvbS1KU1cifQ
2025-06-25 12:42:55 +03:00
Faris Armoush 7e0200aad8 SP-1770-FE-Parent-nodes-in-community-tree-not-partially-selected-when… (#294)
<!--
  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-1770](https://syncrow.atlassian.net/browse/SP-1770)

## Description

Space Tree Selection state reads from the correct list, based on if the
user was filtering or not.

## Type of Change

<!--- Put an `x` in all the boxes that apply: -->

- [ ] ✨ New feature (non-breaking change which adds functionality)
- [x] 🛠️ 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 


[SP-1770]:
https://syncrow.atlassian.net/browse/SP-1770?atlOrigin=eyJpIjoiNWRkNTljNzYxNjVmNDY3MDlhMDU5Y2ZhYzA5YTRkZjUiLCJwIjoiZ2l0aHViLWNvbS1KU1cifQ
2025-06-25 12:42:34 +03:00
mohammad 562c67a958 Add deviceName field to FailedOperation and SuccessOperation models 2025-06-25 12:24:09 +03:00
Faris Armoush 52b843d514 SP-1770-FE-Parent-nodes-in-community-tree-not-partially-selected-when-selecting-space-from-sidebar. 2025-06-25 09:53:09 +03:00
mohammadnemer1 423ad6e687 Enhance navigation buttons in SmartPowerDeviceControl for better user… (#293)
… experience

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



## Description

<!--- Describe your changes in detail -->
Enhance navigation buttons in SmartPowerDeviceControl 

## Type of Change

<!--- Put an `x` in all the boxes that apply: -->

- [ ] ✨ New feature (non-breaking change which adds functionality)
- [x] 🛠️ 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
2025-06-25 09:13:33 +03:00
Rafeek-khoudare 932e50f518 sp:1677 [FE] Device status in Control modal always shows "Online" regardless of actual status (#287)
<!--
  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-1677](https://syncrow.atlassian.net/browse/SP-1677)

## Description

status depend on the real status of the device afterit was static 

## Type of Change

<!--- Put an `x` in all the boxes that apply: -->

- [ ] ✨ New feature (non-breaking change which adds functionality)
- [x] 🛠️ 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 


[SP-1677]:
https://syncrow.atlassian.net/browse/SP-1677?atlOrigin=eyJpIjoiNWRkNTljNzYxNjVmNDY3MDlhMDU5Y2ZhYzA5YTRkZjUiLCJwIjoiZ2l0aHViLWNvbS1KU1cifQ
2025-06-25 08:18:10 +03:00
mohammad c649044a1f Enhance navigation buttons in SmartPowerDeviceControl for better user experience 2025-06-24 16:40:42 +03:00
mohammadnemer1 c46cfb04a7 Add countdown seconds to schedule management (#291)
<!--
  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:
-->


## Description

<!--- Describe your changes in detail -->
Add countdown seconds to schedule management
## Type of Change

<!--- Put an `x` in all the boxes that apply: -->

- [ ] ✨ New feature (non-breaking change which adds functionality)
- [x] 🛠️ 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
2025-06-24 16:21:43 +03:00
mohammadnemer1 8754960713 Cancel-button-on-"Create-Visitor-Password"-modal-unnecessarily-triggers-visitor-passwords-API (#292)
… on pop

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



## 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)
- [x] 🛠️ 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
2025-06-24 16:21:03 +03:00
mohammad c6e98fa245 Refactor visitor password dialog navigation to return specific values on pop 2025-06-24 16:06:53 +03:00
mohammad 277a9ce4f0 Add countdown seconds to schedule management 2025-06-24 15:38:16 +03:00
Faris Armoush f901983aa5 Implemented ranges for the values in the AQI chart based on the selected pollutant. 2025-06-24 15:01:25 +03:00
Faris Armoush 010403f1fa Added day of month axis name to all charts. 2025-06-24 14:50:22 +03:00
Faris Armoush ee1ebeae2e Changed energy management charts titles for a more clear name. 2025-06-24 14:45:15 +03:00
Faris Armoush 6e6ef79ed0 enhanced heat map tooltip's message. 2025-06-24 14:44:56 +03:00
Faris Armoush 7e5825de45 Fixed typo in occupancy sidebar. 2025-06-24 14:44:45 +03:00
Faris Armoush db9e856bca Sp 1711 fe implement blank state (#288)
<!--
  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-1711](https://syncrow.atlassian.net/browse/SP-1711)
[SP-1712](https://syncrow.atlassian.net/browse/SP-1712)

## Description

1. Shows tooltip on space cell.
2. navigates to space when a new space is selected
3. Fixed a thrown Exception because of an Expanded widget
4. Adjusted connections between spaces to select from and to nodes.
5. Created a `SpaceDetailsDialog` and a helper class to show it, because
a space can be created through different widgets, and this was done to
unify the logic and remove code duplication.

## Type of Change

<!--- Put an `x` in all the boxes that apply: -->

- [x] ✨ New feature (non-breaking change which adds functionality)
- [x] 🛠️ 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 


[SP-1711]:
https://syncrow.atlassian.net/browse/SP-1711?atlOrigin=eyJpIjoiNWRkNTljNzYxNjVmNDY3MDlhMDU5Y2ZhYzA5YTRkZjUiLCJwIjoiZ2l0aHViLWNvbS1KU1cifQ

[SP-1712]:
https://syncrow.atlassian.net/browse/SP-1712?atlOrigin=eyJpIjoiNWRkNTljNzYxNjVmNDY3MDlhMDU5Y2ZhYzA5YTRkZjUiLCJwIjoiZ2l0aHViLWNvbS1KU1cifQ
2025-06-24 12:29:25 +03:00
mohammadnemer1 07435ec89e On access management page Create visitor password dialog is not responsive (#289)
<!--
  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-1474](https://syncrow.atlassian.net/browse/SP-1474)

## Description

<!--- Describe your changes in detail -->
Add responsive input fields and radio groups for visitor password setup

## Type of Change

<!--- Put an `x` in all the boxes that apply: -->

- [ ] ✨ New feature (non-breaking change which adds functionality)
- [x] 🛠️ 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 


[SP-1474]:
https://syncrow.atlassian.net/browse/SP-1474?atlOrigin=eyJpIjoiNWRkNTljNzYxNjVmNDY3MDlhMDU5Y2ZhYzA5YTRkZjUiLCJwIjoiZ2l0aHViLWNvbS1KU1cifQ
2025-06-24 12:27:39 +03:00
mohammad 2a2fb7ffca Add responsive input fields and radio groups for visitor password setup 2025-06-24 11:36:50 +03:00
Faris Armoush 5a2299ea2f navigates to initial create space dialog from the respective buttons. 2025-06-24 10:47:48 +03:00
Faris Armoush 90f8305aa1 shows tooltip on SpaceCell. 2025-06-24 10:45:13 +03:00
Faris Armoush 329b2ba472 selects the space from and to connection when selecting a space. 2025-06-24 10:36:13 +03:00
Faris Armoush 0fb9149613 Selects all children of a space when selecting a parent. 2025-06-24 10:30:56 +03:00
Faris Armoush 87b45fff1d removed expanded widget that caused a size exception. 2025-06-24 10:21:25 +03:00
Faris Armoush 95ae50d12d navigates to selected space when changed on sidebar in space management canvas. 2025-06-24 10:16:03 +03:00
raf-dev1 95d6e1ecda if online go green with online status else red with offline status 2025-06-23 16:33:45 +03:00
Faris Armoush 479aa4a091 Sp 1713 implement empty state (#285)
<!--
  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-1713](https://syncrow.atlassian.net/browse/SP-1713)

## Description

Implemented non selected space state
Implemented an initial version of the canvas.

## Type of Change

<!--- Put an `x` in all the boxes that apply: -->

- [x] ✨ 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 


[SP-1713]:
https://syncrow.atlassian.net/browse/SP-1713?atlOrigin=eyJpIjoiNWRkNTljNzYxNjVmNDY3MDlhMDU5Y2ZhYzA5YTRkZjUiLCJwIjoiZ2l0aHViLWNvbS1KU1cifQ
2025-06-23 16:27:42 +03:00
Faris Armoush 75efc595b4 reverted to old import to avoid confusion with QA team. 2025-06-23 16:22:11 +03:00
raf-dev1 d14cc785a8 no need for two condtions inside themselves 2025-06-23 16:16:34 +03:00
raf-dev1 379ecec789 use isCurrentStep instead of checking with multi variables everyTime 2025-06-23 16:15:05 +03:00
raf-dev1 ad00cf35ba added the PR notes 2025-06-23 16:05:16 +03:00
raf-dev1 5276f4186c emit error state when catch error and send the real API exception 2025-06-23 15:55:56 +03:00
Faris Armoush 8bc7a3daa2 Implemented space management canvas. 2025-06-23 15:45:49 +03:00
raf-dev1 e6957d566d use RoleUserModel instead of only id and all is good 2025-06-23 15:29:35 +03:00
raf-dev1 71cf0a636e send all user instead of only uuid 2025-06-23 15:22:24 +03:00
raf-dev1 1200a809c2 now cant use offline device to controll 2025-06-23 14:33:56 +03:00
mohammadnemer1 03a6c5474b SP-1768-FE-The-white-are-in-empty-devices-table-should-take-the-whole-table-size-not-just-the-top (#282)
<!--
  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-1768](https://syncrow.atlassian.net/browse/SP-1768)

## Description

<!--- Describe your changes in detail -->
fix white container take only a small part of the device table

## Type of Change

<!--- Put an `x` in all the boxes that apply: -->

- [ ] ✨ New feature (non-breaking change which adds functionality)
- [x] 🛠️ 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 


[SP-1768]:
https://syncrow.atlassian.net/browse/SP-1768?atlOrigin=eyJpIjoiNWRkNTljNzYxNjVmNDY3MDlhMDU5Y2ZhYzA5YTRkZjUiLCJwIjoiZ2l0aHViLWNvbS1KU1cifQ
2025-06-23 11:14:32 +03:00
Faris Armoush ada7daf179 Switched from using Text to SelectableText in CreateCommunityDialog. 2025-06-23 10:13:30 +03:00
Faris Armoush 4bdb487094 doesnt show a snackbar when creating a community fails, since we show the error message in the dialog itself. 2025-06-23 10:11:23 +03:00
Faris Armoush f8e4c89cdb uses correct error message that the api sends in RemoteCreateCommunityService. 2025-06-23 10:11:03 +03:00
Faris Armoush 7d4cdba0ef Connected templates view into SpaceManagementBody, while applying the correct UI principals if what to show what when? 2025-06-23 10:06:59 +03:00
Faris Armoush a78b5993a9 Created SpaceManagementTemplatesView widget. 2025-06-23 10:05:53 +03:00
Faris Armoush 0e7109a19e Created CommunityTemplateCell widget. 2025-06-23 10:02:15 +03:00
Faris Armoush ff3d5cd996 Created a helper class to show create community dialog, since this dialog can be shown from two different widgets. 2025-06-23 10:02:02 +03:00
mohammad 5f30a5a61b Refactor empty state widget to use a container for better layout control 2025-06-23 10:01:01 +03:00
Faris Armoush 0712e6d64b Sp 1593 reworks (#277)
<!--
  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-1593](https://syncrow.atlassian.net/browse/SP-1593)

## Description

1. AQI Distribution chart bars when all values are empty.
2. Min element in Y axis of Range of AQI chart is visible.
3. Matched AQI chart titles to have the same size for consistency.
4. Allowed `RangeOfAqiValue` model's values to be nullable, and they
fallback to `0` when null.
5. Implemented AQI Legend.
6. Increased the size of AQI Distribution chart's tooltip.
7. Improved alignment of location cell.
8. Doesn't fetch devices on date change.

## Type of Change

<!--- Put an `x` in all the boxes that apply: -->

- [x] ✨ New feature (non-breaking change which adds functionality)
- [x] 🛠️ 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 


[SP-1593]:
https://syncrow.atlassian.net/browse/SP-1593?atlOrigin=eyJpIjoiNWRkNTljNzYxNjVmNDY3MDlhMDU5Y2ZhYzA5YTRkZjUiLCJwIjoiZ2l0aHViLWNvbS1KU1cifQ
2025-06-23 09:48:17 +03:00
Faris Armoush a493ae08ce SP-1710-FE-Create-Sidebar (#278)
<!--
  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-1710](https://syncrow.atlassian.net/browse/SP-1710)

## Description

1. Implemented Space Management Community Side Tree.
2. Implemented Creating a new community feature.

## Type of Change

<!--- Put an `x` in all the boxes that apply: -->

- [x] ✨ 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 


[SP-1710]:
https://syncrow.atlassian.net/browse/SP-1710?atlOrigin=eyJpIjoiNWRkNTljNzYxNjVmNDY3MDlhMDU5Y2ZhYzA5YTRkZjUiLCJwIjoiZ2l0aHViLWNvbS1KU1cifQ
2025-06-23 09:36:30 +03:00
Faris Armoush 27349a6cc0 Implemented PR notes by extracting widgets into their own classes. 2025-06-23 09:24:53 +03:00
raf-dev1 d17d4184be fix it and add lock to open when press (as loved simple animation) (#280)
with adding the timer as circle

<!--
  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-1567](https://syncrow.atlassian.net/browse/SP-1567)

## Description

alll is goood it is now listening to changes and when unlockRequest up
to 0 and under 30 it is start to give permission

## Type of Change

<!--- Put an `x` in all the boxes that apply: -->

- [ ] ✨ New feature (non-breaking change which adds functionality)
- [x] 🛠️ 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 


[SP-1567]:
https://syncrow.atlassian.net/browse/SP-1567?atlOrigin=eyJpIjoiNWRkNTljNzYxNjVmNDY3MDlhMDU5Y2ZhYzA5YTRkZjUiLCJwIjoiZ2l0aHViLWNvbS1KU1cifQ
2025-06-23 08:19:22 +03:00
Faris Armoush 41d4fbb555 Extracted pagination data into a generic DTO. 2025-06-22 16:00:20 +03:00
mohammadnemer1 fccb5cbbab SP-1606-FE-Fix-Search-Function-on-Devices-Screen-to-Support-All-Device-Types-Flush-Mounted-Sensor (#281)
…ctionality

<!--
  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-1606](https://syncrow.atlassian.net/browse/SP-1606)

## Description

<!--- Describe your changes in detail -->
search by product name and device name 

## Type of Change

<!--- Put an `x` in all the boxes that apply: -->

- [ ] ✨ New feature (non-breaking change which adds functionality)
- [x] 🛠️ 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 


[SP-1606]:
https://syncrow.atlassian.net/browse/SP-1606?atlOrigin=eyJpIjoiNWRkNTljNzYxNjVmNDY3MDlhMDU5Y2ZhYzA5YTRkZjUiLCJwIjoiZ2l0aHViLWNvbS1KU1cifQ
2025-06-22 15:49:19 +03:00
mohammad 48d7ab430f refactor: rename productName to deviceNameOrProductName in search functionality 2025-06-22 15:35:46 +03:00
Faris Armoush 28ac911f3f Accomodated for null values in SpaceModel. 2025-06-22 15:30:47 +03:00
raf-dev1 a793cc3967 fix it and add lock to open when press (as loved simple animation)
with adding the timer as circle
2025-06-22 15:24:53 +03:00
Faris Armoush 09446844b0 reverted initializing the new space management page in the router, to avoid any confusion with the QA team. 2025-06-22 15:11:38 +03:00
Faris Armoush f02788eaa5 implemented create community feature. 2025-06-22 14:58:38 +03:00
mohammadnemer1 614db4333c Refactor ScheduleBloc and related components to use dynamic category … (#279)
<!--
  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:
-->



## Description

<!--- Describe your changes in detail -->
Fix scheduling Bugs

## Type of Change

<!--- Put an `x` in all the boxes that apply: -->

- [ ] ✨ New feature (non-breaking change which adds functionality)
- [x] 🛠️ 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
2025-06-22 14:44:18 +03:00
Faris Armoush b79ab06d95 shows a loading indicator when loading. 2025-06-22 12:58:45 +03:00
mohammad 0a424300aa Refactor ScheduleBloc and related components to use dynamic category handling for schedule events 2025-06-22 12:46:54 +03:00
Faris Armoush 8494f0a8f1 matched community and space models with API. 2025-06-22 12:38:54 +03:00
mohammadnemer1 ec12b970b0 Refactor schedule components and update imports for garage door and w… (#271)
…ater heater modules

<!--
  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-1620](https://syncrow.atlassian.net/browse/SP-1620)

## Description

<!--- Describe your changes in detail -->
Refactor schedule components and make this fetcher as a reusable and fix
bugs related to it

## Type of Change

<!--- Put an `x` in all the boxes that apply: -->

- [ x] ✨ New feature (non-breaking change which adds functionality)
- [x] 🛠️ 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 


[SP-1620]:
https://syncrow.atlassian.net/browse/SP-1620?atlOrigin=eyJpIjoiNWRkNTljNzYxNjVmNDY3MDlhMDU5Y2ZhYzA5YTRkZjUiLCJwIjoiZ2l0aHViLWNvbS1KU1cifQ
2025-06-22 12:27:03 +03:00
Faris Armoush 65ed94eb08 debounce and refactored CommunitiesBloc. 2025-06-22 12:01:32 +03:00
Faris Armoush 51c088d998 made communities paginatable. 2025-06-22 11:11:25 +03:00
Faris Armoush 2f233db332 implemented space management side bar. 2025-06-22 11:04:39 +03:00
Faris Armoush 1f82e84115 doesnt fetch devices on date change. 2025-06-22 10:55:41 +03:00
raf-dev1 5da25d8ecb Sp 1612 fe user cannot see the horizontal scroll on any of the tables they have to hover over it but it s not obvious that they can do that (#274)
<!--
  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-1612](https://syncrow.atlassian.net/browse/SP-1612)

## Description

PROBLEM IS SOLVED before but i added comment to insure that 

## Type of Change

<!--- Put an `x` in all the boxes that apply: -->

- [ ] ✨ New feature (non-breaking change which adds functionality)
- [x] 🛠️ 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 


[SP-1612]:
https://syncrow.atlassian.net/browse/SP-1612?atlOrigin=eyJpIjoiNWRkNTljNzYxNjVmNDY3MDlhMDU5Y2ZhYzA5YTRkZjUiLCJwIjoiZ2l0aHViLWNvbS1KU1cifQ
2025-06-22 08:43:13 +03:00
Faris Armoush 23c3bf11f9 Improved alignment of AqiLocationInfoCell. 2025-06-19 15:38:28 +03:00
Faris Armoush 5201a65a97 matched sizes of bottom titles in aqi charts. 2025-06-19 15:19:58 +03:00
Faris Armoush e4cc5fce50 Increased the size of AqiDistributionChart tooltip. 2025-06-19 15:18:18 +03:00
Faris Armoush 8dea89db0e Implemented AQI legend. 2025-06-19 15:12:54 +03:00
Faris Armoush ad5ada9d55 allowed RangeOfAqiValue values to be nullable, and if they were null they fallback to zero. 2025-06-19 14:24:49 +03:00
Faris Armoush 7172a0e3fb Matched aqi charts title's to have the same size no matter what the window size is. 2025-06-19 14:23:39 +03:00
Faris Armoush 78898968e8 include min in RangeOfAqiChartsHelper.titlesData.leftTitles. 2025-06-19 14:23:04 +03:00
Faris Armoush 666c64231f hides bars in AqiDistributionChart where all values are zero. 2025-06-19 14:22:37 +03:00
Faris Armoush 5b5a94cf65 analytics hotfixes. (#275)
<!--
  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:
-->

## Description

analytics hotfixes.

## Type of Change

<!--- Put an `x` in all the boxes that apply: -->

- [ ] ✨ New feature (non-breaking change which adds functionality)
- [x] 🛠️ 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
2025-06-19 11:20:18 +03:00
Faris Armoush d45fa4c957 analytics hotfixes. 2025-06-19 11:12:12 +03:00
raf-dev1 e39c6abd32 show curtain in devices and implement dialog for if and then (#263)
last integrate with backend

<!--
  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-1728](https://syncrow.atlassian.net/browse/SP-1728)

## Description

implement the dialog for CURTAIN and make it appears with devices in
making Routine
integrate it with backend and test it 
## Type of Change

<!--- Put an `x` in all the boxes that apply: -->

- [x] ✨ 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 


[SP-1728]:
https://syncrow.atlassian.net/browse/SP-1728?atlOrigin=eyJpIjoiNWRkNTljNzYxNjVmNDY3MDlhMDU5Y2ZhYzA5YTRkZjUiLCJwIjoiZ2l0aHViLWNvbS1KU1cifQ
2025-06-19 10:45:06 +03:00
raf-dev1 fc6ea640a7 Merge branch 'dev' into SP-1612-FE-User-cannot-see-the-horizontal-scroll-on-any-of-the-tables-they-have-to-hover-over-it-but-it-s-not-obvious-that-they-can-do-that 2025-06-19 10:40:46 +03:00
raf-dev1 09c44f8a5f add comment for problem solve 2025-06-19 09:33:45 +03:00
raf-dev1 c178c36824 remove duplicate feature (#272)
<!--
  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
there is no ticket for this hot edit

## Description

remove Duplicate Feature in space management 

## Type of Change

<!--- Put an `x` in all the boxes that apply: -->

- [ ] ✨ New feature (non-breaking change which adds functionality)
- [x] 🛠️ 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
2025-06-19 09:05:03 +03:00
raf-dev1 ce96afd7af PR fixes 2025-06-19 09:03:24 +03:00
raf-dev1 27dfa0a05a remove duplicate feature 2025-06-19 08:56:41 +03:00
raf-dev1 78979a4375 SP-1661-fe-enhance-the-landing-page-to-be-responsive-and-look-like-design_again (#266)
<!--
  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:1661](https://syncrow.atlassian.net/browse/SP-1661)
## Description

enhance UI in landing page

## Type of Change

<!--- Put an `x` in all the boxes that apply: -->

- [ ] ✨ New feature (non-breaking change which adds functionality)
- [x] 🛠️ 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
2025-06-18 15:54:46 +03:00
Faris Armoush ea19387605 Hotfix/communities loading (#269)
<!--
  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:
-->

## Description

Hotfix/ communities loading v2.

## Type of Change

<!--- Put an `x` in all the boxes that apply: -->

- [ ] ✨ New feature (non-breaking change which adds functionality)
- [x] 🛠️ 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
2025-06-18 15:29:18 +03:00
Faris Armoush 5b33a8617e Merge branch 'dev' of https://github.com/SyncrowIOT/web into hotfix/communities_loading 2025-06-18 15:25:43 +03:00
Faris Armoush 34565a7dab hotfix/communities_loading v2. 2025-06-18 15:25:32 +03:00
mohammadnemer1 caf1ff5c7e Fix energy device condition and community and space dialog (#268)
<!--
  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:
-->



## Description

<!--- Describe your changes in detail -->
fix energy dialog and fix reset value in text form and fix create dialog
routine

## Type of Change

<!--- Put an `x` in all the boxes that apply: -->

- [ ] ✨ New feature (non-breaking change which adds functionality)
- [x] 🛠️ 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
2025-06-18 15:01:08 +03:00
mohammad 01e8002c43 fix: adjust spacing in create new routines dialog for improved layout 2025-06-18 14:55:12 +03:00
mohammad 63da660ece refactor: update function handling in routine dialogs 2025-06-18 14:40:25 +03:00
Faris Armoush 567d0e2d20 hotfix/communities_loading (#267)
<!--
  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:
-->

## Description

hotfix/ loading communities.
## 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)
- [x] ❌ Breaking change (fix or feature that would cause existing
functionality to change)
- [ ] 🧹 Code refactor
- [ ] ✅ Build configuration change
- [ ] 📝 Documentation
- [ ] 🗑️ Chore
2025-06-18 14:37:00 +03:00
Faris Armoush 45e6ea3259 hotfix/communities_loading 2025-06-18 14:33:39 +03:00
raf-dev1 e942957a47 enhance it Done and yazan has watched it 2025-06-18 12:49:58 +03:00
mohammad b9a3b9c719 fix: update dropdown styles and dimensions for better UI consistency 2025-06-18 12:14:17 +03:00
raf-dev1 f5500dfe50 bug fixed it is locally change the state now (#264)
<!--
  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-SP-1413](https://syncrow.atlassian.net/jira/software/projects/SP/boards/5?assignee=712020%3A71e88a7f-7752-44b3-8177-4ab51a950811&selectedIssue=SP-1413)

## Description

state now changes without fetching API it is locallly

## Type of Change

<!--- Put an `x` in all the boxes that apply: -->

- [ ] ✨ New feature (non-breaking change which adds functionality)
- [x] 🛠️ 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
2025-06-18 09:51:06 +03:00
Faris Armoush 20d044f2e5 Merge branch 'dev' of https://github.com/SyncrowIOT/web into SP-1710-FE-Create-Sidebar 2025-06-18 09:44:35 +03:00
Faris Armoush 8caee32822 Initialized new SpaceManagementPage. 2025-06-18 09:39:49 +03:00
Faris Armoush 6c4bc0d634 Sp 1709 fe blocs and services (#260)
<!--
  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-1709](https://syncrow.atlassian.net/browse/SP-1709)

## Description

Created blocs, services, params, and models for those parts of the space
management module:

1. Communities
2. Update Community
3. Create Community
4. Products
5. Space Details
6. Tags
7. Update Community
8. Update Space

## Type of Change

<!--- Put an `x` in all the boxes that apply: -->

- [x] ✨ 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 


[SP-1709]:
https://syncrow.atlassian.net/browse/SP-1709?atlOrigin=eyJpIjoiNWRkNTljNzYxNjVmNDY3MDlhMDU5Y2ZhYzA5YTRkZjUiLCJwIjoiZ2l0aHViLWNvbS1KU1cifQ
2025-06-18 09:14:04 +03:00
raf-dev1 1ba1aba54e PR fixes and tested 2025-06-18 08:42:18 +03:00
raf-dev1 09f2123946 bug fixed it is locally change the state now 2025-06-17 16:21:01 +03:00
Faris Armoush 8fc6e54ecc SP-1737-FE-The-user-appears-as-Null-and-the-project-uuid-is-null-when-we-login-in-after-a-credentials-error (#259)
<!--
  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-1723](https://syncrow.atlassian.net/browse/SP-1723)

## Description

Loads user data on the initial state of `HomePage` instead of loading it
in the `MaterialApp`, because in the previous solution that caused
temporal coupling.

## Type of Change

<!--- Put an `x` in all the boxes that apply: -->

- [ ] ✨ New feature (non-breaking change which adds functionality)
- [x] 🛠️ 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 


[SP-1723]:
https://syncrow.atlassian.net/browse/SP-1723?atlOrigin=eyJpIjoiNWRkNTljNzYxNjVmNDY3MDlhMDU5Y2ZhYzA5YTRkZjUiLCJwIjoiZ2l0aHViLWNvbS1KU1cifQ
2025-06-17 14:33:06 +03:00
Faris Armoush 5d3380ef82 fixed merge conflict. 2025-06-17 14:30:34 +03:00
Faris Armoush 5b0710957d Merge branch 'dev' into SP-1737-FE-The-user-appears-as-Null-and-the-project-uuid-is-null-when-we-login-in-after-a-credentials-error 2025-06-17 14:26:28 +03:00
raf-dev1 056a1daadc show curtain in devices and implement dialog for if and then
last integrate with backend
2025-06-17 13:34:23 +03:00
Faris Armoush 0132805713 Fix PR notes. 2025-06-17 11:26:48 +03:00
Faris Armoush 35f975b261 Merge branch 'main' into dev 2025-06-17 09:17:19 +03:00
Faris Armoush 9600f4fb8b Made CommunitiesState final, to better document that it shouldn't be extended. 2025-06-16 16:49:31 +03:00
Faris Armoush 5cd1384000 Refactored CommunitiesBloc to ensure the CommunitiesService is properly defined as a final member, enhancing clarity and maintainability. Adjusted CommunitiesState to maintain consistent property definitions. 2025-06-16 16:48:08 +03:00
Faris Armoush 0260523121 Made CommunitiesBloc state object one class, instead of multiple, to make searching and pagination easier. 2025-06-16 16:47:47 +03:00
Faris Armoush 6af96fadbd renamed devices module, to products. 2025-06-16 16:39:34 +03:00
Faris Armoush 737762bbaf Created create community bloc, services, and param. 2025-06-16 16:01:09 +03:00
Faris Armoush 6bcfb77a06 Created update community bloc, services, and param. 2025-06-16 16:01:01 +03:00
Faris Armoush 6b76827f21 Created update space service, and bloc. 2025-06-16 15:48:16 +03:00
Faris Armoush 519285fa7c Implemented proper error handling. 2025-06-16 15:41:52 +03:00
Faris Armoush 3eb38d28f7 Implemented devices bloc, service, param and model. 2025-06-16 15:39:40 +03:00
Faris Armoush 2108622b5b moved tags into modules folder. 2025-06-16 15:35:12 +03:00
Faris Armoush ac44af54a3 Implemented tags bloc, services, models, and params 2025-06-16 15:27:50 +03:00
Faris Armoush aa141ef54d Implemented Space details blocs, services, params, and models. 2025-06-16 15:24:17 +03:00
Faris Armoush b0aea94b91 Created communities blocs, services, models, and params. 2025-06-16 15:20:44 +03:00
Faris Armoush 96f463229c SP-1723-FE-Integrate-Charts-with-API-s-for-AQI-sensor. 2025-06-16 13:12:57 +03:00
Faris Armoush 4d9145a953 Sp 1723 fe integrate charts with api s for aqi sensor (#256)
<!--
  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-1723](https://syncrow.atlassian.net/browse/SP-1723)

## Description

Connected AQI dashboard API's into their respective charts.
Fixed a few lint warnings.

## Type of Change

<!--- Put an `x` in all the boxes that apply: -->

- [x] ✨ 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)
- [x] 🧹 Code refactor
- [ ] ✅ Build configuration change
- [ ] 📝 Documentation
- [ ] 🗑️ Chore 


[SP-1723]:
https://syncrow.atlassian.net/browse/SP-1723?atlOrigin=eyJpIjoiNWRkNTljNzYxNjVmNDY3MDlhMDU5Y2ZhYzA5YTRkZjUiLCJwIjoiZ2l0aHViLWNvbS1KU1cifQ
2025-06-16 12:11:40 +03:00
Faris Armoush a2f897c3a6 SP-1723-FE-Integrate-Charts-with-API-s-for-AQI-sensor. (#258)
<!--
  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-1732](https://syncrow.atlassian.net/browse/SP-1732)

## Description

changed occupancy heat map graident alignment, and increased opacity for
low values cells in the heatmap.

## Type of Change

<!--- Put an `x` in all the boxes that apply: -->

- [ ] ✨ New feature (non-breaking change which adds functionality)
- [x] 🛠️ 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 


[SP-1732]:
https://syncrow.atlassian.net/browse/SP-1732?atlOrigin=eyJpIjoiNWRkNTljNzYxNjVmNDY3MDlhMDU5Y2ZhYzA5YTRkZjUiLCJwIjoiZ2l0aHViLWNvbS1KU1cifQ
2025-06-16 12:11:22 +03:00
mohammadnemer1 249c2fb172 Refactor sub-space dialog to use Bloc for state management and simpli… (#255)
…fy confirmation handling

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



## Description

<!--- Describe your changes in detail -->
Fix subSpace select options 

## Type of Change

<!--- Put an `x` in all the boxes that apply: -->

- [ ] ✨ New feature (non-breaking change which adds functionality)
- [x] 🛠️ 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
2025-06-16 11:34:55 +03:00
Faris Armoush 7a8537d39c SP-1683-FE-Charts-data-are-still-overlapping (#257)
<!--
  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-1638](https://syncrow.atlassian.net/browse/SP-1638)

## Description

Hides bottom left minimum axis title of energy management charts.

## Type of Change

<!--- Put an `x` in all the boxes that apply: -->

- [ ] ✨ New feature (non-breaking change which adds functionality)
- [x] 🛠️ 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 


[SP-1638]:
https://syncrow.atlassian.net/browse/SP-1638?atlOrigin=eyJpIjoiNWRkNTljNzYxNjVmNDY3MDlhMDU5Y2ZhYzA5YTRkZjUiLCJwIjoiZ2l0aHViLWNvbS1KU1cifQ
2025-06-16 11:33:30 +03:00
Faris Armoush 1da0cdad4b SP-1723-FE-Integrate-Charts-with-API-s-for-AQI-sensor. 2025-06-16 11:26:27 +03:00
Faris Armoush d10df2ffb8 SP-1723-FE-Integrate-Charts-with-API-s-for-AQI-sensor 2025-06-16 11:17:33 +03:00
Faris Armoush 6ff9c602f1 SP-1723-FE-Integrate-Charts-with-API-s-for-AQI-sensor. 2025-06-16 10:59:51 +03:00
Faris Armoush 5f20d52e57 doesnt load aqi data when spaceUuid is empty. 2025-06-16 10:46:48 +03:00
Faris Armoush 362557d0d0 removed filtered data fromAirQualityDistributionBloc since it isnt needed for this bloc. 2025-06-16 10:29:31 +03:00
Faris Armoush 312d185932 unsort all data from AqiDistributionChart since the api returns it sorted, and the pacakge handles sorting. 2025-06-16 10:29:03 +03:00
Faris Armoush 89e12e47da ajusted AqiType.codes to match the api. 2025-06-16 10:28:26 +03:00
Faris Armoush a0d9819532 Deleted FakeRangeOfAqiService. 2025-06-16 09:30:50 +03:00
Faris Armoush 1316820954 Injected RemoteRangeOfAqiService into RangeOfAqiBloc instead of using FakeRangeOfAqiService, because the API is ready. 2025-06-16 09:30:39 +03:00
Faris Armoush 5591c78d88 Refactor RemoteRangeOfAqiService to update API endpoint to match what the actual endpoint is. 2025-06-16 09:29:31 +03:00
mohammad eaff7c4a52 Remove unused didUpdateWidget method from SubSpaceDialog 2025-06-16 09:21:36 +03:00
mohammad 37b21ecdfb Refactor sub-space dialog to use Bloc for state management and simplify confirmation handling 2025-06-15 16:18:42 +03:00
Faris Armoush 1567f10827 Revert "enhanced ci/cd by not running the deply jobs on the PR itself… (#237)
…, 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."

This reverts commit f19120c754.

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

## 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
- [x] 🗑️ Chore
2025-06-11 09:53:19 +03:00
Faris Armoush cdbd90b54c Revert "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."
This reverts commit f19120c754.
2025-06-11 09:52:08 +03:00
Faris Armoush 03f5c869c6 chore/add-dependabot (#232)
<!--
  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:
-->

## Description

Added `dependabot` configuration, which notifies us of any updated
dependencies, and if there is security aspects that we'd need to take
care of.

## Type of Change

<!--- Put an `x` in all the boxes that apply: -->

- [x] 🗑️ Chore
2025-06-04 16:41:42 +03:00
Faris Armoush 4f98891902 Created dependabot.yaml 2025-06-04 13:13:07 +03:00
Faris Armoush 7002bbfa04 CI/CD Enhancements (#230)
<!--
  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:
-->
## Description

enhanced CI/CD by not running the deploy 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.


## 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
- [x] 🗑️ Chore
2025-06-04 10:41:26 +03:00
Faris Armoush 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
Faris Armoush 6b3eca23af Update pull_request_template.md 2025-05-28 16:46:24 +03:00
Faris Armoush 4f4f11c330 Merge branch 'main' of https://github.com/SyncrowIOT/web 2025-05-28 14:26:36 +03:00
Faris Armoush 8a25fa798c Created pull_request_template.md. 2025-05-28 14:26:33 +03:00
Rafeek Alkhoudare 584845ffdc fix horizontal scroll bar 2025-05-22 04:52:23 -05:00
Faris Armoush 6612e91430 Merge pull request #177 from SyncrowIOT/merge_sprint_19_bugfixes
merged DEV into staging.
2025-05-08 14:32:54 +03:00
Faris Armoush 56c613fb0c Disabled Syncrow Analytics feature for release purposes. 2025-05-08 14:32:08 +03:00
Faris Armoush 8d2d9dd0bb Merge branch 'dev' of https://github.com/SyncrowIOT/web 2025-04-29 10:39:54 +03:00
Abdullah cfc68f1568 Merge pull request #116 from SyncrowIOT/dev
fix real time listenToChanges
2025-03-12 21:26:45 +03:00
Abdullah 02e08ad92f Merge pull request #115 from SyncrowIOT/dev
Dev
2025-03-12 14:21:13 +03:00
Abdullah Alassaf d7899a24f5 Merged with dev 2025-02-20 13:03:38 +03:00
hannathkadher 800c0ba47f Merge pull request #101 from SyncrowIOT/bugfix/fix-endpoint
Main
2025-02-20 13:37:28 +04:00
hannathkadher fe4e775902 fixed endpoint 2025-02-20 13:35:59 +04:00
hannathkadher 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
hannathkadher 4a8b8a32ba added back border of tag list 2025-02-20 11:49:35 +04:00
hannathkadher 2abce77eb5 Merge pull request #97 from SyncrowIOT/feat/fix-cursor-issue-in-main
Fixed cursor issue
2025-02-20 11:35:35 +04:00
hannathkadher 7efd1c3c87 Fixed cursor issue 2025-02-20 11:34:24 +04:00
hannathkadher 7a0d9aefb7 Merge pull request #95 from SyncrowIOT/bugfix/change-endpoint-prod
fix endpoints
2025-02-19 18:01:11 +04:00
hannathkadher 21cc25cfc4 fix endpoints 2025-02-19 17:58:53 +04:00
Abdullah Alassaf e2ec4bbf31 Pulled main changes 2025-02-18 16:27:09 +03:00
Abdullah Alassaf 51b46ae197 Merged with dev 2025-02-18 16:25:33 +03:00
mohammad 36ee22603a fixes CommunityId and spaceUuid 2025-02-10 12:44:35 +03:00
mohammad b0abd42b0c projectId 2025-02-06 11:28:40 +03:00
mohammad ba4da78846 Merge branch 'dev' 2025-02-06 11:20:34 +03:00
Abdullah dc20d69f20 Merge pull request #88 from SyncrowIOT/dev
Dev
2025-02-06 01:03:32 +03:00
Abdullah Alassaf cf6ec231dc Merged with dev 2025-02-06 00:57:29 +03:00
Abdullah Alassaf d0530f7fc3 Added staging space and community IDs 2024-12-04 09:57:29 +03:00
197 changed files with 5896 additions and 1813 deletions
+10
View File
@@ -0,0 +1,10 @@
version: 2
updates:
- package-ecosystem: "github-actions"
directory: "/"
schedule:
interval: "daily"
- package-ecosystem: "pub"
directory: "/"
schedule:
interval: "daily"
Binary file not shown.

After

Width:  |  Height:  |  Size: 290 KiB

@@ -0,0 +1,10 @@
import 'package:flutter/material.dart';
class AppLoadingIndicator extends StatelessWidget {
const AppLoadingIndicator({super.key});
@override
Widget build(BuildContext context) {
return const Center(child: CircularProgressIndicator());
}
}
+7 -7
View File
@@ -11,7 +11,6 @@ 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';
@@ -21,8 +20,10 @@ import 'package:syncrow_web/utils/theme/theme.dart';
Future<void> main() async {
try {
const environment =
String.fromEnvironment('FLAVOR', defaultValue: 'production');
const environment = String.fromEnvironment(
'FLAVOR',
defaultValue: 'production',
);
await dotenv.load(fileName: '.env.$environment');
WidgetsFlutterBinding.ensureInitialized();
await Firebase.initializeApp(
@@ -40,7 +41,7 @@ class MyApp extends StatelessWidget {
initialLocation: RoutesConst.auth,
routes: AppRoutes.getRoutes(),
redirect: (context, state) async {
String checkToken = await AuthBloc.getTokenAndValidate();
final checkToken = await AuthBloc.getTokenAndValidate();
final loggedIn = checkToken == 'Success';
final goingToLogin = state.uri.toString() == RoutesConst.auth;
@@ -58,8 +59,7 @@ class MyApp extends StatelessWidget {
BlocProvider<CreateRoutineBloc>(
create: (context) => CreateRoutineBloc(),
),
BlocProvider(
create: (context) => HomeBloc()..add(const FetchUserInfo())),
BlocProvider(create: (context) => HomeBloc()..add(FetchUserInfo())),
BlocProvider<VisitorPasswordBloc>(
create: (context) => VisitorPasswordBloc(),
),
@@ -67,7 +67,7 @@ class MyApp extends StatelessWidget {
create: (context) => RoutineBloc(),
),
BlocProvider<SpaceTreeBloc>(
create: (context) => SpaceTreeBloc()..add(InitialEvent()),
create: (context) => SpaceTreeBloc(),
),
],
child: MaterialApp.router(
+7 -5
View File
@@ -11,7 +11,6 @@ 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';
@@ -21,7 +20,10 @@ 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(
@@ -39,7 +41,7 @@ class MyApp extends StatelessWidget {
initialLocation: RoutesConst.auth,
routes: AppRoutes.getRoutes(),
redirect: (context, state) async {
String checkToken = await AuthBloc.getTokenAndValidate();
final checkToken = await AuthBloc.getTokenAndValidate();
final loggedIn = checkToken == 'Success';
final goingToLogin = state.uri.toString() == RoutesConst.auth;
@@ -57,7 +59,7 @@ class MyApp extends StatelessWidget {
BlocProvider<CreateRoutineBloc>(
create: (context) => CreateRoutineBloc(),
),
BlocProvider(create: (context) => HomeBloc()..add(const FetchUserInfo())),
BlocProvider(create: (context) => HomeBloc()..add(FetchUserInfo())),
BlocProvider<VisitorPasswordBloc>(
create: (context) => VisitorPasswordBloc(),
),
@@ -65,7 +67,7 @@ class MyApp extends StatelessWidget {
create: (context) => RoutineBloc(),
),
BlocProvider<SpaceTreeBloc>(
create: (context) => SpaceTreeBloc()..add(InitialEvent()),
create: (context) => SpaceTreeBloc(),
),
],
child: MaterialApp.router(
+3 -4
View File
@@ -11,7 +11,6 @@ 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';
@@ -39,7 +38,7 @@ class MyApp extends StatelessWidget {
initialLocation: RoutesConst.auth,
routes: AppRoutes.getRoutes(),
redirect: (context, state) async {
String checkToken = await AuthBloc.getTokenAndValidate();
final checkToken = await AuthBloc.getTokenAndValidate();
final loggedIn = checkToken == 'Success';
final goingToLogin = state.uri.toString() == RoutesConst.auth;
@@ -57,7 +56,7 @@ class MyApp extends StatelessWidget {
BlocProvider<CreateRoutineBloc>(
create: (context) => CreateRoutineBloc(),
),
BlocProvider(create: (context) => HomeBloc()..add(const FetchUserInfo())),
BlocProvider(create: (context) => HomeBloc()..add(FetchUserInfo())),
BlocProvider<VisitorPasswordBloc>(
create: (context) => VisitorPasswordBloc(),
),
@@ -65,7 +64,7 @@ class MyApp extends StatelessWidget {
create: (context) => RoutineBloc(),
),
BlocProvider<SpaceTreeBloc>(
create: (context) => SpaceTreeBloc()..add(InitialEvent()),
create: (context) => SpaceTreeBloc(),
),
],
child: MaterialApp.router(
@@ -15,7 +15,9 @@ class AirQualityDataModel extends Equatable {
return AirQualityDataModel(
date: DateTime.parse(json['date'] as String),
data: (json['data'] as List<dynamic>)
.map((e) => AirQualityPercentageData.fromJson(e as Map<String, dynamic>))
.map(
(e) => AirQualityPercentageData.fromJson(e as Map<String, dynamic>),
)
.toList(),
);
}
@@ -23,9 +25,9 @@ class AirQualityDataModel extends Equatable {
static final Map<String, Color> metricColors = {
'good': ColorsManager.goodGreen.withValues(alpha: 0.7),
'moderate': ColorsManager.moderateYellow.withValues(alpha: 0.7),
'poor': ColorsManager.poorOrange.withValues(alpha: 0.7),
'unhealthy_sensitive': ColorsManager.poorOrange.withValues(alpha: 0.7),
'unhealthy': ColorsManager.unhealthyRed.withValues(alpha: 0.7),
'severe': ColorsManager.severePink.withValues(alpha: 0.7),
'very_unhealthy': ColorsManager.severePink.withValues(alpha: 0.7),
'hazardous': ColorsManager.hazardousPurple.withValues(alpha: 0.7),
};
@@ -36,22 +38,19 @@ class AirQualityDataModel extends Equatable {
class AirQualityPercentageData extends Equatable {
const AirQualityPercentageData({
required this.type,
required this.name,
required this.percentage,
});
final String type;
final String name;
final double percentage;
factory AirQualityPercentageData.fromJson(Map<String, dynamic> json) {
return AirQualityPercentageData(
type: json['type'] as String? ?? '',
name: json['name'] as String? ?? '',
type: json['type'] as String? ?? '',
percentage: (json['percentage'] as num?)?.toDouble() ?? 0,
);
}
@override
List<Object?> get props => [type, name, percentage];
List<Object?> get props => [type, percentage];
}
@@ -25,8 +25,8 @@ class AnalyticsDevice {
factory AnalyticsDevice.fromJson(Map<String, dynamic> json) {
return AnalyticsDevice(
uuid: json['uuid'] as String,
name: json['name'] as String,
uuid: json['uuid'] as String? ?? '',
name: json['name'] as String? ?? '',
createdAt: json['createdAt'] != null
? DateTime.parse(json['createdAt'] as String)
: null,
@@ -39,8 +39,8 @@ class AnalyticsDevice {
? ProductDevice.fromJson(json['productDevice'] as Map<String, dynamic>)
: null,
spaceUuid: json['spaceUuid'] as String?,
latitude: json['lat'] != null ? double.parse(json['lat'] as String) : null,
longitude: json['lon'] != null ? double.parse(json['lon'] as String) : null,
latitude: json['lat'] != null ? double.parse(json['lat'] as String? ?? '0.0') : null,
longitude: json['lon'] != null ? double.parse(json['lon'] as String? ?? '0.0') : null,
);
}
}
@@ -14,12 +14,21 @@ class OccupancyHeatMapModel extends Equatable {
});
factory OccupancyHeatMapModel.fromJson(Map<String, dynamic> json) {
final eventDate = json['event_date'] as String?;
final year = eventDate?.split('-')[0];
final month = eventDate?.split('-')[1];
final day = eventDate?.split('-')[2];
return OccupancyHeatMapModel(
uuid: json['uuid'] as String? ?? '',
eventDate: DateTime.parse(
json['event_date'] as String? ?? '${DateTime.now()}',
),
countTotalPresenceDetected: json['count_total_presence_detected'] as int? ?? 0,
eventDate: DateTime(
int.parse(year ?? '2025'),
int.parse(month ?? '1'),
int.parse(day ?? '1'),
).toUtc(),
countTotalPresenceDetected: num.parse(
json['count_total_presence_detected']?.toString() ?? '0',
).toInt(),
);
}
+3 -3
View File
@@ -38,9 +38,9 @@ class RangeOfAqiValue extends Equatable {
factory RangeOfAqiValue.fromJson(Map<String, dynamic> json) {
return RangeOfAqiValue(
type: json['type'] as String,
min: (json['min'] as num).toDouble(),
average: (json['average'] as num).toDouble(),
max: (json['max'] as num).toDouble(),
min: (json['min'] as num? ?? 0).toDouble(),
average: (json['average'] as num? ?? 0).toDouble(),
max: (json['max'] as num? ?? 0).toDouble(),
);
}
@@ -33,7 +33,6 @@ class AirQualityDistributionBloc
state.copyWith(
status: AirQualityDistributionStatus.success,
chartData: result,
filteredChartData: _arrangeChartDataByType(result, state.selectedAqiType),
),
);
} catch (e) {
@@ -58,24 +57,6 @@ class AirQualityDistributionBloc
UpdateAqiTypeEvent event,
Emitter<AirQualityDistributionState> emit,
) {
emit(
state.copyWith(
selectedAqiType: event.aqiType,
filteredChartData: _arrangeChartDataByType(state.chartData, event.aqiType),
),
);
}
List<AirQualityDataModel> _arrangeChartDataByType(
List<AirQualityDataModel> data,
AqiType aqiType,
) {
final filteredData = data.map(
(data) => AirQualityDataModel(
date: data.date,
data: data.data.where((value) => value.type == aqiType.code).toList(),
),
);
return filteredData.toList();
emit(state.copyWith(selectedAqiType: event.aqiType));
}
}
@@ -11,28 +11,24 @@ class AirQualityDistributionState extends Equatable {
const AirQualityDistributionState({
this.status = AirQualityDistributionStatus.initial,
this.chartData = const [],
this.filteredChartData = const [],
this.errorMessage,
this.selectedAqiType = AqiType.aqi,
});
final AirQualityDistributionStatus status;
final List<AirQualityDataModel> chartData;
final List<AirQualityDataModel> filteredChartData;
final String? errorMessage;
final AqiType selectedAqiType;
AirQualityDistributionState copyWith({
AirQualityDistributionStatus? status,
List<AirQualityDataModel>? chartData,
List<AirQualityDataModel>? filteredChartData,
String? errorMessage,
AqiType? selectedAqiType,
}) {
return AirQualityDistributionState(
status: status ?? this.status,
chartData: chartData ?? this.chartData,
filteredChartData: filteredChartData ?? this.filteredChartData,
errorMessage: errorMessage ?? this.errorMessage,
selectedAqiType: selectedAqiType ?? this.selectedAqiType,
);
@@ -3,7 +3,7 @@ import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:syncrow_web/pages/analytics/modules/air_quality/blocs/air_quality_distribution/air_quality_distribution_bloc.dart';
import 'package:syncrow_web/pages/analytics/modules/air_quality/blocs/device_location/device_location_bloc.dart';
import 'package:syncrow_web/pages/analytics/modules/air_quality/blocs/range_of_aqi/range_of_aqi_bloc.dart';
import 'package:syncrow_web/pages/analytics/modules/analytics/blocs/analytics_date_picker_bloc/analytics_date_picker_bloc.dart';
import 'package:syncrow_web/pages/analytics/modules/air_quality/widgets/aqi_type_dropdown.dart';
import 'package:syncrow_web/pages/analytics/modules/analytics/blocs/analytics_devices/analytics_devices_bloc.dart';
import 'package:syncrow_web/pages/analytics/modules/energy_management/blocs/realtime_device_changes/realtime_device_changes_bloc.dart';
import 'package:syncrow_web/pages/analytics/params/get_air_quality_distribution_param.dart';
@@ -21,12 +21,14 @@ abstract final class FetchAirQualityDataHelper {
required String spaceUuid,
bool shouldFetchAnalyticsDevices = true,
}) {
final date = context.read<AnalyticsDatePickerBloc>().state.monthlyDate;
loadAnalyticsDevices(
context,
communityUuid: communityUuid,
spaceUuid: spaceUuid,
);
final aqiType = context.read<AirQualityDistributionBloc>().state.selectedAqiType;
if (shouldFetchAnalyticsDevices) {
loadAnalyticsDevices(
context,
communityUuid: communityUuid,
spaceUuid: spaceUuid,
);
}
loadRangeOfAqi(
context,
spaceUuid: spaceUuid,
@@ -36,6 +38,7 @@ abstract final class FetchAirQualityDataHelper {
context,
spaceUuid: spaceUuid,
date: date,
aqiType: aqiType,
);
}
@@ -104,10 +107,15 @@ abstract final class FetchAirQualityDataHelper {
BuildContext context, {
required String spaceUuid,
required DateTime date,
required AqiType aqiType,
}) {
context.read<AirQualityDistributionBloc>().add(
LoadAirQualityDistribution(
GetAirQualityDistributionParam(spaceUuid: spaceUuid, date: date),
GetAirQualityDistributionParam(
spaceUuid: spaceUuid,
date: date,
aqiType: aqiType,
),
),
);
}
@@ -18,11 +18,16 @@ abstract final class RangeOfAqiChartsHelper {
(ColorsManager.hazardousPurple, 'Hazardous'),
];
static FlTitlesData titlesData(BuildContext context, List<RangeOfAqi> data) {
static FlTitlesData titlesData(
BuildContext context,
List<RangeOfAqi> data, {
double leftSideInterval = 50,
}) {
final titlesData = EnergyManagementChartsHelper.titlesData(context);
return titlesData.copyWith(
bottomTitles: titlesData.bottomTitles.copyWith(
sideTitles: titlesData.bottomTitles.sideTitles.copyWith(
reservedSize: 36,
getTitlesWidget: (value, meta) => Padding(
padding: const EdgeInsetsDirectional.only(top: 20.0),
child: Text(
@@ -38,10 +43,11 @@ abstract final class RangeOfAqiChartsHelper {
leftTitles: titlesData.leftTitles.copyWith(
sideTitles: titlesData.leftTitles.sideTitles.copyWith(
reservedSize: 70,
interval: 50,
interval: leftSideInterval,
maxIncluded: false,
minIncluded: true,
getTitlesWidget: (value, meta) {
final text = value >= 300 ? '301+' : value.toInt().toString();
final text = value.toInt().toString();
return Padding(
padding: const EdgeInsetsDirectional.only(end: 12),
child: FittedBox(
@@ -1,6 +1,7 @@
import 'package:flutter/material.dart';
import 'package:syncrow_web/pages/analytics/modules/air_quality/widgets/air_quality_end_side_widget.dart';
import 'package:syncrow_web/pages/analytics/modules/air_quality/widgets/aqi_distribution_chart_box.dart';
import 'package:syncrow_web/pages/analytics/modules/air_quality/widgets/aqi_legend.dart';
import 'package:syncrow_web/pages/analytics/modules/air_quality/widgets/range_of_aqi_chart_box.dart';
class AirQualityView extends StatelessWidget {
@@ -20,6 +21,10 @@ class AirQualityView extends StatelessWidget {
child: Column(
spacing: 32,
children: [
SizedBox(
height: height * 0.1,
child: const AqiLegend(),
),
SizedBox(
height: height * 1.2,
child: const AirQualityEndSideWidget(),
@@ -40,7 +45,7 @@ class AirQualityView extends StatelessWidget {
return SingleChildScrollView(
child: Container(
padding: _padding,
height: height * 1.1,
height: height * 1.2,
child: const Column(
children: [
Expanded(
@@ -52,8 +57,9 @@ class AirQualityView extends StatelessWidget {
child: Column(
spacing: 20,
children: [
Expanded(child: RangeOfAqiChartBox()),
Expanded(child: AqiDistributionChartBox()),
Expanded(flex: 2, child: AqiLegend()),
Expanded(flex: 12, child: RangeOfAqiChartBox()),
Expanded(flex: 12, child: AqiDistributionChartBox()),
],
),
),
@@ -65,7 +65,7 @@ class AqiDeviceInfo extends StatelessWidget {
);
final tvocValue = _getValueForStatus(
status,
'tvoc_value',
'voc_value',
formatter: (value) => (value / 100).toStringAsFixed(2),
);
@@ -3,6 +3,7 @@ import 'package:flutter/material.dart';
import 'package:intl/intl.dart';
import 'package:syncrow_web/pages/analytics/models/air_quality_data_model.dart';
import 'package:syncrow_web/pages/analytics/modules/energy_management/helpers/energy_management_charts_helper.dart';
import 'package:syncrow_web/pages/analytics/widgets/charts_x_axis_title.dart';
import 'package:syncrow_web/utils/color_manager.dart';
import 'package:syncrow_web/utils/extension/build_context_x.dart';
@@ -16,11 +17,6 @@ class AqiDistributionChart extends StatelessWidget {
@override
Widget build(BuildContext context) {
final sortedData = List<AirQualityDataModel>.from(chartData)
..sort(
(a, b) => a.date.compareTo(b.date),
);
return BarChart(
BarChartData(
maxY: 100.1,
@@ -30,29 +26,30 @@ class AqiDistributionChart extends StatelessWidget {
borderData: EnergyManagementChartsHelper.borderData(),
barTouchData: _barTouchData(context),
titlesData: _titlesData(context),
barGroups: _buildBarGroups(sortedData),
barGroups: _buildBarGroups(),
),
duration: Duration.zero,
);
}
List<BarChartGroupData> _buildBarGroups(List<AirQualityDataModel> sortedData) {
return List.generate(sortedData.length, (index) {
final data = sortedData[index];
List<BarChartGroupData> _buildBarGroups() {
final groups = <BarChartGroupData>[];
for (var i = 0; i < chartData.length; i++) {
final data = chartData[i];
final isAllZero = data.data.every((d) => d.percentage == 0);
if (isAllZero) {
continue;
}
final stackItems = <BarChartRodData>[];
double currentY = 0;
bool isFirstElement = true;
var isFirstElement = true;
// Sort data by type to ensure consistent order
final sortedPercentageData = List<AirQualityPercentageData>.from(data.data)
..sort((a, b) => a.type.compareTo(b.type));
for (final percentageData in sortedPercentageData) {
for (final percentageData in data.data) {
stackItems.add(
BarChartRodData(
fromY: currentY,
toY: currentY + percentageData.percentage ,
color: AirQualityDataModel.metricColors[percentageData.name]!,
toY: currentY + percentageData.percentage,
color: AirQualityDataModel.metricColors[percentageData.type],
borderRadius: isFirstElement
? const BorderRadius.only(
topLeft: Radius.circular(22),
@@ -65,13 +62,15 @@ class AqiDistributionChart extends StatelessWidget {
currentY += percentageData.percentage + _rodStackItemsSpacing;
isFirstElement = false;
}
return BarChartGroupData(
x: index,
barRods: stackItems,
groupVertically: true,
groups.add(
BarChartGroupData(
x: i,
barRods: stackItems,
groupVertically: true,
),
);
});
}
return groups;
}
BarTouchData _barTouchData(BuildContext context) {
@@ -82,25 +81,27 @@ class AqiDistributionChart extends StatelessWidget {
color: ColorsManager.semiTransparentBlack,
),
tooltipRoundedRadius: 16,
maxContentWidth: 500,
tooltipPadding: const EdgeInsets.all(8),
getTooltipItem: (group, groupIndex, rod, rodIndex) {
final data = chartData[group.x.toInt()];
final data = chartData[group.x];
final List<TextSpan> children = [];
final children = <TextSpan>[];
final textStyle = context.textTheme.bodySmall?.copyWith(
color: ColorsManager.blackColor,
fontSize: 12,
fontSize: 11,
);
// Sort data by type to ensure consistent order
final sortedPercentageData = List<AirQualityPercentageData>.from(data.data)
..sort((a, b) => a.type.compareTo(b.type));
for (final percentageData in sortedPercentageData) {
for (final percentageData in data.data) {
if (percentageData.percentage == 0) {
continue;
}
final percentage = percentageData.percentage.toStringAsFixed(1);
final type = percentageData.type[0].toUpperCase() +
percentageData.type.substring(1).replaceAll('_', ' ');
children.add(TextSpan(
text:
'\n${percentageData.type.toUpperCase()}: ${percentageData.percentage.toStringAsFixed(1)}%',
text: '\n$type: $percentage%',
style: textStyle,
));
}
@@ -109,9 +110,10 @@ class AqiDistributionChart extends StatelessWidget {
DateFormat('dd/MM/yyyy').format(data.date),
context.textTheme.bodyMedium!.copyWith(
color: ColorsManager.blackColor,
fontSize: 16,
fontSize: 12,
fontWeight: FontWeight.w600,
),
textAlign: TextAlign.start,
children: children,
);
},
@@ -128,7 +130,6 @@ class AqiDistributionChart extends StatelessWidget {
final leftTitles = titlesData.leftTitles.copyWith(
sideTitles: titlesData.leftTitles.sideTitles.copyWith(
reservedSize: 70,
interval: 20,
maxIncluded: false,
minIncluded: true,
getTitlesWidget: (value, meta) => Padding(
@@ -149,8 +150,9 @@ class AqiDistributionChart extends StatelessWidget {
);
final bottomTitles = AxisTitles(
axisNameWidget: const ChartsXAxisTitle(),
sideTitles: SideTitles(
showTitles: true,
showTitles: chartData.isNotEmpty,
getTitlesWidget: (value, _) => FittedBox(
alignment: AlignmentDirectional.bottomCenter,
fit: BoxFit.scaleDown,
@@ -158,7 +160,7 @@ class AqiDistributionChart extends StatelessWidget {
chartData[value.toInt()].date.day.toString(),
style: context.textTheme.bodySmall?.copyWith(
color: ColorsManager.lightGreyColor,
fontSize: 8,
fontSize: 12,
),
),
),
@@ -33,7 +33,7 @@ class AqiDistributionChartBox extends StatelessWidget {
const Divider(),
const SizedBox(height: 20),
Expanded(
child: AqiDistributionChart(chartData: state.filteredChartData),
child: AqiDistributionChart(chartData: state.chartData),
),
],
),
@@ -2,8 +2,11 @@ import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:syncrow_web/pages/analytics/modules/air_quality/blocs/air_quality_distribution/air_quality_distribution_bloc.dart';
import 'package:syncrow_web/pages/analytics/modules/air_quality/widgets/aqi_type_dropdown.dart';
import 'package:syncrow_web/pages/analytics/modules/analytics/blocs/analytics_date_picker_bloc/analytics_date_picker_bloc.dart';
import 'package:syncrow_web/pages/analytics/modules/energy_management/widgets/chart_title.dart';
import 'package:syncrow_web/pages/analytics/params/get_air_quality_distribution_param.dart';
import 'package:syncrow_web/pages/analytics/widgets/charts_loading_widget.dart';
import 'package:syncrow_web/pages/space_tree/bloc/space_tree_bloc.dart';
class AqiDistributionChartTitle extends StatelessWidget {
const AqiDistributionChartTitle({required this.isLoading, super.key});
@@ -16,7 +19,7 @@ class AqiDistributionChartTitle extends StatelessWidget {
children: [
ChartsLoadingWidget(isLoading: isLoading),
const Expanded(
flex: 3,
flex: 4,
child: FittedBox(
fit: BoxFit.scaleDown,
alignment: AlignmentDirectional.centerStart,
@@ -25,20 +28,44 @@ class AqiDistributionChartTitle extends StatelessWidget {
),
),
),
FittedBox(
alignment: AlignmentDirectional.centerEnd,
fit: BoxFit.scaleDown,
child: AqiTypeDropdown(
onChanged: (value) {
if (value != null) {
context
.read<AirQualityDistributionBloc>()
.add(UpdateAqiTypeEvent(value));
}
},
Expanded(
flex: 2,
child: FittedBox(
alignment: AlignmentDirectional.centerEnd,
fit: BoxFit.scaleDown,
child: AqiTypeDropdown(
onChanged: (value) {
if (value != null) {
final bloc = context.read<AirQualityDistributionBloc>();
try {
final param = _makeLoadAqiDistributionParam(context, value);
bloc.add(LoadAirQualityDistribution(param));
} catch (_) {
return;
} finally {
bloc.add(UpdateAqiTypeEvent(value));
}
}
},
),
),
),
],
);
}
GetAirQualityDistributionParam _makeLoadAqiDistributionParam(
BuildContext context,
AqiType aqiType,
) {
final date = context.read<AnalyticsDatePickerBloc>().state.monthlyDate;
final spaceUuid =
context.read<SpaceTreeBloc>().state.selectedSpaces.firstOrNull ?? '';
if (spaceUuid.isEmpty) throw Exception('Space UUID is empty');
return GetAirQualityDistributionParam(
date: date,
spaceUuid: spaceUuid,
aqiType: aqiType,
);
}
}
@@ -0,0 +1,38 @@
import 'package:flutter/material.dart';
import 'package:syncrow_web/pages/analytics/modules/air_quality/helpers/range_of_aqi_charts_helper.dart';
import 'package:syncrow_web/pages/analytics/modules/analytics/widgets/chart_informative_cell.dart';
import 'package:syncrow_web/utils/style.dart';
class AqiLegend extends StatelessWidget {
const AqiLegend({super.key});
@override
Widget build(BuildContext context) {
return Container(
padding: const EdgeInsetsDirectional.all(20),
decoration: subSectionContainerDecoration.copyWith(
borderRadius: BorderRadius.circular(20),
),
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
spacing: 16,
children: RangeOfAqiChartsHelper.gradientData.map((e) {
return Flexible(
flex: 4,
child: FittedBox(
fit: BoxFit.fill,
child: ChartInformativeCell(
color: e.$1,
title: FittedBox(
fit: BoxFit.fill,
child: Text(e.$2),
),
height: null,
),
),
);
}).toList(),
),
);
}
}
@@ -47,36 +47,37 @@ class AqiLocationInfoCell extends StatelessWidget {
),
),
Align(
alignment: AlignmentDirectional.bottomEnd,
child: Padding(
padding: const EdgeInsetsDirectional.all(10),
child: SizedBox(
height: 40,
width: 120,
child: FittedBox(
fit: BoxFit.scaleDown,
alignment: AlignmentDirectional.bottomEnd,
child: Text(
value,
style: context.textTheme.bodySmall?.copyWith(
color: ColorsManager.vividBlue.withValues(alpha: 0.7),
fontWeight: FontWeight.w700,
fontSize: 24,
alignment: AlignmentDirectional.bottomCenter,
child: Row(
crossAxisAlignment: CrossAxisAlignment.end,
children: [
Expanded(
child: SvgPicture.asset(
svgPath,
fit: BoxFit.scaleDown,
alignment: AlignmentDirectional.bottomStart,
),
),
Expanded(
child: FittedBox(
fit: BoxFit.scaleDown,
alignment: AlignmentDirectional.bottomEnd,
child: Padding(
padding: const EdgeInsetsDirectional.all(10),
child: Text(
value,
style: context.textTheme.bodySmall?.copyWith(
color: ColorsManager.vividBlue.withValues(
alpha: 0.7,
),
fontWeight: FontWeight.w700,
fontSize: 24,
),
),
),
),
),
),
),
),
Align(
alignment: AlignmentDirectional.bottomStart,
child: SizedBox.square(
dimension: MediaQuery.sizeOf(context).width * 0.45,
child: FittedBox(
fit: BoxFit.scaleDown,
alignment: AlignmentDirectional.bottomStart,
child: SvgPicture.asset(svgPath),
),
],
),
),
],
@@ -6,8 +6,8 @@ enum AqiType {
aqi('AQI', '', 'aqi'),
pm25('PM2.5', 'µg/m³', 'pm25'),
pm10('PM10', 'µg/m³', 'pm10'),
hcho('HCHO', 'mg/m³', 'hcho'),
tvoc('TVOC', 'µg/m³', 'tvoc'),
hcho('HCHO', 'mg/m³', 'ch2o'),
tvoc('TVOC', 'mg/m³', 'voc'),
co2('CO2', 'ppm', 'co2');
const AqiType(this.value, this.unit, this.code);
@@ -2,15 +2,18 @@ import 'package:fl_chart/fl_chart.dart';
import 'package:flutter/material.dart';
import 'package:syncrow_web/pages/analytics/models/range_of_aqi.dart';
import 'package:syncrow_web/pages/analytics/modules/air_quality/helpers/range_of_aqi_charts_helper.dart';
import 'package:syncrow_web/pages/analytics/modules/air_quality/widgets/aqi_type_dropdown.dart';
import 'package:syncrow_web/pages/analytics/modules/energy_management/helpers/energy_management_charts_helper.dart';
import 'package:syncrow_web/utils/color_manager.dart';
class RangeOfAqiChart extends StatelessWidget {
final List<RangeOfAqi> chartData;
final AqiType selectedAqiType;
const RangeOfAqiChart({
super.key,
required this.chartData,
required this.selectedAqiType,
});
List<(List<double> values, Color color, Color? dotColor)> get _lines {
@@ -45,15 +48,34 @@ class RangeOfAqiChart extends StatelessWidget {
];
}
(double maxY, double interval) get _maxYForAqiType {
const aqiMaxValues = <AqiType, (double maxY, double interval)>{
AqiType.aqi: (401, 100),
AqiType.pm25: (351, 50),
AqiType.pm10: (501, 100),
AqiType.hcho: (301, 50),
AqiType.tvoc: (501, 50),
AqiType.co2: (1251, 250),
};
return aqiMaxValues[selectedAqiType]!;
}
@override
Widget build(BuildContext context) {
return LineChart(
LineChartData(
minY: 0,
maxY: 301,
maxY: _maxYForAqiType.$1,
clipData: const FlClipData.vertical(),
gridData: EnergyManagementChartsHelper.gridData(horizontalInterval: 50),
titlesData: RangeOfAqiChartsHelper.titlesData(context, chartData),
gridData: EnergyManagementChartsHelper.gridData(
horizontalInterval: _maxYForAqiType.$2,
),
titlesData: RangeOfAqiChartsHelper.titlesData(
context,
chartData,
leftSideInterval: _maxYForAqiType.$2,
),
borderData: EnergyManagementChartsHelper.borderData(),
lineTouchData: RangeOfAqiChartsHelper.lineTouchData(chartData),
betweenBarsData: [
@@ -63,7 +85,7 @@ class RangeOfAqiChart extends StatelessWidget {
gradient: LinearGradient(
begin: Alignment.bottomCenter,
end: Alignment.topCenter,
stops: [0.0, 0.2, 0.4, 0.6, 0.8, 1.0],
stops: const [0.0, 0.2, 0.4, 0.6, 0.8, 1.0],
colors: RangeOfAqiChartsHelper.gradientData.map((e) {
final (color, _) = e;
return color.withValues(alpha: 0.6);
@@ -32,7 +32,12 @@ class RangeOfAqiChartBox extends StatelessWidget {
const SizedBox(height: 10),
const Divider(),
const SizedBox(height: 20),
Expanded(child: RangeOfAqiChart(chartData: state.filteredRangeOfAqi)),
Expanded(
child: RangeOfAqiChart(
chartData: state.filteredRangeOfAqi,
selectedAqiType: state.selectedAqiType,
),
),
],
),
);
@@ -16,7 +16,7 @@ import 'package:syncrow_web/pages/analytics/modules/energy_management/blocs/real
import 'package:syncrow_web/pages/analytics/modules/energy_management/blocs/total_energy_consumption/total_energy_consumption_bloc.dart';
import 'package:syncrow_web/pages/analytics/modules/occupancy/blocs/occupancy/occupancy_bloc.dart';
import 'package:syncrow_web/pages/analytics/modules/occupancy/blocs/occupancy_heat_map/occupancy_heat_map_bloc.dart';
import 'package:syncrow_web/pages/analytics/services/air_quality_distribution/fake_air_quality_distribution_service.dart';
import 'package:syncrow_web/pages/analytics/services/air_quality_distribution/remote_air_quality_distribution_service.dart';
import 'package:syncrow_web/pages/analytics/services/analytics_devices/analytics_devices_service_delagate.dart';
import 'package:syncrow_web/pages/analytics/services/analytics_devices/remote_energy_management_analytics_devices_service.dart';
import 'package:syncrow_web/pages/analytics/services/analytics_devices/remote_occupancy_analytics_devices_service.dart';
@@ -27,10 +27,12 @@ import 'package:syncrow_web/pages/analytics/services/energy_consumption_per_devi
import 'package:syncrow_web/pages/analytics/services/occupacy/remote_occupancy_service.dart';
import 'package:syncrow_web/pages/analytics/services/occupancy_heat_map/remote_occupancy_heat_map_service.dart';
import 'package:syncrow_web/pages/analytics/services/power_clamp_info/remote_power_clamp_info_service.dart';
import 'package:syncrow_web/pages/analytics/services/range_of_aqi/fake_range_of_aqi_service.dart';
import 'package:syncrow_web/pages/analytics/services/range_of_aqi/remote_range_of_aqi_service.dart';
import 'package:syncrow_web/pages/analytics/services/realtime_device_service/firebase_realtime_device_service.dart';
import 'package:syncrow_web/pages/analytics/services/total_energy_consumption/remote_total_energy_consumption_service.dart';
import 'package:syncrow_web/pages/device_managment/shared/navigate_home_grid_view.dart';
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/api/http_service.dart';
import 'package:syncrow_web/utils/theme/responsive_text_theme.dart';
import 'package:syncrow_web/web_layout/web_scaffold.dart';
@@ -104,12 +106,12 @@ class _AnalyticsPageState extends State<AnalyticsPage> {
),
BlocProvider(
create: (context) => RangeOfAqiBloc(
FakeRangeOfAqiService(),
RemoteRangeOfAqiService(_httpService),
),
),
BlocProvider(
create: (context) => AirQualityDistributionBloc(
FakeAirQualityDistributionService(),
RemoteAirQualityDistributionService(_httpService),
),
),
BlocProvider(
@@ -130,9 +132,19 @@ class _AnalyticsPageState extends State<AnalyticsPage> {
}
}
class AnalyticsPageForm extends StatelessWidget {
class AnalyticsPageForm extends StatefulWidget {
const AnalyticsPageForm({super.key});
@override
State<AnalyticsPageForm> createState() => _AnalyticsPageFormState();
}
class _AnalyticsPageFormState extends State<AnalyticsPageForm> {
@override
void initState() {
context.read<SpaceTreeBloc>().add(InitialEvent());
super.initState();
}
@override
Widget build(BuildContext context) {
return WebScaffold(
@@ -20,7 +20,7 @@ class AnalyticsDateFilterButton extends StatefulWidget {
final void Function(DateTime)? onDateSelected;
final DatePickerType datePickerType;
static final _color = ColorsManager.blackColor.withValues(alpha: 0.8);
static final Color _color = ColorsManager.blackColor.withValues(alpha: 0.8);
@override
State<AnalyticsDateFilterButton> createState() =>
@@ -60,23 +60,21 @@ class _AnalyticsDateFilterButtonState extends State<AnalyticsDateFilterButton> {
),
),
onPressed: () {
showDialog(
showDialog<void>(
context: context,
builder: (_) {
return switch (widget.datePickerType) {
DatePickerType.month => MonthPickerWidget(
selectedDate: widget.selectedDate,
onDateSelected: (value) {
widget.onDateSelected?.call(value);
},
),
DatePickerType.year => YearPickerWidget(
selectedDate: widget.selectedDate,
onDateSelected: (value) {
widget.onDateSelected?.call(value);
},
),
};
builder: (_) => switch (widget.datePickerType) {
DatePickerType.month => MonthPickerWidget(
selectedDate: widget.selectedDate,
onDateSelected: (value) {
widget.onDateSelected?.call(value);
},
),
DatePickerType.year => YearPickerWidget(
selectedDate: widget.selectedDate,
onDateSelected: (value) {
widget.onDateSelected?.call(value);
},
),
},
);
},
@@ -118,7 +118,7 @@ class AnalyticsPageTabsAndChildren extends StatelessWidget {
communityUuid: communities.firstOrNull ?? '',
spaceUuid: spaces.firstOrNull ?? '',
);
break;
return;
case AnalyticsPageTab.airQuality:
_onAirQualityDateChanged(
context,
@@ -126,8 +126,9 @@ class AnalyticsPageTabsAndChildren extends StatelessWidget {
communityUuid: communities.firstOrNull ?? '',
spaceUuid: spaces.firstOrNull ?? '',
);
return;
default:
break;
return;
}
}
}
@@ -157,6 +158,7 @@ class AnalyticsPageTabsAndChildren extends StatelessWidget {
required String communityUuid,
required String spaceUuid,
}) {
if (spaceUuid.isEmpty) return;
FetchAirQualityDataHelper.loadAirQualityData(
context,
date: date,
@@ -7,16 +7,18 @@ class ChartInformativeCell extends StatelessWidget {
required this.title,
required this.color,
this.hasBorder = false,
this.height,
});
final Widget title;
final Color color;
final bool hasBorder;
final double? height;
@override
Widget build(BuildContext context) {
return Container(
height: MediaQuery.sizeOf(context).height * 0.0385,
height: height ?? MediaQuery.sizeOf(context).height * 0.0385,
padding: const EdgeInsetsDirectional.symmetric(
vertical: 8,
horizontal: 12,
@@ -1,6 +1,7 @@
import 'package:fl_chart/fl_chart.dart';
import 'package:flutter/material.dart';
import 'package:syncrow_web/pages/analytics/helpers/format_number_to_kwh.dart';
import 'package:syncrow_web/pages/analytics/widgets/charts_x_axis_title.dart';
import 'package:syncrow_web/utils/color_manager.dart';
import 'package:syncrow_web/utils/extension/build_context_x.dart';
@@ -15,6 +16,7 @@ abstract final class EnergyManagementChartsHelper {
return FlTitlesData(
show: true,
bottomTitles: AxisTitles(
axisNameWidget: const ChartsXAxisTitle(),
drawBelowEverything: true,
sideTitles: SideTitles(
interval: 1,
@@ -38,7 +40,7 @@ abstract final class EnergyManagementChartsHelper {
sideTitles: SideTitles(
showTitles: true,
maxIncluded: false,
minIncluded: true,
minIncluded: false,
interval: leftTitlesInterval,
reservedSize: 110,
getTitlesWidget: (value, meta) => Padding(
@@ -62,17 +64,12 @@ abstract final class EnergyManagementChartsHelper {
);
}
static String getToolTipLabel(num month, double value) {
final monthLabel = month.toString();
final valueLabel = value.formatNumberToKwh;
final labels = [monthLabel, valueLabel];
return labels.where((element) => element.isNotEmpty).join(', ');
}
static String getToolTipLabel(double value) => value.formatNumberToKwh;
static List<LineTooltipItem?> getTooltipItems(List<LineBarSpot> touchedSpots) {
return touchedSpots.map((spot) {
return LineTooltipItem(
getToolTipLabel(spot.x, spot.y),
getToolTipLabel(spot.y),
const TextStyle(
color: ColorsManager.textPrimaryColor,
fontWeight: FontWeight.w600,
@@ -46,7 +46,7 @@ class AnalyticsEnergyManagementView extends StatelessWidget {
spacing: 32,
children: [
Expanded(
flex: 2,
flex: 7,
child: Column(
spacing: 20,
children: [
@@ -55,7 +55,7 @@ class AnalyticsEnergyManagementView extends StatelessWidget {
],
),
),
Expanded(child: PowerClampEnergyDataWidget()),
Expanded(flex: 4, child: PowerClampEnergyDataWidget()),
],
),
),
@@ -28,15 +28,29 @@ class AnalyticsDeviceDropdown extends StatelessWidget {
),
),
child: Visibility(
visible: state.devices.isNotEmpty,
replacement: _buildNoDevicesFound(context),
child: _buildDevicesDropdown(context, state),
visible: state.status != AnalyticsDevicesStatus.loading,
replacement: _buildLoadingIndicator(),
child: Visibility(
visible: state.devices.isNotEmpty,
replacement: _buildNoDevicesFound(context),
child: _buildDevicesDropdown(context, state),
),
),
);
},
);
}
Widget _buildLoadingIndicator() {
return const Center(
child: SizedBox(
width: 24,
height: 24,
child: CircularProgressIndicator(strokeWidth: 3),
),
);
}
static const _defaultPadding = EdgeInsetsDirectional.symmetric(
horizontal: 20,
vertical: 2,
@@ -37,7 +37,7 @@ class EnergyConsumptionPerDeviceChartBox extends StatelessWidget {
fit: BoxFit.scaleDown,
alignment: AlignmentDirectional.centerStart,
child: ChartTitle(
title: Text('Energy Consumption per Device'),
title: Text('Device energy consumed'),
),
),
),
@@ -14,14 +14,17 @@ class TotalEnergyConsumptionChart extends StatelessWidget {
return Expanded(
child: LineChart(
LineChartData(
maxY: chartData.isEmpty
? null
: chartData.map((e) => e.value).reduce((a, b) => a > b ? a : b) + 250,
clipData: const FlClipData.vertical(),
titlesData: EnergyManagementChartsHelper.titlesData(
context,
leftTitlesInterval: 250,
leftTitlesInterval: 500,
),
gridData: EnergyManagementChartsHelper.gridData().copyWith(
checkToShowHorizontalLine: (value) => true,
horizontalInterval: 250,
horizontalInterval: 500,
),
borderData: EnergyManagementChartsHelper.borderData(),
lineTouchData: EnergyManagementChartsHelper.lineTouchData(),
@@ -29,7 +32,6 @@ class TotalEnergyConsumptionChart extends StatelessWidget {
),
duration: Duration.zero,
curve: Curves.easeIn,
),
);
}
@@ -32,7 +32,7 @@ class TotalEnergyConsumptionChartBox extends StatelessWidget {
child: FittedBox(
alignment: AlignmentDirectional.centerStart,
fit: BoxFit.scaleDown,
child: ChartTitle(title: Text('Total Energy Consumption')),
child: ChartTitle(title: Text('Space energy consumed')),
),
),
const Spacer(flex: 4),
@@ -81,7 +81,7 @@ abstract final class FetchOccupancyDataHelper {
param: GetAnalyticsDevicesParam(
communityUuid: communityUuid,
spaceUuid: spaceUuid,
deviceTypes: ['WPS', 'CPS'],
deviceTypes: ['WPS', 'CPS', 'NCPS'],
requestType: AnalyticsDeviceRequestType.occupancy,
),
onSuccess: (device) {
@@ -20,7 +20,7 @@ class AnalyticsOccupancyView extends StatelessWidget {
child: Column(
spacing: 32,
children: [
SizedBox(height: height * 0.46, child: const OccupancyEndSideBar()),
SizedBox(height: height * 0.8, child: const OccupancyEndSideBar()),
SizedBox(height: height * 0.5, child: const OccupancyChartBox()),
SizedBox(height: height * 0.5, child: const OccupancyHeatMapBox()),
],
@@ -31,12 +31,12 @@ class AnalyticsOccupancyView extends StatelessWidget {
return SingleChildScrollView(
child: Container(
padding: _padding,
height: height * 0.9,
height: height * 1,
child: const Row(
spacing: 32,
children: [
Expanded(
flex: 5,
flex: 7,
child: Column(
spacing: 20,
children: [
@@ -45,7 +45,7 @@ class AnalyticsOccupancyView extends StatelessWidget {
],
),
),
Expanded(flex: 2, child: OccupancyEndSideBar()),
Expanded(flex: 4, child: OccupancyEndSideBar()),
],
),
),
@@ -39,7 +39,7 @@ class HeatMapTooltip extends StatelessWidget {
),
const Divider(height: 2, thickness: 1),
Text(
'$value Occupants',
'Occupancy detected: $value',
style: context.textTheme.bodySmall?.copyWith(
fontSize: 10,
fontWeight: FontWeight.w500,
@@ -52,7 +52,7 @@ class _InteractiveHeatMapState extends State<InteractiveHeatMap> {
color: Colors.transparent,
child: Transform.translate(
offset: Offset(-(widget.cellSize * 2.5), -50),
child: HeatMapTooltip(date: item.date, value: item.value),
child: HeatMapTooltip(date: item.date.toUtc(), value: item.value),
),
),
),
@@ -2,6 +2,7 @@ import 'package:fl_chart/fl_chart.dart';
import 'package:flutter/material.dart';
import 'package:syncrow_web/pages/analytics/models/occupacy.dart';
import 'package:syncrow_web/pages/analytics/modules/energy_management/helpers/energy_management_charts_helper.dart';
import 'package:syncrow_web/pages/analytics/widgets/charts_x_axis_title.dart';
import 'package:syncrow_web/utils/color_manager.dart';
import 'package:syncrow_web/utils/extension/build_context_x.dart';
@@ -88,8 +89,8 @@ class OccupancyChart extends StatelessWidget {
}) {
final data = chartData;
final occupancyValue = double.parse(data[group.x.toInt()].occupancy);
final percentage = '${(occupancyValue).toStringAsFixed(0)}%';
final occupancyValue = double.parse(data[group.x].occupancy);
final percentage = '${occupancyValue.toStringAsFixed(0)}%';
return BarTooltipItem(
percentage,
@@ -116,7 +117,7 @@ class OccupancyChart extends StatelessWidget {
alignment: AlignmentDirectional.centerStart,
fit: BoxFit.scaleDown,
child: Text(
'${(value).toStringAsFixed(0)}%',
'${value.toStringAsFixed(0)}%',
style: context.textTheme.bodySmall?.copyWith(
fontSize: 12,
color: ColorsManager.greyColor,
@@ -128,6 +129,7 @@ class OccupancyChart extends StatelessWidget {
);
final bottomTitles = AxisTitles(
axisNameWidget: const ChartsXAxisTitle(),
sideTitles: SideTitles(
showTitles: true,
getTitlesWidget: (value, _) => FittedBox(
@@ -23,38 +23,45 @@ class OccupancyEndSideBar extends StatelessWidget {
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const AnalyticsSidebarHeader(title: 'Presnce Sensor'),
SizedBox(
height: MediaQuery.sizeOf(context).height * 0.2,
child: PowerClampEnergyStatusWidget(
status: [
PowerClampEnergyStatus(
iconPath: Assets.presenceState,
title: 'Presence Status',
value: _valueFromCode(
'presence_state',
state.deviceStatusList,
const AnalyticsSidebarHeader(title: 'Presence Sensor'),
Expanded(
child: SizedBox(
child: PowerClampEnergyStatusWidget(
status: [
PowerClampEnergyStatus(
iconPath: Assets.presenceState,
title: 'Presence Status',
value: _valueFromCode(
'presence_state',
state.deviceStatusList,
),
unit: '',
),
unit: '',
),
PowerClampEnergyStatus(
iconPath: Assets.presenceTimeIcon,
title: 'Presence Time',
value:
'${_valueFromCode('none_body_time', state.deviceStatusList)} Min',
unit: '',
),
PowerClampEnergyStatus(
iconPath: Assets.currentDistanceIcon,
title: 'Detection Distance',
value:
'${_valueFromCode('space_move_val', state.deviceStatusList)} M',
unit: '',
),
],
PowerClampEnergyStatus(
iconPath: Assets.presenceTimeIcon,
title: 'Presence Time',
value:
'${_valueFromCode('none_body_time', state.deviceStatusList)} Min',
unit: '',
),
PowerClampEnergyStatus(
iconPath: Assets.currentDistanceIcon,
title: 'Detection Distance',
value:
'${_valueFromCode('space_move_val', state.deviceStatusList)} M',
unit: '',
),
],
),
),
),
const SizedBox(height: 20),
Expanded(
flex: 2,
child: FittedBox(
child: Image.asset(Assets.autocadOccupancyImage),
),
),
],
),
);
@@ -20,14 +20,14 @@ class OccupancyHeatMap extends StatelessWidget {
: 0;
DateTime _getStartingDate() {
final jan1 = DateTime(DateTime.now().year, 1, 1);
final jan1 = DateTime(DateTime.now().year, 1, 1).toUtc();
final startOfWeek = jan1.subtract(Duration(days: jan1.weekday - 1));
return startOfWeek;
}
List<OccupancyPaintItem> _generatePaintItems(DateTime startDate) {
return List.generate(_totalWeeks * 7, (index) {
final date = startDate.add(Duration(days: index));
final date = startDate.toUtc().add(Duration(days: index));
final value = heatMapData[date] ?? 0;
return OccupancyPaintItem(index: index, value: value, date: date);
});
@@ -34,8 +34,8 @@ class OccupancyHeatMapGradient extends StatelessWidget {
width: 1,
),
gradient: LinearGradient(
begin: AlignmentDirectional.centerEnd,
end: AlignmentDirectional.centerStart,
begin: AlignmentDirectional.centerStart,
end: AlignmentDirectional.centerEnd,
colors: _heatMapColors(),
),
),
@@ -28,11 +28,11 @@ class OccupancyPainter extends CustomPainter {
@override
void paint(Canvas canvas, Size size) {
final Paint fillPaint = Paint();
final Paint borderPaint = Paint()
final fillPaint = Paint();
final borderPaint = Paint()
..color = ColorsManager.grayBorder.withValues(alpha: 0.4)
..style = PaintingStyle.stroke;
final Paint hoveredBorderPaint = Paint()
final hoveredBorderPaint = Paint()
..color = Colors.black
..style = PaintingStyle.stroke
..strokeWidth = 1.5;
@@ -48,7 +48,6 @@ class OccupancyPainter extends CustomPainter {
final rect = Rect.fromLTWH(x, y, cellSize, cellSize);
canvas.drawRect(rect, fillPaint);
// Highlight the hovered item
if (hoveredItem != null && hoveredItem!.index == item.index) {
canvas.drawRect(rect, hoveredBorderPaint);
} else {
@@ -73,16 +72,16 @@ class OccupancyPainter extends CustomPainter {
}
void _drawDashedLine(Canvas canvas, Offset start, Offset end, Paint paint) {
const double dashWidth = 2.0;
const double dashSpace = 4.0;
final double totalLength = (end - start).distance;
final Offset direction = (end - start) / (end - start).distance;
const dashWidth = 2.0;
const dashSpace = 4.0;
final totalLength = (end - start).distance;
final direction = (end - start) / (end - start).distance;
double currentLength = 0.0;
var currentLength = 0.0;
while (currentLength < totalLength) {
final Offset dashStart = start + direction * currentLength;
final double nextLength = currentLength + dashWidth;
final Offset dashEnd =
final dashStart = start + direction * currentLength;
final nextLength = currentLength + dashWidth;
final dashEnd =
start + direction * (nextLength < totalLength ? nextLength : totalLength);
canvas.drawLine(dashStart, dashEnd, paint);
currentLength = nextLength + dashSpace;
@@ -91,8 +90,9 @@ class OccupancyPainter extends CustomPainter {
Color _getColor(int value) {
if (maxValue == 0) return ColorsManager.vividBlue.withValues(alpha: 0);
final opacity = value.clamp(0, maxValue) / maxValue;
return ColorsManager.vividBlue.withValues(alpha: opacity);
final clampedValue = 0.075 + (1 * value.clamp(0, maxValue) / maxValue);
final opacity = value == 0 ? 0 : clampedValue;
return ColorsManager.vividBlue.withValues(alpha: opacity.toDouble());
}
@override
@@ -1,9 +1,14 @@
import 'package:syncrow_web/pages/analytics/modules/air_quality/widgets/aqi_type_dropdown.dart';
class GetAirQualityDistributionParam {
final DateTime date;
final String spaceUuid;
final AqiType aqiType;
const GetAirQualityDistributionParam({
const GetAirQualityDistributionParam(
{
required this.date,
required this.spaceUuid,
required this.aqiType,
});
}
@@ -1,95 +0,0 @@
import 'dart:math';
import 'package:syncrow_web/pages/analytics/models/air_quality_data_model.dart';
import 'package:syncrow_web/pages/analytics/modules/air_quality/widgets/aqi_type_dropdown.dart';
import 'package:syncrow_web/pages/analytics/params/get_air_quality_distribution_param.dart';
import 'package:syncrow_web/pages/analytics/services/air_quality_distribution/air_quality_distribution_service.dart';
class FakeAirQualityDistributionService implements AirQualityDistributionService {
final _random = Random();
@override
Future<List<AirQualityDataModel>> getAirQualityDistribution(
GetAirQualityDistributionParam param,
) async {
return Future.delayed(
const Duration(milliseconds: 400),
() => List.generate(30, (index) {
final date = DateTime(2025, 5, 1).add(Duration(days: index));
final values = _generateRandomPercentages();
final nullMask = List.generate(6, (_) => _shouldBeNull());
if (nullMask.every((isNull) => isNull)) {
nullMask[_random.nextInt(6)] = false;
}
final nonNullValues = _redistributePercentages(values, nullMask);
return AirQualityDataModel(
date: date,
data: [
AirQualityPercentageData(
type: AqiType.aqi.code,
percentage: nonNullValues[0],
name: 'good',
),
AirQualityPercentageData(
name: 'moderate',
type: AqiType.co2.code,
percentage: nonNullValues[1],
),
AirQualityPercentageData(
name: 'poor',
percentage: nonNullValues[2],
type: AqiType.hcho.code,
),
AirQualityPercentageData(
name: 'unhealthy',
percentage: nonNullValues[3],
type: AqiType.pm10.code,
),
AirQualityPercentageData(
name: 'severe',
type: AqiType.pm25.code,
percentage: nonNullValues[4],
),
AirQualityPercentageData(
name: 'hazardous',
percentage: nonNullValues[5],
type: AqiType.co2.code,
),
],
);
}),
);
}
List<double> _redistributePercentages(
List<double> originalValues,
List<bool> nullMask,
) {
double nonNullSum = 0;
for (int i = 0; i < originalValues.length; i++) {
if (!nullMask[i]) {
nonNullSum += originalValues[i];
}
}
return List.generate(originalValues.length, (i) {
if (nullMask[i]) return 0;
return (originalValues[i] / nonNullSum * 100).roundToDouble();
});
}
bool _shouldBeNull() => _random.nextDouble() < 0.6;
List<double> _generateRandomPercentages() {
final values = List.generate(6, (_) => _random.nextDouble());
final sum = values.reduce((a, b) => a + b);
return values.map((value) => (value / sum * 100).roundToDouble()).toList();
}
}
@@ -3,7 +3,8 @@ import 'package:syncrow_web/pages/analytics/params/get_air_quality_distribution_
import 'package:syncrow_web/pages/analytics/services/air_quality_distribution/air_quality_distribution_service.dart';
import 'package:syncrow_web/services/api/http_service.dart';
class RemoteAirQualityDistributionService implements AirQualityDistributionService {
final class RemoteAirQualityDistributionService
implements AirQualityDistributionService {
RemoteAirQualityDistributionService(this._httpService);
final HTTPService _httpService;
@@ -14,10 +15,10 @@ class RemoteAirQualityDistributionService implements AirQualityDistributionServi
) async {
try {
final response = await _httpService.get(
path: 'endpoint',
path: '/aqi/distribution/space/${param.spaceUuid}',
queryParameters: {
'spaceUuid': param.spaceUuid,
'date': param.date.toIso8601String(),
'monthDate': _formatDate(param.date),
'pollutantType': param.aqiType.code,
},
expectedResponseModel: (data) {
final json = data as Map<String, dynamic>? ?? {};
@@ -33,4 +34,8 @@ class RemoteAirQualityDistributionService implements AirQualityDistributionServi
throw Exception('Failed to load energy consumption per phase: $e');
}
}
static String _formatDate(DateTime date) {
return '${date.year}-${date.month.toString().padLeft(2, '0')}';
}
}
@@ -26,15 +26,15 @@ class DeviceLocationDetailsServiceDecorator implements DeviceLocationService {
if (data != null) {
final addressData = data['address'] as Map<String, dynamic>;
return deviceLocationInfo.copyWith(
city: addressData['city'],
country: addressData['country_code'].toString().toUpperCase(),
address: addressData['state'],
city: addressData['city'] as String?,
country: addressData['country_code']?.toString().toUpperCase(),
address: addressData['state'] as String?,
);
}
return deviceLocationInfo;
} catch (e) {
throw Exception('Failed to load device location info: ${e.toString()}');
throw Exception('Failed to load device location info: $e');
}
}
}
@@ -1,36 +0,0 @@
import 'package:syncrow_web/pages/analytics/models/range_of_aqi.dart';
import 'package:syncrow_web/pages/analytics/modules/air_quality/widgets/aqi_type_dropdown.dart';
import 'package:syncrow_web/pages/analytics/params/get_range_of_aqi_param.dart';
import 'package:syncrow_web/pages/analytics/services/range_of_aqi/range_of_aqi_service.dart';
class FakeRangeOfAqiService implements RangeOfAqiService {
@override
Future<List<RangeOfAqi>> load(GetRangeOfAqiParam param) async {
return await Future.delayed(const Duration(milliseconds: 800), () {
final random = DateTime.now().millisecondsSinceEpoch;
return List.generate(30, (index) {
final date = DateTime(2025, 5, 1).add(Duration(days: index));
final min = ((random + index * 17) % 200).toDouble();
final avgDelta = ((random + index * 23) % 50).toDouble() + 20;
final maxDelta = ((random + index * 31) % 50).toDouble() + 30;
final avg = (min + avgDelta).clamp(0.0, 301.0);
final max = (avg + maxDelta).clamp(0.0, 301.0);
return RangeOfAqi(
data: [
RangeOfAqiValue(type: AqiType.aqi.code, min: min, average: avg, max: max),
RangeOfAqiValue(type: AqiType.pm25.code, min: min, average: avg, max: max),
RangeOfAqiValue(type: AqiType.pm10.code, min: min, average: avg, max: max),
RangeOfAqiValue(type: AqiType.hcho.code, min: min, average: avg, max: max),
RangeOfAqiValue(type: AqiType.tvoc.code, min: min, average: avg, max: max),
RangeOfAqiValue(type: AqiType.co2.code, min: min, average: avg, max: max),
],
date: date,
);
});
});
}
}
@@ -12,11 +12,8 @@ final class RemoteRangeOfAqiService implements RangeOfAqiService {
Future<List<RangeOfAqi>> load(GetRangeOfAqiParam param) async {
try {
final response = await _httpService.get(
path: 'endpoint',
queryParameters: {
'spaceUuid': param.spaceUuid,
'date': param.date.toIso8601String(),
},
path: '/aqi/range/space/${param.spaceUuid}',
queryParameters: {'monthDate': _formatDate(param.date)},
expectedResponseModel: (data) {
final json = data as Map<String, dynamic>? ?? {};
final mappedData = json['data'] as List<dynamic>? ?? [];
@@ -28,7 +25,11 @@ final class RemoteRangeOfAqiService implements RangeOfAqiService {
);
return response;
} catch (e) {
throw Exception('Failed to load energy consumption per phase: $e');
throw Exception('Failed to load range of aqi: $e');
}
}
static String _formatDate(DateTime date) {
return '${date.year}-${date.month.toString().padLeft(2, '0')}';
}
}
@@ -0,0 +1,23 @@
import 'package:flutter/material.dart';
import 'package:syncrow_web/utils/color_manager.dart';
import 'package:syncrow_web/utils/extension/build_context_x.dart';
class ChartsXAxisTitle extends StatelessWidget {
const ChartsXAxisTitle({
this.label = 'Day of month',
super.key,
});
final String label;
@override
Widget build(BuildContext context) {
return Text(
label,
style: context.textTheme.bodySmall?.copyWith(
color: ColorsManager.lightGreyColor,
fontSize: 8,
),
);
}
}
+20 -14
View File
@@ -36,7 +36,8 @@ class AuthBloc extends Bloc<AuthEvent, AuthState> {
////////////////////////////// forget password //////////////////////////////////
final TextEditingController forgetEmailController = TextEditingController();
final TextEditingController forgetPasswordController = TextEditingController();
final TextEditingController forgetPasswordController =
TextEditingController();
final TextEditingController forgetOtp = TextEditingController();
final forgetFormKey = GlobalKey<FormState>();
final forgetEmailKey = GlobalKey<FormState>();
@@ -53,7 +54,8 @@ class AuthBloc extends Bloc<AuthEvent, AuthState> {
return;
}
_remainingTime = 1;
add(UpdateTimerEvent(remainingTime: _remainingTime, isButtonEnabled: false));
add(UpdateTimerEvent(
remainingTime: _remainingTime, isButtonEnabled: false));
try {
forgetEmailValidate = '';
_remainingTime = (await AuthenticationAPI.sendOtp(
@@ -90,7 +92,8 @@ class AuthBloc extends Bloc<AuthEvent, AuthState> {
_timer?.cancel();
add(const UpdateTimerEvent(remainingTime: 0, isButtonEnabled: true));
} else {
add(UpdateTimerEvent(remainingTime: _remainingTime, isButtonEnabled: false));
add(UpdateTimerEvent(
remainingTime: _remainingTime, isButtonEnabled: false));
}
});
}
@@ -100,7 +103,7 @@ class AuthBloc extends Bloc<AuthEvent, AuthState> {
emit(const TimerState(isButtonEnabled: true, remainingTime: 0));
}
Future<void> changePassword(
Future<void> changePassword(
ChangePasswordEvent event, Emitter<AuthState> emit) async {
emit(LoadingForgetState());
try {
@@ -122,7 +125,6 @@ Future<void> changePassword(
}
}
String? validateCode(String? value) {
if (value == null || value.isEmpty) {
return 'Code is required';
@@ -131,7 +133,9 @@ Future<void> changePassword(
}
void _onUpdateTimer(UpdateTimerEvent event, Emitter<AuthState> emit) {
emit(TimerState(isButtonEnabled: event.isButtonEnabled, remainingTime: event.remainingTime));
emit(TimerState(
isButtonEnabled: event.isButtonEnabled,
remainingTime: event.remainingTime));
}
///////////////////////////////////// login /////////////////////////////////////
@@ -151,7 +155,6 @@ Future<void> changePassword(
static UserModel? user;
bool showValidationMessage = false;
void _login(LoginButtonPressed event, Emitter<AuthState> emit) async {
emit(AuthLoading());
if (isChecked) {
@@ -170,11 +173,11 @@ Future<void> changePassword(
);
} on APIException catch (e) {
validate = e.message;
emit(LoginInitial());
emit(LoginFailure(error: validate));
return;
} catch (e) {
validate = 'Something went wrong';
emit(LoginInitial());
emit(LoginFailure(error: validate));
return;
}
@@ -197,7 +200,6 @@ Future<void> changePassword(
}
}
checkBoxToggle(
CheckBoxEvent event,
Emitter<AuthState> emit,
@@ -339,12 +341,14 @@ Future<void> changePassword(
static Future<String> getTokenAndValidate() async {
try {
const storage = FlutterSecureStorage();
final firstLaunch =
await SharedPreferencesHelper.readBoolFromSP(StringsManager.firstLaunch) ?? true;
final firstLaunch = await SharedPreferencesHelper.readBoolFromSP(
StringsManager.firstLaunch) ??
true;
if (firstLaunch) {
storage.deleteAll();
}
await SharedPreferencesHelper.saveBoolToSP(StringsManager.firstLaunch, false);
await SharedPreferencesHelper.saveBoolToSP(
StringsManager.firstLaunch, false);
final value = await storage.read(key: Token.loginAccessTokenKey) ?? '';
if (value.isEmpty) {
return 'Token not found';
@@ -397,7 +401,9 @@ Future<void> changePassword(
final String formattedTime = [
if (days > 0) '${days}d', // Append 'd' for days
if (days > 0 || hours > 0)
hours.toString().padLeft(2, '0'), // Show hours if there are days or hours
hours
.toString()
.padLeft(2, '0'), // Show hours if there are days or hours
minutes.toString().padLeft(2, '0'),
seconds.toString().padLeft(2, '0'),
].join(':');
+93 -116
View File
@@ -50,20 +50,11 @@ class _DynamicTableState extends State<DynamicTable> {
bool _selectAll = false;
final ScrollController _verticalScrollController = ScrollController();
final ScrollController _horizontalScrollController = ScrollController();
late ScrollController _horizontalHeaderScrollController;
late ScrollController _horizontalBodyScrollController;
@override
void initState() {
super.initState();
_initializeSelection();
_horizontalHeaderScrollController = ScrollController();
_horizontalBodyScrollController = ScrollController();
// Synchronize horizontal scrolling
_horizontalBodyScrollController.addListener(() {
_horizontalHeaderScrollController
.jumpTo(_horizontalBodyScrollController.offset);
});
}
@override
@@ -113,94 +104,112 @@ class _DynamicTableState extends State<DynamicTable> {
context.read<DeviceManagementBloc>().add(UpdateSelection(_selectedRows));
}
@override
void dispose() {
_horizontalHeaderScrollController.dispose();
_horizontalBodyScrollController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Container(
decoration: widget.cellDecoration,
child: Column(
children: [
Container(
decoration: widget.headerDecoration ??
const BoxDecoration(color: ColorsManager.boxColor),
child: Scrollbar(
controller: _verticalScrollController,
thumbVisibility: true,
trackVisibility: true,
child: Scrollbar(
//fixed the horizontal scrollbar issue
controller: _horizontalScrollController,
thumbVisibility: true,
trackVisibility: true,
notificationPredicate: (notif) => notif.depth == 1,
child: SingleChildScrollView(
controller: _verticalScrollController,
child: SingleChildScrollView(
controller: _horizontalScrollController,
scrollDirection: Axis.horizontal,
physics: const NeverScrollableScrollPhysics(),
controller: _horizontalHeaderScrollController,
child: SizedBox(
width: widget.size.width,
child: Row(
child: Column(
children: [
if (widget.withCheckBox) _buildSelectAllCheckbox(),
...List.generate(widget.headers.length, (index) {
return _buildTableHeaderCell(
widget.headers[index], index);
}),
Container(
decoration: widget.headerDecoration ??
const BoxDecoration(
color: ColorsManager.boxColor,
),
child: Row(
children: [
if (widget.withCheckBox) _buildSelectAllCheckbox(),
...List.generate(widget.headers.length, (index) {
return _buildTableHeaderCell(
widget.headers[index], index);
})
//...widget.headers.map((header) => _buildTableHeaderCell(header)),
],
),
),
SizedBox(
width: widget.size.width,
child: widget.isEmpty
? _buildEmptyState()
: Column(
children:
List.generate(widget.data.length, (rowIndex) {
final row = widget.data[rowIndex];
return Row(
children: [
if (widget.withCheckBox)
_buildRowCheckbox(
rowIndex, widget.size.height * 0.08),
...row.asMap().entries.map((entry) {
return _buildTableCell(
entry.value.toString(),
widget.size.height * 0.08,
rowIndex: rowIndex,
columnIndex: entry.key,
);
}).toList(),
],
);
}),
),
),
],
),
),
),
),
Expanded(
child: Scrollbar(
controller: _verticalScrollController,
thumbVisibility: true,
trackVisibility: true,
child: SingleChildScrollView(
controller: _verticalScrollController,
child: Scrollbar(
controller: _horizontalBodyScrollController,
thumbVisibility: false,
trackVisibility: false,
notificationPredicate: (notif) => notif.depth == 1,
child: SingleChildScrollView(
scrollDirection: Axis.horizontal,
controller: _horizontalBodyScrollController,
child: Container(
color: ColorsManager.whiteColors,
child: SizedBox(
width: widget.size.width,
child: widget.isEmpty
? _buildEmptyState()
: Column(
children: List.generate(widget.data.length,
(rowIndex) {
final row = widget.data[rowIndex];
return Row(
children: [
if (widget.withCheckBox)
_buildRowCheckbox(rowIndex,
widget.size.height * 0.08),
...row.asMap().entries.map((entry) {
return _buildTableCell(
entry.value.toString(),
widget.size.height * 0.08,
rowIndex: rowIndex,
columnIndex: entry.key,
);
}).toList(),
],
);
}),
),
),
),
),
),
),
),
),
],
),
),
);
}
Widget _buildEmptyState() => Container(
height: widget.size.height,
color: ColorsManager.whiteColors,
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Row(
crossAxisAlignment: CrossAxisAlignment.center,
mainAxisAlignment: MainAxisAlignment.center,
children: [
Column(
children: [
SvgPicture.asset(Assets.emptyTable),
const SizedBox(height: 15),
Text(
widget.tableName == 'AccessManagement'
? 'No Password '
: 'No Devices',
style: Theme.of(context)
.textTheme
.bodySmall!
.copyWith(color: ColorsManager.grayColor),
)
],
),
],
),
SizedBox(height: widget.size.height * 0.5),
],
),
);
Widget _buildSelectAllCheckbox() {
return Container(
width: 50,
@@ -218,32 +227,6 @@ class _DynamicTableState extends State<DynamicTable> {
);
}
Widget _buildEmptyState() => Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Row(
crossAxisAlignment: CrossAxisAlignment.center,
mainAxisAlignment: MainAxisAlignment.center,
children: [
Column(
children: [
SvgPicture.asset(Assets.emptyTable),
const SizedBox(height: 15),
Text(
widget.tableName == 'AccessManagement'
? 'No Password '
: 'No Devices',
style: Theme.of(context)
.textTheme
.bodySmall!
.copyWith(color: ColorsManager.grayColor),
)
],
),
],
),
],
);
Widget _buildRowCheckbox(int index, double size) {
return Container(
width: 50,
@@ -298,12 +281,8 @@ class _DynamicTableState extends State<DynamicTable> {
);
}
Widget _buildTableCell(
String content,
double size, {
required int rowIndex,
required int columnIndex,
}) {
Widget _buildTableCell(String content, double size,
{required int rowIndex, required int columnIndex}) {
bool isBatteryLevel = content.endsWith('%');
double? batteryLevel;
@@ -311,7 +290,6 @@ class _DynamicTableState extends State<DynamicTable> {
batteryLevel = double.tryParse(content.replaceAll('%', '').trim());
}
bool isSettingsColumn = widget.headers[columnIndex] == 'Settings';
if (isSettingsColumn) {
return buildSettingsIcon(
width: 120,
@@ -416,11 +394,10 @@ class _DynamicTableState extends State<DynamicTable> {
padding: const EdgeInsets.all(8.0),
child: Center(
child: SvgPicture.asset(
Assets.settings, // ضع المسار الصحيح هنا
Assets.settings,
width: 40,
height: 22,
color: ColorsManager
.primaryColor, // نفس لون الأيقونة في الصورة
color: ColorsManager.primaryColor,
),
),
),
+30 -15
View File
@@ -45,7 +45,8 @@ class AcBloc extends Bloc<AcsEvent, AcsState> {
) async {
emit(AcsLoadingState());
try {
final status = await DevicesManagementApi().getDeviceStatus(event.deviceId);
final status =
await DevicesManagementApi().getDeviceStatus(event.deviceId);
deviceStatus = AcStatusModel.fromJson(event.deviceId, status.status);
if (deviceStatus.countdown1 != 0) {
final totalMinutes = deviceStatus.countdown1 * 6;
@@ -68,12 +69,13 @@ class AcBloc extends Bloc<AcsEvent, AcsState> {
}
}
void _listenToChanges(deviceId) {
StreamSubscription<DatabaseEvent>? _deviceStatusSubscription;
void _listenToChanges(String deviceId) {
try {
final ref = FirebaseDatabase.instance.ref('device-status/$deviceId');
final stream = ref.onValue;
stream.listen((DatabaseEvent event) async {
_deviceStatusSubscription =
ref.onValue.listen((DatabaseEvent event) async {
if (event.snapshot.value == null) return;
Map<dynamic, dynamic> usersMap =
@@ -82,10 +84,14 @@ class AcBloc extends Bloc<AcsEvent, AcsState> {
List<Status> statusList = [];
usersMap['status'].forEach((element) {
statusList.add(Status(code: element['code'], value: element['value']));
statusList
.add(Status(code: element['code'], value: element['value']));
});
deviceStatus = AcStatusModel.fromJson(usersMap['productUuid'], statusList);
deviceStatus =
AcStatusModel.fromJson(usersMap['productUuid'], statusList);
print('Device status updated: ${deviceStatus.acSwitch}');
if (!isClosed) {
add(AcStatusUpdated(deviceStatus));
}
@@ -106,15 +112,14 @@ class AcBloc extends Bloc<AcsEvent, AcsState> {
Emitter<AcsState> emit,
) async {
emit(AcsLoadingState());
_updateDeviceFunctionFromCode(event.code, event.value);
emit(ACStatusLoaded(status: deviceStatus));
try {
final success = await controlDeviceService.controlDevice(
deviceUuid: event.deviceId,
status: Status(code: event.code, value: event.value),
);
_updateDeviceFunctionFromCode(event.code, event.value);
emit(ACStatusLoaded(status: deviceStatus));
if (!success) {
emit(const AcsFailedState(error: 'Failed to control device'));
}
@@ -129,8 +134,10 @@ class AcBloc extends Bloc<AcsEvent, AcsState> {
) async {
emit(AcsLoadingState());
try {
final status = await DevicesManagementApi().getBatchStatus(event.devicesIds);
deviceStatus = AcStatusModel.fromJson(event.devicesIds.first, status.status);
final status =
await DevicesManagementApi().getBatchStatus(event.devicesIds);
deviceStatus =
AcStatusModel.fromJson(event.devicesIds.first, status.status);
emit(ACStatusLoaded(status: deviceStatus));
} catch (e) {
emit(AcsFailedState(error: e.toString()));
@@ -293,13 +300,17 @@ class AcBloc extends Bloc<AcsEvent, AcsState> {
totalSeconds--;
scheduledHours = totalSeconds ~/ 3600;
scheduledMinutes = (totalSeconds % 3600) ~/ 60;
add(UpdateTimerEvent());
if (!isClosed) {
add(UpdateTimerEvent());
}
} else {
_countdownTimer?.cancel();
timerActive = false;
scheduledHours = 0;
scheduledMinutes = 0;
add(TimerCompletedEvent());
if (!isClosed) {
add(TimerCompletedEvent());
}
}
});
}
@@ -326,7 +337,9 @@ class AcBloc extends Bloc<AcsEvent, AcsState> {
_startCountdownTimer(
emit,
);
add(UpdateTimerEvent());
if (!isClosed) {
add(UpdateTimerEvent());
}
}
}
@@ -370,6 +383,8 @@ class AcBloc extends Bloc<AcsEvent, AcsState> {
@override
Future<void> close() {
add(OnClose());
_countdownTimer?.cancel();
_deviceStatusSubscription?.cancel();
return super.close();
}
}
@@ -40,17 +40,18 @@ class DeviceManagementBloc
List<AllDevicesModel> devices = [];
_devices.clear();
var spaceBloc = event.context.read<SpaceTreeBloc>();
final projectUuid = await ProjectManager.getProjectUUID() ?? '';
final projectUuid = await ProjectManager.getProjectUUID() ?? '';
if (spaceBloc.state.selectedCommunities.isEmpty) {
devices = await DevicesManagementApi().fetchDevices('', '', projectUuid);
devices =
await DevicesManagementApi().fetchDevices('', '', projectUuid);
} else {
for (var community in spaceBloc.state.selectedCommunities) {
List<String> spacesList =
spaceBloc.state.selectedCommunityAndSpaces[community] ?? [];
for (var space in spacesList) {
devices.addAll(await DevicesManagementApi().fetchDevices(
community, space, projectUuid));
devices.addAll(await DevicesManagementApi()
.fetchDevices(community, space, projectUuid));
}
}
}
@@ -100,7 +101,7 @@ class DeviceManagementBloc
));
if (currentProductName.isNotEmpty) {
add(SearchDevices(productName: currentProductName));
add(SearchDevices(deviceNameOrProductName: currentProductName));
}
}
}
@@ -269,34 +270,41 @@ class DeviceManagementBloc
return 'All';
}
}
void _onSearchDevices(
SearchDevices event, Emitter<DeviceManagementState> emit) {
if ((event.community == null || event.community!.isEmpty) &&
(event.unitName == null || event.unitName!.isEmpty) &&
(event.productName == null || event.productName!.isEmpty)) {
(event.deviceNameOrProductName == null ||
event.deviceNameOrProductName!.isEmpty)) {
currentProductName = '';
if (state is DeviceManagementFiltered) {
add(FilterDevices(_getFilterFromIndex(_selectedIndex)));
} else {
return;
}
_filteredDevices = List.from(_devices);
emit(DeviceManagementLoaded(
devices: _devices,
selectedIndex: _selectedIndex,
onlineCount: _onlineCount,
offlineCount: _offlineCount,
lowBatteryCount: _lowBatteryCount,
selectedDevice: null,
isControlButtonEnabled: false,
));
return;
}
if (event.productName == currentProductName &&
if (event.deviceNameOrProductName == currentProductName &&
event.community == currentCommunity &&
event.unitName == currentUnitName &&
event.searchField) {
return;
}
currentProductName = event.productName ?? '';
currentProductName = event.deviceNameOrProductName ?? '';
currentCommunity = event.community;
currentUnitName = event.unitName;
List<AllDevicesModel> devicesToSearch = _filteredDevices;
List<AllDevicesModel> devicesToSearch = _devices;
if (devicesToSearch.isNotEmpty) {
final searchText = event.deviceNameOrProductName?.toLowerCase() ?? '';
final filteredDevices = devicesToSearch.where((device) {
final matchesCommunity = event.community == null ||
event.community!.isEmpty ||
@@ -304,31 +312,25 @@ class DeviceManagementBloc
?.toLowerCase()
.contains(event.community!.toLowerCase()) ??
false);
final matchesUnit = event.unitName == null ||
event.unitName!.isEmpty ||
(device.spaces != null &&
device.spaces!.isNotEmpty &&
device.spaces![0].spaceName!
.toLowerCase()
.contains(event.unitName!.toLowerCase()));
final matchesProductName = event.productName == null ||
event.productName!.isEmpty ||
(device.name
?.toLowerCase()
.contains(event.productName!.toLowerCase()) ??
false);
final matchesDeviceName = event.productName == null ||
event.productName!.isEmpty ||
(device.categoryName
?.toLowerCase()
.contains(event.productName!.toLowerCase()) ??
false);
device.spaces!.any((space) =>
space.spaceName != null &&
space.spaceName!
.toLowerCase()
.contains(event.unitName!.toLowerCase())));
return matchesCommunity &&
matchesUnit &&
(matchesProductName || matchesDeviceName);
final matchesSearchText = searchText.isEmpty ||
(device.name?.toLowerCase().contains(searchText) ?? false) ||
(device.productName?.toLowerCase().contains(searchText) ?? false);
return matchesCommunity && matchesUnit && matchesSearchText;
}).toList();
_filteredDevices = filteredDevices;
emit(DeviceManagementFiltered(
filteredDevices: filteredDevices,
selectedIndex: _selectedIndex,
@@ -38,18 +38,18 @@ class SelectedFilterChanged extends DeviceManagementEvent {
class SearchDevices extends DeviceManagementEvent {
final String? community;
final String? unitName;
final String? productName;
final String? deviceNameOrProductName;
final bool searchField;
const SearchDevices({
this.community,
this.unitName,
this.productName,
this.deviceNameOrProductName,
this.searchField = false,
});
@override
List<Object?> get props => [community, unitName, productName];
List<Object?> get props => [community, unitName, deviceNameOrProductName];
}
class SelectDevice extends DeviceManagementEvent {
@@ -6,6 +6,7 @@ import 'package:syncrow_web/pages/device_managment/all_devices/models/device_tag
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/curtain/curtain_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';
@@ -359,6 +360,14 @@ SOS
uuid: uuid ?? '',
name: name ?? '',
);
case 'CUR':
return [
ControlCurtainFunction(
deviceId: uuid ?? '',
deviceName: name ?? '',
type: 'BOTH',
)
];
case 'NCPS':
return [
FlushPresenceDelayFunction(
@@ -441,15 +450,10 @@ SOS
VoltageCStatusFunction(
deviceId: uuid ?? '', deviceName: name ?? '', type: 'IF'),
CurrentCStatusFunction(
deviceId: uuid ?? '',
deviceName: name ?? '',
type: 'IF'),
deviceId: uuid ?? '', deviceName: name ?? '', type: 'IF'),
PowerFactorCStatusFunction(
deviceId: uuid ?? '',
deviceName: name ?? '',
type: 'IF'),
deviceId: uuid ?? '', deviceName: name ?? '', type: 'IF'),
];
default:
return [];
}
@@ -8,15 +8,28 @@ import 'package:syncrow_web/pages/routines/bloc/create_routine_bloc/create_routi
import 'package:syncrow_web/pages/routines/bloc/routine_bloc/routine_bloc.dart';
import 'package:syncrow_web/pages/routines/view/create_new_routine_view.dart';
import 'package:syncrow_web/pages/routines/view/routines_view.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/utils/color_manager.dart';
import 'package:syncrow_web/utils/extension/build_context_x.dart';
import 'package:syncrow_web/utils/helpers/responsice_layout_helper/responsive_layout_helper.dart';
import 'package:syncrow_web/utils/theme/responsive_text_theme.dart';
import 'package:syncrow_web/web_layout/web_scaffold.dart';
class DeviceManagementPage extends StatelessWidget with HelperResponsiveLayout {
class DeviceManagementPage extends StatefulWidget with HelperResponsiveLayout {
const DeviceManagementPage({super.key});
@override
State<DeviceManagementPage> createState() => _DeviceManagementPageState();
}
class _DeviceManagementPageState extends State<DeviceManagementPage> {
@override
void initState() {
context.read<SpaceTreeBloc>().add(InitialEvent());
super.initState();
}
@override
Widget build(BuildContext context) {
return MultiBlocProvider(
@@ -62,7 +62,8 @@ class DeviceManagementBody extends StatelessWidget with HelperResponsiveLayout {
final buttonLabel =
(selectedDevices.length > 1) ? 'Batch Control' : 'Control';
final isAnyDeviceOffline =
selectedDevices.any((element) => !(element.online ?? false));
return Row(
children: [
Expanded(child: SpaceTreeView(
@@ -103,8 +104,26 @@ class DeviceManagementBody extends StatelessWidget with HelperResponsiveLayout {
decoration: containerDecoration,
child: Center(
child: DefaultButton(
backgroundColor: isAnyDeviceOffline
? ColorsManager.primaryColor
.withValues(alpha: 0.1)
: null,
onPressed: isControlButtonEnabled
? () {
if (isAnyDeviceOffline) {
ScaffoldMessenger.of(context)
.showSnackBar(
const SnackBar(
content: Text(
'This Device is Offline',
),
duration:
Duration(seconds: 2),
),
);
return;
}
if (selectedDevices.length == 1) {
showDialog(
context: context,
@@ -53,7 +53,7 @@ class _DeviceSearchFiltersState extends State<DeviceSearchFilters>
controller: controller,
onSubmitted: () {
final searchDevicesEvent = SearchDevices(
productName: _productNameController.text,
deviceNameOrProductName: _productNameController.text,
unitName: _unitNameController.text,
searchField: true,
);
@@ -68,7 +68,7 @@ class _DeviceSearchFiltersState extends State<DeviceSearchFilters>
onSearch: () => context.read<DeviceManagementBloc>().add(
SearchDevices(
unitName: _unitNameController.text,
productName: _productNameController.text,
deviceNameOrProductName: _productNameController.text,
searchField: true,
),
),
@@ -1,7 +1,10 @@
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_svg/svg.dart';
import 'package:syncrow_web/pages/device_managment/all_devices/bloc/device_mgmt_bloc/device_managment_bloc.dart';
import 'package:syncrow_web/pages/device_managment/all_devices/models/devices_model.dart';
import 'package:syncrow_web/pages/device_managment/device_setting/bloc/setting_bloc_bloc.dart';
import 'package:syncrow_web/pages/device_managment/device_setting/settings_model/device_info_model.dart';
import 'package:syncrow_web/pages/device_managment/device_setting/settings_model/sub_space_model.dart';
import 'package:syncrow_web/pages/device_managment/device_setting/sub_space_dialog.dart';
@@ -66,14 +69,25 @@ class DeviceManagementContent extends StatelessWidget {
Padding(
padding: const EdgeInsets.all(10.0),
child: InkWell(
onTap: () {
showSubSpaceDialog(
onTap: () async {
final selectedSubSpace = await showSubSpaceDialog(
context,
communityUuid: device.community!.uuid!,
spaceUuid: device.spaces!.first.uuid!,
subSpaces: subSpaces,
selected: device.subspace!.uuid,
selected: deviceInfo.subspace.uuid,
);
if (selectedSubSpace != null) {
Future.delayed(const Duration(milliseconds: 500), () {
context.read<SettingDeviceBloc>().add(
SettingBlocAssignRoom(
communityUuid: device.community!.uuid!,
spaceUuid: device.spaces!.first.uuid!,
subSpaceUuid: selectedSubSpace.id ?? '',
),
);
});
}
},
child: infoRow(
label: 'Sub-Space:',
@@ -9,13 +9,11 @@ import 'package:syncrow_web/utils/extension/build_context_x.dart';
class SubSpaceDialog extends StatefulWidget {
final List<SubSpaceModel> subSpaces;
final String? selected;
final void Function(SubSpaceModel?) onConfirmed;
const SubSpaceDialog({
Key? key,
required this.subSpaces,
this.selected,
required this.onConfirmed,
}) : super(key: key);
@override
@@ -86,30 +84,21 @@ class _SubSpaceDialogState extends State<SubSpaceDialog> {
}
}
void showSubSpaceDialog(
Future<SubSpaceModel?> showSubSpaceDialog(
BuildContext context, {
required List<SubSpaceModel> subSpaces,
String? selected,
required String communityUuid,
required String spaceUuid,
}) {
showDialog(
return showDialog<SubSpaceModel>(
context: context,
barrierDismissible: true,
builder: (ctx) => SubSpaceDialog(
subSpaces: subSpaces,
selected: selected,
onConfirmed: (selectedModel) {
if (selectedModel != null) {
context.read<SettingDeviceBloc>().add(
SettingBlocAssignRoom(
communityUuid: communityUuid,
spaceUuid: spaceUuid,
subSpaceUuid: selectedModel.id ?? '',
),
);
}
},
builder: (ctx) => BlocProvider.value(
value: BlocProvider.of<SettingDeviceBloc>(context),
child: SubSpaceDialog(
subSpaces: subSpaces,
selected: selected,
),
),
);
}
@@ -1,6 +1,4 @@
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:syncrow_web/pages/device_managment/device_setting/bloc/setting_bloc_bloc.dart';
import 'package:syncrow_web/pages/device_managment/device_setting/settings_model/sub_space_model.dart';
import 'package:syncrow_web/pages/device_managment/device_setting/sub_space_dialog.dart';
import 'package:syncrow_web/utils/color_manager.dart';
@@ -62,11 +60,12 @@ class SubSpaceDialogButtons extends StatelessWidget {
? null
: () {
final selectedModel = widget.subSpaces.firstWhere(
(space) => space.id == _selectedId,
orElse: () =>
SubSpaceModel(id: null, name: '', devices: []));
widget.onConfirmed(selectedModel);
Navigator.of(context).pop();
(space) => space.id == _selectedId,
orElse: () =>
SubSpaceModel(id: null, name: '', devices: []),
);
Navigator.of(context)
.pop(selectedModel);
},
child: Text(
'Confirm',
@@ -84,31 +83,3 @@ class SubSpaceDialogButtons extends StatelessWidget {
);
}
}
void showSubSpaceDialog(
BuildContext context, {
required List<SubSpaceModel> subSpaces,
String? selected,
required String communityUuid,
required String spaceUuid,
}) {
showDialog(
context: context,
barrierDismissible: true,
builder: (ctx) => SubSpaceDialog(
subSpaces: subSpaces,
selected: selected,
onConfirmed: (selectedModel) {
if (selectedModel != null) {
context.read<SettingDeviceBloc>().add(
SettingBlocAssignRoom(
communityUuid: communityUuid,
spaceUuid: spaceUuid,
subSpaceUuid: selectedModel.id ?? '',
),
);
}
},
),
);
}
@@ -1,5 +1,3 @@
// ignore_for_file: invalid_use_of_visible_for_testing_member
import 'dart:async';
import 'package:firebase_database/firebase_database.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
@@ -16,45 +14,38 @@ class DoorLockBloc extends Bloc<DoorLockEvent, DoorLockState> {
DoorLockBloc({required this.deviceId}) : super(DoorLockInitial()) {
on<DoorLockFetchStatus>(_onFetchDeviceStatus);
//on<DoorLockControl>(_onDoorLockControl);
on<UpdateLockEvent>(_updateLock);
on<DoorLockFactoryReset>(_onFactoryReset);
on<StatusUpdated>(_onStatusUpdated);
}
_listenToChanges(deviceId) {
void _listenToChanges(String deviceId) {
try {
DatabaseReference ref =
FirebaseDatabase.instance.ref('device-status/$deviceId');
Stream<DatabaseEvent> stream = ref.onValue;
final ref = FirebaseDatabase.instance.ref('device-status/$deviceId');
ref.onValue.listen((event) {
final data = event.snapshot.value;
if (data is Map) {
final statusData = data['status'] as List<dynamic>? ?? [];
final statusList = statusData.map((item) {
return Status(code: item['code'], value: item['value']);
}).toList();
stream.listen((DatabaseEvent event) {
Map<dynamic, dynamic> usersMap =
event.snapshot.value as Map<dynamic, dynamic>;
List<Status> statusList = [];
usersMap['status'].forEach((element) {
statusList
.add(Status(code: element['code'], value: element['value']));
});
deviceStatus =
DoorLockStatusModel.fromJson(usersMap['productUuid'], statusList);
if (!isClosed) {
add(StatusUpdated(deviceStatus));
final model =
DoorLockStatusModel.fromJson(data['productUuid'], statusList);
if (!isClosed) {
add(StatusUpdated(model));
}
}
});
} catch (_) {}
}
void _onStatusUpdated(StatusUpdated event, Emitter<DoorLockState> emit) {
emit(DoorLockStatusLoading());
deviceStatus = event.deviceStatus;
emit(DoorLockStatusLoaded(deviceStatus));
}
FutureOr<void> _onFetchDeviceStatus(
Future<void> _onFetchDeviceStatus(
DoorLockFetchStatus event, Emitter<DoorLockState> emit) async {
emit(DoorLockStatusLoading());
try {
@@ -63,14 +54,13 @@ class DoorLockBloc extends Bloc<DoorLockEvent, DoorLockState> {
deviceStatus =
DoorLockStatusModel.fromJson(event.deviceId, status.status);
_listenToChanges(event.deviceId);
emit(DoorLockStatusLoaded(deviceStatus));
} catch (e) {
emit(DoorLockControlError(e.toString()));
}
}
FutureOr<void> _updateLock(
Future<void> _updateLock(
UpdateLockEvent event, Emitter<DoorLockState> emit) async {
final oldValue = deviceStatus.normalOpenSwitch;
deviceStatus = deviceStatus.copyWith(normalOpenSwitch: !oldValue);
@@ -78,7 +68,6 @@ class DoorLockBloc extends Bloc<DoorLockEvent, DoorLockState> {
try {
final response = await DevicesManagementApi.openDoorLock(deviceId);
if (!response) {
_revertValueAndEmit(deviceId, 'normal_open_switch', oldValue, emit);
}
@@ -88,35 +77,8 @@ class DoorLockBloc extends Bloc<DoorLockEvent, DoorLockState> {
}
}
Future<void> _runDebounce({
required String deviceId,
required String code,
required dynamic value,
required dynamic oldValue,
required Emitter<DoorLockState> emit,
}) async {
if (_timer != null) {
_timer!.cancel();
}
_timer = Timer(const Duration(seconds: 1), () async {
try {
final response = await DevicesManagementApi()
.deviceControl(deviceId, Status(code: code, value: value));
if (!response) {
_revertValueAndEmit(deviceId, code, oldValue, emit);
}
} catch (e) {
_revertValueAndEmit(deviceId, code, oldValue, emit);
}
});
}
void _revertValueAndEmit(
String deviceId,
String code,
dynamic oldValue,
Emitter<DoorLockState> emit,
) {
void _revertValueAndEmit(String deviceId, String code, dynamic oldValue,
Emitter<DoorLockState> emit) {
_updateLocalValue(code, oldValue);
emit(DoorLockStatusLoaded(deviceStatus));
emit(const DoorLockControlError('Failed to control the device.'));
@@ -124,34 +86,23 @@ class DoorLockBloc extends Bloc<DoorLockEvent, DoorLockState> {
void _updateLocalValue(String code, dynamic value) {
switch (code) {
case 'reverse_lock':
if (value is bool) {
deviceStatus = deviceStatus.copyWith(reverseLock: value);
}
break;
case 'normal_open_switch':
if (value is bool) {
deviceStatus = deviceStatus.copyWith(normalOpenSwitch: value);
}
break;
case 'reverse_lock':
if (value is bool) {
deviceStatus = deviceStatus.copyWith(reverseLock: value);
}
break;
default:
break;
}
emit(DoorLockStatusLoaded(deviceStatus));
}
dynamic _getValueByCode(String code) {
switch (code) {
case 'reverse_lock':
return deviceStatus.reverseLock;
case 'normal_open_switch':
return deviceStatus.normalOpenSwitch;
default:
return null;
}
}
FutureOr<void> _onFactoryReset(
Future<void> _onFactoryReset(
DoorLockFactoryReset event, Emitter<DoorLockState> emit) async {
emit(DoorLockStatusLoading());
try {
@@ -8,7 +8,7 @@ import 'package:syncrow_web/pages/device_managment/door_lock/models/door_lock_st
import 'package:syncrow_web/utils/color_manager.dart';
import 'package:syncrow_web/utils/constants/assets.dart';
class DoorLockButton extends StatefulWidget {
class DoorLockButton extends StatelessWidget {
const DoorLockButton({
super.key,
required this.doorLock,
@@ -18,70 +18,28 @@ class DoorLockButton extends StatefulWidget {
final AllDevicesModel doorLock;
final DoorLockStatusModel smartDoorModel;
@override
State<DoorLockButton> createState() =>
_DoorLockButtonState(smartDoorModel: smartDoorModel);
}
class _DoorLockButtonState extends State<DoorLockButton>
with SingleTickerProviderStateMixin {
late AnimationController _animationController;
late Animation<double> _animation;
DoorLockStatusModel smartDoorModel;
_DoorLockButtonState({required this.smartDoorModel});
@override
void initState() {
super.initState();
_animationController = AnimationController(
vsync: this,
duration: const Duration(milliseconds: 500),
);
_animation = Tween<double>(begin: 0, end: 1).animate(_animationController)
..addListener(() {
setState(() {});
});
if (smartDoorModel.unlockRequest > 0) {
_animationController.reverse(from: 1);
}
}
@override
void didUpdateWidget(covariant DoorLockButton oldWidget) {
super.didUpdateWidget(oldWidget);
if (oldWidget.smartDoorModel.normalOpenSwitch !=
widget.smartDoorModel.normalOpenSwitch) {
setState(() {
smartDoorModel = widget.smartDoorModel;
});
if (smartDoorModel.unlockRequest > 0) {
_animationController.forward(from: 0);
} else {
_animationController.reverse(from: 1);
}
}
}
@override
void dispose() {
_animationController.dispose();
super.dispose();
double _calculateProgress() {
final value = smartDoorModel.unlockRequest;
if (value <= 0 || value > 30) return 0;
return value / 30.0;
}
@override
Widget build(BuildContext context) {
final progress = _calculateProgress();
final isEnabled = smartDoorModel.unlockRequest > 0;
return SizedBox(
width: 255,
height: 255,
child: InkWell(
onTap: () {
_animationController.forward(from: 0);
BlocProvider.of<DoorLockBloc>(context)
.add(UpdateLockEvent(value: !smartDoorModel.normalOpenSwitch));
},
onTap: isEnabled
? () {
BlocProvider.of<DoorLockBloc>(context).add(
UpdateLockEvent(value: !smartDoorModel.normalOpenSwitch),
);
}
: null,
child: Container(
width: 255,
height: 255,
@@ -115,15 +73,16 @@ class _DoorLockButtonState extends State<DoorLockButton>
),
),
),
SizedBox.expand(
child: CircularProgressIndicator(
value: _animation.value,
strokeWidth: 8,
backgroundColor: Colors.transparent,
valueColor: const AlwaysStoppedAnimation<Color>(
ColorsManager.primaryColor),
if (progress > 0)
SizedBox.expand(
child: CircularProgressIndicator(
value: progress,
strokeWidth: 8,
backgroundColor: Colors.transparent,
valueColor: const AlwaysStoppedAnimation<Color>(
ColorsManager.primaryColor),
),
),
),
],
),
),
@@ -12,7 +12,8 @@ import 'package:syncrow_web/utils/constants/assets.dart';
import 'package:syncrow_web/utils/helpers/responsice_layout_helper/responsive_layout_helper.dart';
//Smart Power Clamp
class SmartPowerDeviceControl extends StatelessWidget with HelperResponsiveLayout {
class SmartPowerDeviceControl extends StatelessWidget
with HelperResponsiveLayout {
final String deviceId;
const SmartPowerDeviceControl({super.key, required this.deviceId});
@@ -145,13 +146,16 @@ class SmartPowerDeviceControl extends StatelessWidget with HelperResponsiveLayou
children: [
IconButton(
icon: const Icon(Icons.arrow_left),
onPressed: () {
blocProvider.add(SmartPowerArrowPressedEvent(-1));
pageController.previousPage(
duration: const Duration(milliseconds: 300),
curve: Curves.easeInOut,
);
},
onPressed: blocProvider.currentPage <= 0
? null
: () {
blocProvider
.add(SmartPowerArrowPressedEvent(-1));
pageController.previousPage(
duration: const Duration(milliseconds: 300),
curve: Curves.easeInOut,
);
},
),
Text(
currentPage == 0
@@ -165,13 +169,16 @@ class SmartPowerDeviceControl extends StatelessWidget with HelperResponsiveLayou
),
IconButton(
icon: const Icon(Icons.arrow_right),
onPressed: () {
blocProvider.add(SmartPowerArrowPressedEvent(1));
pageController.nextPage(
duration: const Duration(milliseconds: 300),
curve: Curves.easeInOut,
);
},
onPressed: blocProvider.currentPage >= 3
? null
: () {
blocProvider
.add(SmartPowerArrowPressedEvent(1));
pageController.nextPage(
duration: const Duration(milliseconds: 300),
curve: Curves.easeInOut,
);
},
),
],
),
@@ -195,8 +202,8 @@ class SmartPowerDeviceControl extends StatelessWidget with HelperResponsiveLayou
blocProvider.add(SelectDateEvent(context: context));
blocProvider.add(FilterRecordsByDateEvent(
selectedDate: blocProvider.dateTime!,
viewType:
blocProvider.views[blocProvider.currentIndex]));
viewType: blocProvider
.views[blocProvider.currentIndex]));
},
widget: blocProvider.dateSwitcher(),
chartData: blocProvider.energyDataList.isNotEmpty
@@ -83,6 +83,12 @@ class ScheduleBloc extends Bloc<ScheduleEvent, ScheduleState> {
emit(currentState.copyWith(
scheduleMode: event.scheduleMode,
countdownRemaining: Duration.zero,
countdownHours: 0,
countdownMinutes: 0,
inchingHours: 0,
inchingMinutes: 0,
isCountdownActive: false,
isInchingActive: false,
));
}
}
@@ -94,6 +100,7 @@ class ScheduleBloc extends Bloc<ScheduleEvent, ScheduleState> {
if (state is ScheduleLoaded) {
final currentState = state as ScheduleLoaded;
emit(currentState.copyWith(
countdownSeconds: event.seconds,
countdownHours: event.hours,
countdownMinutes: event.minutes,
inchingHours: 0,
@@ -113,6 +120,7 @@ class ScheduleBloc extends Bloc<ScheduleEvent, ScheduleState> {
inchingHours: event.hours,
inchingMinutes: event.minutes,
countdownRemaining: Duration.zero,
inchingSeconds: 0, // Add this
));
}
}
@@ -424,6 +432,7 @@ class ScheduleBloc extends Bloc<ScheduleEvent, ScheduleState> {
countdownMinutes: countdownDuration.inMinutes % 60,
countdownRemaining: countdownDuration,
isCountdownActive: true,
countdownSeconds: countdownDuration.inSeconds,
),
);
@@ -437,6 +446,7 @@ class ScheduleBloc extends Bloc<ScheduleEvent, ScheduleState> {
countdownMinutes: 0,
countdownRemaining: Duration.zero,
isCountdownActive: false,
countdownSeconds: 0,
),
);
}
@@ -448,6 +458,7 @@ class ScheduleBloc extends Bloc<ScheduleEvent, ScheduleState> {
inchingMinutes: inchingDuration.inMinutes % 60,
isInchingActive: true,
countdownRemaining: inchingDuration,
countdownSeconds: inchingDuration.inSeconds,
),
);
}
@@ -574,8 +585,7 @@ class ScheduleBloc extends Bloc<ScheduleEvent, ScheduleState> {
}
String extractTime(String isoDateTime) {
// Example input: "2025-06-19T15:45:00.000"
return isoDateTime.split('T')[1].split('.')[0]; // gives "15:45:00"
return isoDateTime.split('T')[1].split('.')[0];
}
int? getTimeStampWithoutSeconds(DateTime? dateTime) {
@@ -146,14 +146,16 @@ class UpdateScheduleModeEvent extends ScheduleEvent {
class UpdateCountdownTimeEvent extends ScheduleEvent {
final int hours;
final int minutes;
final int seconds;
const UpdateCountdownTimeEvent({
required this.hours,
required this.minutes,
required this.seconds,
});
@override
List<Object> get props => [hours, minutes];
List<Object> get props => [hours, minutes, seconds];
}
class UpdateInchingTimeEvent extends ScheduleEvent {
@@ -26,11 +26,15 @@ class ScheduleLoaded extends ScheduleState {
final bool isCountdownActive;
final int inchingHours;
final int inchingMinutes;
final int inchingSeconds;
final bool isInchingActive;
final ScheduleModes scheduleMode;
final Duration? countdownRemaining;
final int? countdownSeconds;
const ScheduleLoaded({
this.countdownSeconds = 0,
this.inchingSeconds = 0,
required this.schedules,
this.selectedTime,
required this.selectedDays,
@@ -61,6 +65,9 @@ class ScheduleLoaded extends ScheduleState {
bool? isInchingActive,
ScheduleModes? scheduleMode,
Duration? countdownRemaining,
String? deviceId,
int? countdownSeconds,
int? inchingSeconds,
}) {
return ScheduleLoaded(
schedules: schedules ?? this.schedules,
@@ -68,7 +75,7 @@ class ScheduleLoaded extends ScheduleState {
selectedDays: selectedDays ?? this.selectedDays,
functionOn: functionOn ?? this.functionOn,
isEditing: isEditing ?? this.isEditing,
deviceId: deviceId,
deviceId: deviceId ?? this.deviceId,
countdownHours: countdownHours ?? this.countdownHours,
countdownMinutes: countdownMinutes ?? this.countdownMinutes,
isCountdownActive: isCountdownActive ?? this.isCountdownActive,
@@ -77,6 +84,8 @@ class ScheduleLoaded extends ScheduleState {
isInchingActive: isInchingActive ?? this.isInchingActive,
scheduleMode: scheduleMode ?? this.scheduleMode,
countdownRemaining: countdownRemaining ?? this.countdownRemaining,
countdownSeconds: countdownSeconds ?? this.countdownSeconds,
inchingSeconds: inchingSeconds ?? this.inchingSeconds,
);
}
@@ -96,6 +105,8 @@ class ScheduleLoaded extends ScheduleState {
isInchingActive,
scheduleMode,
countdownRemaining,
countdownSeconds,
inchingSeconds,
];
}
@@ -6,7 +6,8 @@ import 'package:syncrow_web/utils/color_manager.dart';
import 'package:syncrow_web/utils/extension/build_context_x.dart';
class CountdownInchingView extends StatefulWidget {
const CountdownInchingView({super.key});
final String deviceId;
const CountdownInchingView({super.key, required this.deviceId});
@override
State<CountdownInchingView> createState() => _CountdownInchingViewState();
@@ -15,25 +16,30 @@ class CountdownInchingView extends StatefulWidget {
class _CountdownInchingViewState extends State<CountdownInchingView> {
late FixedExtentScrollController _hoursController;
late FixedExtentScrollController _minutesController;
late FixedExtentScrollController _secondsController;
int _lastHours = -1;
int _lastMinutes = -1;
int _lastSeconds = -1;
@override
void initState() {
super.initState();
_hoursController = FixedExtentScrollController();
_minutesController = FixedExtentScrollController();
_secondsController = FixedExtentScrollController();
}
@override
void dispose() {
_hoursController.dispose();
_minutesController.dispose();
_secondsController.dispose();
super.dispose();
}
void _updateControllers(int displayHours, int displayMinutes) {
void _updateControllers(
int displayHours, int displayMinutes, int displaySeconds) {
if (_lastHours != displayHours) {
WidgetsBinding.instance.addPostFrameCallback((_) {
if (_hoursController.hasClients) {
@@ -50,6 +56,15 @@ class _CountdownInchingViewState extends State<CountdownInchingView> {
});
_lastMinutes = displayMinutes;
}
// Update seconds controller
if (_lastSeconds != displaySeconds) {
WidgetsBinding.instance.addPostFrameCallback((_) {
if (_secondsController.hasClients) {
_secondsController.jumpToItem(displaySeconds);
}
});
_lastSeconds = displaySeconds;
}
}
@override
@@ -57,7 +72,6 @@ class _CountdownInchingViewState extends State<CountdownInchingView> {
return BlocBuilder<ScheduleBloc, ScheduleState>(
builder: (context, state) {
if (state is! ScheduleLoaded) return const SizedBox.shrink();
final isCountDown = state.scheduleMode == ScheduleModes.countdown;
final isActive =
isCountDown ? state.isCountdownActive : state.isInchingActive;
@@ -67,8 +81,21 @@ class _CountdownInchingViewState extends State<CountdownInchingView> {
final displayMinutes = isActive && state.countdownRemaining != null
? state.countdownRemaining!.inMinutes.remainder(60)
: (isCountDown ? state.countdownMinutes : state.inchingMinutes);
final displaySeconds = isActive && state.countdownRemaining != null
? state.countdownRemaining!.inSeconds.remainder(60)
: (isCountDown ? state.countdownSeconds : state.inchingSeconds);
_updateControllers(displayHours, displayMinutes, displaySeconds!);
if (displayHours == 0 && displayMinutes == 0 && displaySeconds == 0) {
context.read<ScheduleBloc>().add(
StopScheduleEvent(
mode: ScheduleModes.countdown,
deviceId: widget.deviceId,
),
);
}
_updateControllers(displayHours, displayMinutes);
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
@@ -100,7 +127,10 @@ class _CountdownInchingViewState extends State<CountdownInchingView> {
(value) {
if (!isActive) {
context.read<ScheduleBloc>().add(UpdateCountdownTimeEvent(
hours: value, minutes: displayMinutes));
hours: value,
minutes: displayMinutes,
seconds: displaySeconds,
));
}
},
isActive: isActive,
@@ -115,11 +145,35 @@ class _CountdownInchingViewState extends State<CountdownInchingView> {
(value) {
if (!isActive) {
context.read<ScheduleBloc>().add(UpdateCountdownTimeEvent(
hours: displayHours, minutes: value));
hours: displayHours,
minutes: value,
seconds: displaySeconds,
));
}
},
isActive: isActive,
),
const SizedBox(width: 10),
if (isActive)
_buildPickerColumn(
context,
's',
displaySeconds,
60,
_secondsController,
(value) {
if (!isActive) {
context
.read<ScheduleBloc>()
.add(UpdateCountdownTimeEvent(
hours: displayHours,
minutes: displayMinutes,
seconds: value,
));
}
},
isActive: isActive,
),
],
),
],
@@ -74,7 +74,9 @@ class BuildScheduleView extends StatelessWidget {
),
if (state.scheduleMode == ScheduleModes.countdown ||
state.scheduleMode == ScheduleModes.inching)
const CountdownInchingView(),
CountdownInchingView(
deviceId: deviceUuid,
),
const SizedBox(height: 20),
if (state.scheduleMode == ScheduleModes.countdown)
CountdownModeButtons(
@@ -79,6 +79,7 @@ class DeviceControlDialog extends StatelessWidget with RouteControlsBasedCode {
}
Widget _buildDeviceInfoSection() {
final isOnlineDevice = device.online != null && device.online!;
return Padding(
padding: const EdgeInsets.symmetric(vertical: 25, horizontal: 50),
child: Table(
@@ -107,7 +108,7 @@ class DeviceControlDialog extends StatelessWidget with RouteControlsBasedCode {
'Installation Date and Time:',
formatDateTime(
DateTime.fromMillisecondsSinceEpoch(
((device.createTime ?? 0) * 1000),
(device.createTime ?? 0) * 1000,
),
),
),
@@ -126,12 +127,16 @@ class DeviceControlDialog extends StatelessWidget with RouteControlsBasedCode {
),
TableRow(
children: [
_buildInfoRow('Status:', 'Online', statusColor: Colors.green),
_buildInfoRow(
'Status:',
isOnlineDevice ? 'Online' : 'offline',
statusColor: isOnlineDevice ? Colors.green : Colors.red,
),
_buildInfoRow(
'Last Offline Date and Time:',
formatDateTime(
DateTime.fromMillisecondsSinceEpoch(
((device.updateTime ?? 0) * 1000),
(device.updateTime ?? 0) * 1000,
),
),
),
+16 -50
View File
@@ -13,30 +13,32 @@ import 'package:syncrow_web/pages/space_tree/bloc/space_tree_event.dart';
import 'package:syncrow_web/services/home_api.dart';
import 'package:syncrow_web/utils/constants/assets.dart';
import 'package:syncrow_web/utils/constants/routes_const.dart';
import 'package:syncrow_web/utils/navigation_service.dart';
class HomeBloc extends Bloc<HomeEvent, HomeState> {
UserModel? user;
String terms = '';
String policy = '';
HomeBloc() : super((HomeInitial())) {
// on<CreateNewNode>(_createNode);
HomeBloc() : super(HomeInitial()) {
on<FetchUserInfo>(_fetchUserInfo);
on<FetchTermEvent>(_fetchTerms);
on<FetchPolicyEvent>(_fetchPolicy);
on<ConfirmUserAgreementEvent>(_confirmUserAgreement);
}
Future _fetchUserInfo(FetchUserInfo event, Emitter<HomeState> emit) async {
Future<void> _fetchUserInfo(
FetchUserInfo event,
Emitter<HomeState> emit,
) async {
try {
var uuid =
final uuid =
await const FlutterSecureStorage().read(key: UserModel.userUuidKey);
user = await HomeApi().fetchUserInfo(uuid);
if (uuid != null) {
user = await HomeApi().fetchUserInfo(uuid);
}
if (user != null && user!.project != null) {
if (user != null && user?.project != null) {
await ProjectManager.setProjectUUID(user!.project!.uuid);
}
add(FetchTermEvent());
add(FetchPolicyEvent());
@@ -47,7 +49,7 @@ class HomeBloc extends Bloc<HomeEvent, HomeState> {
}
}
Future _fetchTerms(FetchTermEvent event, Emitter<HomeState> emit) async {
Future<void> _fetchTerms(FetchTermEvent event, Emitter<HomeState> emit) async {
try {
emit(LoadingHome());
terms = await HomeApi().fetchTerms();
@@ -57,22 +59,22 @@ class HomeBloc extends Bloc<HomeEvent, HomeState> {
}
}
Future _fetchPolicy(FetchPolicyEvent event, Emitter<HomeState> emit) async {
Future<void> _fetchPolicy(FetchPolicyEvent event, Emitter<HomeState> emit) async {
try {
emit(LoadingHome());
policy = await HomeApi().fetchPolicy();
emit(HomeInitial());
} catch (e) {
debugPrint("Error fetching policy: $e");
debugPrint('Error fetching policy: $e');
return;
}
}
Future _confirmUserAgreement(
Future<void> _confirmUserAgreement(
ConfirmUserAgreementEvent event, Emitter<HomeState> emit) async {
try {
emit(LoadingHome());
var uuid =
final uuid =
await const FlutterSecureStorage().read(key: UserModel.userUuidKey);
policy = await HomeApi().confirmUserAgreements(uuid);
emit(PolicyAgreement());
@@ -81,7 +83,7 @@ class HomeBloc extends Bloc<HomeEvent, HomeState> {
}
}
List<HomeItemModel> homeItems = [
final List<HomeItemModel> homeItems = [
HomeItemModel(
title: 'Access Management',
icon: Assets.accessIcon,
@@ -126,41 +128,5 @@ class HomeBloc extends Bloc<HomeEvent, HomeState> {
},
color: const Color(0xFF023DFE),
),
// HomeItemModel(
// title: 'Move in',
// icon: Assets.moveinIcon,
// active: false,
// onPress: (context) {},
// color: ColorsManager.primaryColor,
// ),
// HomeItemModel(
// title: 'Construction',
// icon: Assets.constructionIcon,
// active: false,
// onPress: (context) {},
// color: ColorsManager.primaryColor,
// ),
// HomeItemModel(
// title: 'Energy',
// icon: Assets.energyIcon,
// active: false,
// onPress: (context) {},
// color: ColorsManager.slidingBlueColor.withOpacity(0.2),
// ),
// HomeItemModel(
// title: 'Integrations',
// icon: Assets.integrationsIcon,
// active: false,
// onPress: (context) {},
// color: ColorsManager.slidingBlueColor.withOpacity(0.2),
// ),
// HomeItemModel(
// title: 'Asset',
// icon: Assets.assetIcon,
// active: false,
// onPress: (context) {},
// color: ColorsManager.slidingBlueColor.withOpacity(0.2),
// ),
];
}
+72 -11
View File
@@ -34,17 +34,9 @@ class HomeCard extends StatelessWidget {
child: Row(
mainAxisAlignment: MainAxisAlignment.start,
children: [
Flexible(
child: FittedBox(
fit: BoxFit.scaleDown,
child: Text(
name,
style: const TextStyle(
fontSize: 30,
color: Colors.white,
fontWeight: FontWeight.bold,
),
),
Expanded(
child: SpliteNameHelperWidget(
name: name,
),
),
],
@@ -63,3 +55,72 @@ class HomeCard extends StatelessWidget {
);
}
}
class SpliteNameHelperWidget extends StatelessWidget {
final String name;
const SpliteNameHelperWidget({
super.key,
required this.name,
});
@override
Widget build(BuildContext context) {
List<String> parts = name.split(' ');
if (parts.length == 2) {
// Two-word string
return Padding(
padding: const EdgeInsetsGeometry.only(top: 10, left: 10),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Expanded(
child: FittedBox(
fit: BoxFit.scaleDown,
child: Text(
parts[0],
style: const TextStyle(
fontSize: 30,
color: Colors.white,
fontWeight: FontWeight.bold,
),
),
),
),
Expanded(
child: FittedBox(
fit: BoxFit.scaleDown,
child: Text(
parts[1],
style: const TextStyle(
fontSize: 30,
color: Colors.white,
fontWeight: FontWeight.bold,
),
),
),
),
],
),
);
} else {
// One-word string
return Text(
name,
style: const TextStyle(
fontSize: 30,
color: Colors.white,
fontWeight: FontWeight.bold,
),
);
}
}
}
// Text(
// name,
// style: const TextStyle(
// fontSize: 32,
// color: Colors.white,
// fontWeight: FontWeight.bold,
// ),
// )
+26 -6
View File
@@ -1,17 +1,37 @@
import 'package:flutter/cupertino.dart';
import 'package:flutter_bloc/flutter_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/home/view/home_page_mobile.dart';
import 'package:syncrow_web/pages/home/view/home_page_web.dart';
import 'package:syncrow_web/utils/helpers/responsice_layout_helper/responsive_layout_helper.dart';
class HomePage extends StatelessWidget with HelperResponsiveLayout {
class HomePage extends StatefulWidget {
const HomePage({super.key});
@override
State<HomePage> createState() => _HomePageState();
}
class _HomePageState extends State<HomePage> with HelperResponsiveLayout {
@override
void initState() {
_fetchUserInfo();
super.initState();
}
@override
Widget build(BuildContext context) {
final isSmallScreen = isSmallScreenSize(context);
final isMediumScreen = isMediumScreenSize(context);
return isSmallScreen || isMediumScreen
? HomeMobilePage()
: const HomeWebPage();
if (isSmallScreenSize(context) || isMediumScreenSize(context)) {
return HomeMobilePage();
}
return const HomeWebPage();
}
void _fetchUserInfo() {
final bloc = context.read<HomeBloc>();
if (bloc.user == null) bloc.add(const FetchUserInfo());
}
}
+1 -1
View File
@@ -92,7 +92,7 @@ class _HomeWebPageState extends State<HomeWebPage> {
flex: 4,
child: SizedBox(
height: size.height * 0.6,
width: size.width * 0.68,
width: size.width * 0.8,
child: GridView.builder(
itemCount: homeBloc.homeItems.length,
gridDelegate:
@@ -1,7 +1,9 @@
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_svg/flutter_svg.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/roles_and_permission/model/roles_user_model.dart';
import 'package:syncrow_web/pages/roles_and_permission/users_page/add_user_dialog/bloc/users_bloc.dart';
import 'package:syncrow_web/pages/roles_and_permission/users_page/add_user_dialog/bloc/users_event.dart';
import 'package:syncrow_web/pages/roles_and_permission/users_page/add_user_dialog/bloc/users_status.dart';
@@ -12,8 +14,11 @@ import 'package:syncrow_web/utils/color_manager.dart';
import 'package:syncrow_web/utils/constants/assets.dart';
class EditUserDialog extends StatefulWidget {
final String? userId;
const EditUserDialog({super.key, this.userId});
final RolesUserModel? user;
const EditUserDialog({
super.key,
this.user,
});
@override
_EditUserDialogState createState() => _EditUserDialogState();
@@ -28,10 +33,11 @@ class _EditUserDialogState extends State<EditUserDialog> {
create: (BuildContext context) => UsersBloc()
// ..add(const LoadCommunityAndSpacesEvent())
..add(const RoleEvent())
..add(GetUserByIdEvent(uuid: widget.userId)),
..add(GetUserByIdEvent(uuid: widget.user!.uuid)),
child: BlocConsumer<UsersBloc, UsersState>(listener: (context, state) {
if (state is SpacesLoadedState) {
BlocProvider.of<UsersBloc>(context).add(GetUserByIdEvent(uuid: widget.userId));
BlocProvider.of<UsersBloc>(context)
.add(GetUserByIdEvent(uuid: widget.user!.uuid));
}
}, builder: (context, state) {
final _blocRole = BlocProvider.of<UsersBloc>(context);
@@ -39,7 +45,8 @@ class _EditUserDialogState extends State<EditUserDialog> {
return Dialog(
child: Container(
decoration: const BoxDecoration(
color: Colors.white, borderRadius: BorderRadius.all(Radius.circular(20))),
color: Colors.white,
borderRadius: BorderRadius.all(Radius.circular(20))),
width: 900,
child: Column(
children: [
@@ -68,7 +75,8 @@ class _EditUserDialogState extends State<EditUserDialog> {
children: [
_buildStep1Indicator(1, "Basics", _blocRole),
_buildStep2Indicator(2, "Spaces", _blocRole),
_buildStep3Indicator(3, "Role & Permissions", _blocRole),
_buildStep3Indicator(
3, "Role & Permissions", _blocRole),
],
),
),
@@ -86,7 +94,7 @@ class _EditUserDialogState extends State<EditUserDialog> {
children: [
const SizedBox(height: 10),
Expanded(
child: _getFormContent(widget.userId),
child: _getFormContent(widget.user!),
),
const SizedBox(height: 20),
],
@@ -116,13 +124,14 @@ class _EditUserDialogState extends State<EditUserDialog> {
if (currentStep < 3) {
currentStep++;
if (currentStep == 2) {
_blocRole.add(CheckStepStatus(isEditUser: true));
_blocRole
.add(CheckStepStatus(isEditUser: true));
} else if (currentStep == 3) {
_blocRole.add(const CheckSpacesStepStatus());
}
} else {
_blocRole
.add(EditInviteUsers(context: context, userId: widget.userId!));
_blocRole.add(EditInviteUsers(
context: context, userId: widget.user!.uuid));
}
});
},
@@ -131,7 +140,8 @@ class _EditUserDialogState extends State<EditUserDialog> {
style: TextStyle(
color: (_blocRole.isCompleteSpaces == false ||
_blocRole.isCompleteBasics == false ||
_blocRole.isCompleteRolePermissions == false) &&
_blocRole.isCompleteRolePermissions ==
false) &&
currentStep == 3
? ColorsManager.grayColor
: ColorsManager.secondaryColor),
@@ -146,15 +156,15 @@ class _EditUserDialogState extends State<EditUserDialog> {
}));
}
Widget _getFormContent(userid) {
Widget _getFormContent(RolesUserModel user) {
switch (currentStep) {
case 1:
return BasicsView(
userId: userid,
userId: user.uuid,
);
case 2:
return SpacesAccessView(
userId: userid,
userId: user.uuid,
);
case 3:
return const RolesAndPermission();
@@ -166,6 +176,7 @@ class _EditUserDialogState extends State<EditUserDialog> {
int step3 = 0;
Widget _buildStep1Indicator(int step, String label, UsersBloc bloc) {
final isCurrentStep = currentStep == step;
return GestureDetector(
onTap: () {
setState(() {
@@ -189,7 +200,7 @@ class _EditUserDialogState extends State<EditUserDialog> {
child: Row(
children: [
SvgPicture.asset(
currentStep == step
isCurrentStep
? Assets.currentProcessIcon
: bloc.isCompleteBasics == false
? Assets.wrongProcessIcon
@@ -204,8 +215,11 @@ class _EditUserDialogState extends State<EditUserDialog> {
label,
style: TextStyle(
fontSize: 16,
color: currentStep == step ? ColorsManager.blackColor : ColorsManager.greyColor,
fontWeight: currentStep == step ? FontWeight.bold : FontWeight.normal,
color: isCurrentStep
? ColorsManager.blackColor
: ColorsManager.greyColor,
fontWeight:
isCurrentStep ? FontWeight.bold : FontWeight.normal,
),
),
],
@@ -229,6 +243,7 @@ class _EditUserDialogState extends State<EditUserDialog> {
}
Widget _buildStep2Indicator(int step, String label, UsersBloc bloc) {
final isCurrentStep = currentStep == step;
return GestureDetector(
onTap: () {
setState(() {
@@ -248,7 +263,7 @@ class _EditUserDialogState extends State<EditUserDialog> {
child: Row(
children: [
SvgPicture.asset(
currentStep == step
isCurrentStep
? Assets.currentProcessIcon
: bloc.isCompleteSpaces == false
? Assets.wrongProcessIcon
@@ -263,8 +278,11 @@ class _EditUserDialogState extends State<EditUserDialog> {
label,
style: TextStyle(
fontSize: 16,
color: currentStep == step ? ColorsManager.blackColor : ColorsManager.greyColor,
fontWeight: currentStep == step ? FontWeight.bold : FontWeight.normal,
color: isCurrentStep
? ColorsManager.blackColor
: ColorsManager.greyColor,
fontWeight:
isCurrentStep ? FontWeight.bold : FontWeight.normal,
),
),
],
@@ -288,6 +306,7 @@ class _EditUserDialogState extends State<EditUserDialog> {
}
Widget _buildStep3Indicator(int step, String label, UsersBloc bloc) {
final isCurrentStep = currentStep == step;
return GestureDetector(
onTap: () {
setState(() {
@@ -306,7 +325,7 @@ class _EditUserDialogState extends State<EditUserDialog> {
child: Row(
children: [
SvgPicture.asset(
currentStep == step
isCurrentStep
? Assets.currentProcessIcon
: bloc.isCompleteRolePermissions == false
? Assets.wrongProcessIcon
@@ -321,8 +340,11 @@ class _EditUserDialogState extends State<EditUserDialog> {
label,
style: TextStyle(
fontSize: 16,
color: currentStep == step ? ColorsManager.blackColor : ColorsManager.greyColor,
fontWeight: currentStep == step ? FontWeight.bold : FontWeight.normal,
color: isCurrentStep
? ColorsManager.blackColor
: ColorsManager.greyColor,
fontWeight:
isCurrentStep ? FontWeight.bold : FontWeight.normal,
),
),
],
@@ -452,33 +452,31 @@ class UsersPage extends StatelessWidget {
),
Row(
children: [
user.isEnabled != false
? actionButton(
isActive: true,
title: "Edit",
onTap: () {
context
.read<SpaceTreeBloc>()
.add(ClearCachedData());
showDialog(
context: context,
barrierDismissible: false,
builder: (BuildContext context) {
return EditUserDialog(
userId: user.uuid);
},
).then((v) {
if (v != null) {
if (v != null) {
_blocRole.add(const GetUsers());
}
}
});
if (user.isEnabled != false)
actionButton(
isActive: true,
title: "Edit",
onTap: () {
context
.read<SpaceTreeBloc>()
.add(ClearCachedData());
showDialog(
context: context,
barrierDismissible: false,
builder: (BuildContext context) {
return EditUserDialog(user: user);
},
)
: actionButton(
title: "Edit",
),
).then((v) {
if (v != null) {
_blocRole.add(const GetUsers());
}
});
},
)
else
actionButton(
title: "Edit",
),
actionButton(
title: "Delete",
onTap: () {
@@ -13,6 +13,35 @@ class FunctionBloc extends Bloc<FunctionBlocEvent, FunctionBlocState> {
on<AddFunction>(_onAddFunction);
on<SelectFunction>(_onSelectFunction);
}
// void _onAddFunction(AddFunction event, Emitter<FunctionBlocState> emit) {
// final functions = List<DeviceFunctionData>.from(state.addedFunctions);
// final existingIndex = functions.indexWhere(
// (f) => f.functionCode == event.functionData.functionCode,
// );
// if (existingIndex != -1) {
// final existingData = functions[existingIndex];
// functions[existingIndex] = DeviceFunctionData(
// entityId: event.functionData.entityId,
// functionCode: event.functionData.functionCode,
// operationName: event.functionData.operationName,
// value: event.functionData.value ?? existingData.value,
// valueDescription: event.functionData.valueDescription ??
// existingData.valueDescription,
// condition: event.functionData.condition ?? existingData.condition,
// step: event.functionData.step ?? existingData.step,
// );
// } else {
// functions.clear();
// functions.add(event.functionData);
// }
// emit(state.copyWith(
// addedFunctions: functions,
// selectedFunction: event.functionData.functionCode,
// ));
// }
void _onAddFunction(AddFunction event, Emitter<FunctionBlocState> emit) {
final functions = List<DeviceFunctionData>.from(state.addedFunctions);
final existingIndex = functions.indexWhere(
@@ -20,19 +49,10 @@ class FunctionBloc extends Bloc<FunctionBlocEvent, FunctionBlocState> {
);
if (existingIndex != -1) {
final existingData = functions[existingIndex];
functions[existingIndex] = DeviceFunctionData(
entityId: event.functionData.entityId,
functionCode: event.functionData.functionCode,
operationName: event.functionData.operationName,
value: event.functionData.value ?? existingData.value,
valueDescription: event.functionData.valueDescription ??
existingData.valueDescription,
condition: event.functionData.condition ?? existingData.condition,
step: event.functionData.step ?? existingData.step,
);
// Update the function value
functions[existingIndex] = event.functionData;
} else {
functions.clear();
// Add new function value
functions.add(event.functionData);
}
@@ -4,8 +4,8 @@ import 'package:bloc/bloc.dart';
import 'package:dio/dio.dart';
import 'package:equatable/equatable.dart';
import 'package:flutter/material.dart';
import 'package:syncrow_web/pages/common/bloc/project_manager.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:syncrow_web/pages/common/bloc/project_manager.dart';
import 'package:syncrow_web/pages/device_managment/all_devices/models/devices_model.dart';
import 'package:syncrow_web/pages/routines/bloc/automation_scene_trigger_bloc/automation_status_update.dart';
import 'package:syncrow_web/pages/routines/bloc/create_routine_bloc/create_routine_bloc.dart';
@@ -27,9 +27,6 @@ import 'package:uuid/uuid.dart';
part 'routine_event.dart';
part 'routine_state.dart';
// String spaceId = '25c96044-fadf-44bb-93c7-3c079e527ce6';
// String communityId = 'aff21a57-2f91-4e5c-b99b-0182c3ab65a9';
class RoutineBloc extends Bloc<RoutineEvent, RoutineState> {
RoutineBloc() : super(const RoutineState()) {
on<AddToIfContainer>(_onAddToIfContainer);
@@ -173,45 +170,45 @@ class RoutineBloc extends Bloc<RoutineEvent, RoutineState> {
}
}
Future<void> _onLoadScenes(
LoadScenes event, Emitter<RoutineState> emit) async {
emit(state.copyWith(isLoading: true, errorMessage: null));
List<ScenesModel> scenes = [];
try {
BuildContext context = NavigationService.navigatorKey.currentContext!;
var createRoutineBloc = context.read<CreateRoutineBloc>();
final projectUuid = await ProjectManager.getProjectUUID() ?? '';
if (createRoutineBloc.selectedSpaceId == '' &&
createRoutineBloc.selectedCommunityId == '') {
var spaceBloc = context.read<SpaceTreeBloc>();
for (var communityId in spaceBloc.state.selectedCommunities) {
List<String> spacesList =
spaceBloc.state.selectedCommunityAndSpaces[communityId] ?? [];
for (var spaceId in spacesList) {
scenes.addAll(
await SceneApi.getScenes(spaceId, communityId, projectUuid));
}
Future<void> _onLoadScenes(
LoadScenes event, Emitter<RoutineState> emit) async {
emit(state.copyWith(isLoading: true, errorMessage: null));
List<ScenesModel> scenes = [];
try {
BuildContext context = NavigationService.navigatorKey.currentContext!;
var createRoutineBloc = context.read<CreateRoutineBloc>();
final projectUuid = await ProjectManager.getProjectUUID() ?? '';
if (createRoutineBloc.selectedSpaceId == '' &&
createRoutineBloc.selectedCommunityId == '') {
var spaceBloc = context.read<SpaceTreeBloc>();
for (var communityId in spaceBloc.state.selectedCommunities) {
List<String> spacesList =
spaceBloc.state.selectedCommunityAndSpaces[communityId] ?? [];
for (var spaceId in spacesList) {
scenes.addAll(
await SceneApi.getScenes(spaceId, communityId, projectUuid));
}
} else {
scenes.addAll(await SceneApi.getScenes(
createRoutineBloc.selectedSpaceId,
createRoutineBloc.selectedCommunityId,
projectUuid));
}
emit(state.copyWith(
scenes: scenes,
isLoading: false,
));
} catch (e) {
emit(state.copyWith(
isLoading: false,
loadScenesErrorMessage: 'Failed to load scenes',
errorMessage: '',
loadAutomationErrorMessage: '',
scenes: scenes));
} else {
scenes.addAll(await SceneApi.getScenes(
createRoutineBloc.selectedSpaceId,
createRoutineBloc.selectedCommunityId,
projectUuid));
}
emit(state.copyWith(
scenes: scenes,
isLoading: false,
));
} catch (e) {
emit(state.copyWith(
isLoading: false,
loadScenesErrorMessage: 'Failed to load scenes',
errorMessage: '',
loadAutomationErrorMessage: '',
scenes: scenes));
}
}
Future<void> _onLoadAutomation(
LoadAutomation event, Emitter<RoutineState> emit) async {
@@ -1163,8 +1160,8 @@ class RoutineBloc extends Bloc<RoutineEvent, RoutineState> {
if (result['success']) {
add(ResetRoutineState());
add(LoadAutomation());
add(LoadScenes());
add(const LoadAutomation());
add(const LoadScenes());
} else {
emit(state.copyWith(
isLoading: false,
@@ -1422,15 +1419,17 @@ class RoutineBloc extends Bloc<RoutineEvent, RoutineState> {
event.automationId, event.automationStatusUpdate, projectId);
if (success) {
final updatedAutomations = await SceneApi.getAutomationByUnitId(
event.automationStatusUpdate.spaceUuid,
event.communityId,
projectId);
// await SceneApi.getAutomationByUnitId(
// event.automationStatusUpdate.spaceUuid,
// event.communityId,
// projectId);
// Remove from loading set safely
final updatedLoadingIds = {...state.loadingAutomationIds!}
..remove(event.automationId);
final updatedAutomations = changeItemStateOnToggelingSceen(
state.automations, event.automationId);
emit(state.copyWith(
automations: updatedAutomations,
loadingAutomationIds: updatedLoadingIds,
@@ -1452,4 +1451,24 @@ class RoutineBloc extends Bloc<RoutineEvent, RoutineState> {
));
}
}
List<ScenesModel> changeItemStateOnToggelingSceen(
List<ScenesModel> oldSceen, String automationId) {
return oldSceen.map((scene) {
if (scene.id == automationId) {
return ScenesModel(
id: scene.id,
sceneTuyaId: scene.sceneTuyaId,
name: scene.name,
status: scene.status == 'enable' ? 'disable' : 'enable',
type: scene.type,
spaceName: scene.spaceName,
spaceId: scene.spaceId,
communityId: scene.communityId,
icon: scene.icon,
);
}
return scene;
}).toList();
}
}
@@ -117,7 +117,7 @@ class _DropdownContentState extends State<_DropdownContent> {
final selectedCommunity = _findCommunity(state, state.selectedSpaceId);
return Container(
height: 46,
height: 40,
decoration: BoxDecoration(
border: Border.all(color: Colors.grey.shade300),
borderRadius: BorderRadius.circular(12),
@@ -149,7 +149,7 @@ class _DropdownContentState extends State<_DropdownContent> {
),
),
height: 45,
width: 33,
width: 44,
child: const Icon(
Icons.keyboard_arrow_down,
color: ColorsManager.textGray,
@@ -44,144 +44,156 @@ class _CreateNewRoutinesDialogState extends State<CreateNewRoutinesDialog> {
_selectedSpace = null;
_selectedCommunity = _selectedId;
}
return AlertDialog(
return Dialog(
backgroundColor: Colors.white,
insetPadding: EdgeInsets.zero,
contentPadding: EdgeInsets.zero,
insetPadding: const EdgeInsets.symmetric(
horizontal: 20,
),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12)),
title: Text(
'Create New Routines',
textAlign: TextAlign.center,
style: Theme.of(context).textTheme.bodyMedium!.copyWith(
color: ColorsManager.spaceColor,
fontSize: 20,
fontWeight: FontWeight.w700,
),
),
content: Stack(
children: [
Column(
mainAxisSize: MainAxisSize.min,
children: [
const Divider(),
const SizedBox(height: 20),
Column(
children: [
Padding(
padding:
const EdgeInsets.only(left: 13, right: 8),
child: Column(
children: [
SpaceTreeDropdown(
selectedSpaceId: _selectedId,
child: Container(
width: 450,
child: Stack(
children: [
Column(
mainAxisSize: MainAxisSize.min,
children: [
const SizedBox(height: 20),
Text(
'Create New Routines',
textAlign: TextAlign.center,
style:
Theme.of(context).textTheme.bodyMedium!.copyWith(
color: ColorsManager.spaceColor,
fontSize: 20,
fontWeight: FontWeight.w700,
),
),
const Divider(),
const SizedBox(height: 20),
Column(
children: [
Column(
children: [
Padding(
padding: const EdgeInsets.only(
left: 13, right: 10),
child: Column(
children: [
SpaceTreeDropdown(
selectedSpaceId: _selectedId,
onChanged: (String? newValue) {
setState(
() => _selectedId = newValue!);
if (_selectedId != null) {
_bloc.add(
SpaceOnlyWithDevicesEvent(
_selectedId!));
}
},
),
],
)),
const SizedBox(height: 21),
Padding(
padding: const EdgeInsets.only(
left: 15, right: 20),
child: SpaceDropdown(
hintMessage: spaceHint,
spaces: spaces,
selectedValue: _selectedSpace,
onChanged: (String? newValue) {
setState(() => _selectedId = newValue!);
if (_selectedId != null) {
_bloc.add(SpaceOnlyWithDevicesEvent(
_selectedId!));
}
setState(() {
_selectedSpace = newValue;
});
},
),
],
)),
const SizedBox(height: 5),
const SizedBox(height: 8),
Padding(
padding: const EdgeInsets.only(left: 15, right: 15),
child: SpaceDropdown(
hintMessage: spaceHint,
spaces: spaces,
selectedValue: _selectedSpace,
onChanged: (String? newValue) {
setState(() {
_selectedSpace = newValue;
});
},
),
],
),
),
],
),
const SizedBox(height: 20),
const Divider(),
Row(
mainAxisAlignment: MainAxisAlignment.spaceAround,
children: [
Padding(
padding: const EdgeInsets.only(
left: 20,
right: 20,
),
child: TextButton(
onPressed: () {
Navigator.of(context).pop();
},
child: Text(
'Cancel',
style: Theme.of(context)
.textTheme
.bodyMedium!
.copyWith(
fontWeight: FontWeight.w400,
fontSize: 14,
color: ColorsManager.blackColor,
),
],
),
const SizedBox(height: 20),
const Divider(),
Row(
mainAxisAlignment: MainAxisAlignment.spaceAround,
children: [
Padding(
padding: const EdgeInsets.only(
left: 20,
right: 20,
),
child: TextButton(
onPressed: () {
Navigator.of(context).pop();
},
child: Text(
'Cancel',
style: Theme.of(context)
.textTheme
.bodyMedium!
.copyWith(
fontWeight: FontWeight.w400,
fontSize: 14,
color: ColorsManager.blackColor,
),
),
),
),
),
Padding(
padding: const EdgeInsets.only(
left: 20,
right: 20,
),
child: TextButton(
onPressed: _selectedCommunity != null &&
_selectedSpace != null
? () {
Navigator.of(context).pop({
'community': _selectedCommunity,
'space': _selectedSpace,
});
}
: null,
child: Text(
'Next',
style: Theme.of(context)
.textTheme
.bodyMedium!
.copyWith(
fontWeight: FontWeight.w400,
fontSize: 14,
color: _selectedCommunity != null &&
_selectedSpace != null
? ColorsManager.blueColor
: Colors.blue.shade100,
),
Padding(
padding: const EdgeInsets.only(
left: 20,
right: 20,
),
child: TextButton(
onPressed: _selectedCommunity != null &&
_selectedSpace != null
? () {
Navigator.of(context).pop({
'community': _selectedCommunity,
'space': _selectedSpace,
});
}
: null,
child: Text(
'Next',
style: Theme.of(context)
.textTheme
.bodyMedium!
.copyWith(
fontWeight: FontWeight.w400,
fontSize: 14,
color: _selectedCommunity != null &&
_selectedSpace != null
? ColorsManager.blueColor
: Colors.blue.shade100,
),
),
),
),
),
],
),
const SizedBox(height: 10),
],
),
if (isLoadingCommunities)
const SizedBox(
height: 200,
child: Column(
mainAxisSize: MainAxisSize.min,
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Center(
child: CircularProgressIndicator(
color: ColorsManager.primaryColor,
),
),
],
),
],
),
const SizedBox(height: 10),
],
),
],
if (isLoadingCommunities)
const SizedBox(
height: 200,
child: Column(
mainAxisSize: MainAxisSize.min,
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Center(
child: CircularProgressIndicator(
color: ColorsManager.primaryColor,
),
),
],
),
),
],
),
),
);
},
@@ -34,7 +34,9 @@ class SpaceDropdown extends StatelessWidget {
),
SizedBox(
child: Container(
height: 40,
decoration: BoxDecoration(
color: ColorsManager.whiteColors,
borderRadius: BorderRadius.circular(10),
),
child: DropdownButton2<String>(
@@ -45,7 +47,7 @@ class SpaceDropdown extends StatelessWidget {
value: space.uuid,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisAlignment: MainAxisAlignment.start,
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(
' ${space.name}',
@@ -88,7 +90,7 @@ class SpaceDropdown extends StatelessWidget {
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Expanded(
flex: 6,
flex: 8,
child: Padding(
padding: const EdgeInsets.only(left: 10),
child: Text(
@@ -129,6 +131,7 @@ class SpaceDropdown extends StatelessWidget {
dropdownStyleData: DropdownStyleData(
maxHeight: MediaQuery.of(context).size.height * 0.4,
decoration: BoxDecoration(
color: ColorsManager.whiteColors,
borderRadius: BorderRadius.circular(10),
),
),
@@ -4,6 +4,7 @@ import 'package:syncrow_web/pages/routines/bloc/routine_bloc/routine_bloc.dart';
import 'package:syncrow_web/pages/routines/models/device_functions.dart';
import 'package:syncrow_web/pages/routines/widgets/routine_dialogs/ac_dialog.dart';
import 'package:syncrow_web/pages/routines/widgets/routine_dialogs/ceiling_sensor/ceiling_sensor_helper.dart';
import 'package:syncrow_web/pages/routines/widgets/routine_dialogs/curtain_dialog.dart';
import 'package:syncrow_web/pages/routines/widgets/routine_dialogs/flush_presence_sensor/flush_presence_sensor.dart';
import 'package:syncrow_web/pages/routines/widgets/routine_dialogs/gateway/gateway_helper.dart';
import 'package:syncrow_web/pages/routines/widgets/routine_dialogs/one_gang_switch_dialog.dart';
@@ -26,7 +27,7 @@ class DeviceDialogHelper {
final result = await _getDialogForDeviceType(
dialogType: dialogType,
context: context,
productType: data['productType'],
productType: data['productType'] as String,
data: data,
functions: functions,
removeComparetors: removeComparetors,
@@ -65,7 +66,14 @@ class DeviceDialogHelper {
removeComparetors: removeComparetors,
dialogType: dialogType,
);
case 'CUR':
return CurtainHelper.showControlDialog(
dialogType: dialogType,
context: context,
functions: functions,
uniqueCustomId: data['uniqueCustomId'],
device: data['device'],
);
case '1G':
return OneGangSwitchHelper.showSwitchFunctionsDialog(
dialogType: dialogType,
@@ -17,9 +17,10 @@ class SaveRoutineHelper {
builder: (context) {
return BlocBuilder<RoutineBloc, RoutineState>(
builder: (context, state) {
final selectedConditionLabel = state.selectedAutomationOperator == 'and'
? 'All Conditions are met'
: 'Any Condition is met';
final selectedConditionLabel =
state.selectedAutomationOperator == 'and'
? 'All Conditions are met'
: 'Any Condition is met';
return AlertDialog(
contentPadding: EdgeInsets.zero,
@@ -37,10 +38,11 @@ class SaveRoutineHelper {
Text(
'Create a scene: ${state.routineName ?? ""}',
textAlign: TextAlign.center,
style: Theme.of(context).textTheme.headlineMedium!.copyWith(
color: ColorsManager.primaryColorWithOpacity,
fontWeight: FontWeight.bold,
),
style:
Theme.of(context).textTheme.headlineMedium!.copyWith(
color: ColorsManager.primaryColorWithOpacity,
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: 18),
_buildDivider(),
@@ -58,7 +60,8 @@ class SaveRoutineHelper {
_buildIfConditions(state, context),
Container(
width: 1,
color: ColorsManager.greyColor.withValues(alpha: 0.8),
color: ColorsManager.greyColor
.withValues(alpha: 0.8),
),
_buildThenActions(state, context),
],
@@ -97,7 +100,8 @@ class SaveRoutineHelper {
child: Row(
spacing: 16,
children: [
Expanded(child: Text('IF: $selectedConditionLabel', style: textStyle)),
Expanded(
child: Text('IF: $selectedConditionLabel', style: textStyle)),
const Expanded(child: Text('THEN:', style: textStyle)),
],
),
@@ -109,7 +113,7 @@ class SaveRoutineHelper {
spacing: 16,
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
DialogFooterButton(
DialogFooterButton(
text: 'Back',
onTap: () => Navigator.pop(context),
),
@@ -143,7 +147,8 @@ class SaveRoutineHelper {
child: ListView(
// shrinkWrap: true,
children: state.thenItems.map((item) {
final functions = state.selectedFunctions[item['uniqueCustomId']] ?? [];
final functions =
state.selectedFunctions[item['uniqueCustomId']] ?? [];
return functionRow(item, context, functions);
}).toList(),
),
@@ -203,19 +208,20 @@ class SaveRoutineHelper {
),
),
child: Center(
child: item['type'] == 'tap_to_run' || item['type'] == 'scene'
? Image.memory(
base64Decode(item['icon']),
width: 12,
height: 22,
fit: BoxFit.scaleDown,
)
: SvgPicture.asset(
item['imagePath'],
width: 12,
height: 12,
fit: BoxFit.scaleDown,
),
child:
item['type'] == 'tap_to_run' || item['type'] == 'scene'
? Image.memory(
base64Decode(item['icon']),
width: 12,
height: 22,
fit: BoxFit.scaleDown,
)
: SvgPicture.asset(
item['imagePath'],
width: 12,
height: 12,
fit: BoxFit.scaleDown,
),
),
),
Flexible(
@@ -0,0 +1,49 @@
import 'package:syncrow_web/pages/device_managment/curtain/model/curtain_model.dart';
import 'package:syncrow_web/pages/routines/models/curtain/curtain_opertion_value.dart';
import 'package:syncrow_web/pages/routines/models/device_functions.dart'
show DeviceFunction;
import 'package:syncrow_web/utils/constants/app_enum.dart';
import 'package:syncrow_web/utils/constants/assets.dart';
abstract class CurtainFunction extends DeviceFunction<CurtainModel> {
final String type;
CurtainFunction({
required super.deviceId,
required super.deviceName,
required this.type,
required super.code,
required super.operationName,
required super.icon,
});
List<CurtainOperationalValue> getOperationalValues();
}
class ControlCurtainFunction extends CurtainFunction {
ControlCurtainFunction({
required super.deviceId,
required super.deviceName,
required super.type,
super.code = 'control',
super.operationName = 'Control',
super.icon = Assets.curtain,
});
@override
List<CurtainOperationalValue> getOperationalValues() => [
CurtainOperationalValue(
icon: Assets.curtain,
description: 'OPEN',
value: 'open',
),
CurtainOperationalValue(
icon: Assets.curtain,
description: 'STOP',
value: 'stop',
),
CurtainOperationalValue(
icon: Assets.curtain,
description: 'CLOSE',
value: 'close',
)
];
}
@@ -0,0 +1,11 @@
class CurtainOperationalValue {
final String icon;
final String description;
final String value;
CurtainOperationalValue({
required this.icon,
required this.description,
required this.value,
});
}
@@ -405,8 +405,8 @@ class PowerFactorCStatusFunction extends EnergyClampFunctions {
code: 'PowerFactorC',
operationName: 'Power Factor C',
icon: Assets.speedoMeter,
min: 0.00,
max: 1.00,
min: 0.0,
max: 1.0,
step: 0.1,
unit: "",
);
@@ -148,6 +148,7 @@ class IfContainer extends StatelessWidget {
'NCPS',
'WH',
'PC',
'CUR',
].contains(mutableData['productType'])) {
context
.read<RoutineBloc>()
@@ -28,6 +28,7 @@ class _RoutineDevicesState extends State<RoutineDevices> {
'NCPS',
'WH',
'PC',
'CUR',
};
@override
@@ -117,10 +117,22 @@ class ACHelper {
},
onConfirm: state.addedFunctions.isNotEmpty
? () {
final selectedFunctionData =
state.addedFunctions.firstWhere(
(f) =>
f.functionCode == state.selectedFunction,
orElse: () => DeviceFunctionData(
entityId: '',
functionCode: state.selectedFunction ?? '',
operationName: '',
value: null,
),
);
/// add the functions to the routine bloc
context.read<RoutineBloc>().add(
AddFunctionToRoutine(
state.addedFunctions,
[selectedFunctionData],
uniqueCustomId,
),
);
@@ -78,12 +78,22 @@ class _CeilingSensorDialogState extends State<CeilingSensorDialog> {
onCancel: () => Navigator.pop(context),
onConfirm: state.addedFunctions.isNotEmpty
? () {
final selectedFunctionData =
state.addedFunctions.firstWhere(
(f) => f.functionCode == state.selectedFunction,
orElse: () => DeviceFunctionData(
entityId: '',
functionCode: state.selectedFunction ?? '',
operationName: '',
value: null,
),
);
final functions = _updateValuesForAddedFunctions(
state.addedFunctions,
);
context.read<RoutineBloc>().add(
AddFunctionToRoutine(
functions,
[selectedFunctionData],
'${widget.uniqueCustomId}',
),
);
@@ -0,0 +1,270 @@
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_svg/svg.dart';
import 'package:syncrow_web/pages/device_managment/all_devices/models/devices_model.dart';
import 'package:syncrow_web/pages/routines/bloc/functions_bloc/functions_bloc_bloc.dart';
import 'package:syncrow_web/pages/routines/bloc/routine_bloc/routine_bloc.dart';
import 'package:syncrow_web/pages/routines/models/curtain/curtain_function.dart';
import 'package:syncrow_web/pages/routines/models/curtain/curtain_opertion_value.dart';
import 'package:syncrow_web/pages/routines/models/device_functions.dart';
import 'package:syncrow_web/pages/routines/widgets/dialog_footer.dart';
import 'package:syncrow_web/pages/routines/widgets/dialog_header.dart';
import 'package:syncrow_web/pages/routines/widgets/routine_dialogs/helpers/routine_tap_function_helper.dart';
import 'package:syncrow_web/utils/color_manager.dart';
import 'package:syncrow_web/utils/extension/build_context_x.dart';
class CurtainHelper {
static Future<Map<String, dynamic>?> showControlDialog({
required String dialogType,
required BuildContext context,
required List<DeviceFunction> functions,
required String uniqueCustomId,
required AllDevicesModel? device,
}) async {
List<ControlCurtainFunction> curtainFunctions =
functions.whereType<ControlCurtainFunction>().where((function) {
if (dialogType == 'THEN') {
return function.type == 'THEN' || function.type == 'BOTH';
}
return function.type == 'IF' || function.type == 'BOTH';
}).toList();
return showDialog<Map<String, dynamic>?>(
context: context,
builder: (context) => BlocProvider(
create: (_) => FunctionBloc()..add(const InitializeFunctions([])),
child: AlertDialog(
contentPadding: EdgeInsets.zero,
content: BlocBuilder<FunctionBloc, FunctionBlocState>(
builder: (context, state) {
final selectedFunction = state.selectedFunction;
final selectedOperationName = state.selectedOperationName;
final selectedFunctionData = state.addedFunctions
.firstWhere((f) => f.functionCode == selectedFunction,
orElse: () => DeviceFunctionData(
entityId: '',
functionCode: selectedFunction ?? '',
operationName: '',
value: null,
));
return Container(
width: selectedFunction != null ? 600 : 360,
height: 450,
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(20),
),
padding: const EdgeInsets.only(top: 20),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
const DialogHeader('AC Functions'),
Expanded(
child: Row(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
// Function list
SizedBox(
width: selectedFunction != null ? 320 : 360,
child: _buildFunctionsList(
context: context,
curtainFunctions: curtainFunctions,
onFunctionSelected:
(functionCode, operationName) {
RoutineTapFunctionHelper.onTapFunction(
context,
functionCode: functionCode,
functionOperationName: operationName,
functionValueDescription:
selectedFunctionData.valueDescription,
deviceUuid: device?.uuid,
codesToAddIntoFunctionsWithDefaultValue: [
'temp_set',
'temp_current',
],
defaultValue: 0);
}),
),
// Value selector
if (selectedFunction != null)
Expanded(
child: _buildValueSelector(
context: context,
selectedFunction: selectedFunction,
selectedFunctionData: selectedFunctionData,
controlFunctions: curtainFunctions,
device: device,
operationName: selectedOperationName ?? '',
),
),
],
),
),
DialogFooter(
onCancel: () {
Navigator.pop(context);
},
onConfirm: state.addedFunctions.isNotEmpty
? () {
/// add the functions to the routine bloc
context.read<RoutineBloc>().add(
AddFunctionToRoutine(
state.addedFunctions,
uniqueCustomId,
),
);
// Return the device data to be added to the container
Navigator.pop(context, {
'deviceId': functions.first.deviceId,
});
}
: null,
isConfirmEnabled: selectedFunction != null,
),
],
),
);
},
),
),
),
).then((value) {
return value;
});
}
static Widget _buildFunctionsList({
required BuildContext context,
required List<ControlCurtainFunction> curtainFunctions,
required Function(String, String) onFunctionSelected,
}) {
return ListView.separated(
shrinkWrap: false,
physics: const AlwaysScrollableScrollPhysics(),
itemCount: curtainFunctions.length,
separatorBuilder: (context, index) => const Padding(
padding: EdgeInsets.symmetric(horizontal: 40.0),
child: Divider(
color: ColorsManager.dividerColor,
),
),
itemBuilder: (context, index) {
final function = curtainFunctions[index];
return ListTile(
leading: SvgPicture.asset(
function.icon,
width: 24,
height: 24,
placeholderBuilder: (BuildContext context) => Container(
width: 24,
height: 24,
color: Colors.transparent,
),
),
title: Text(
function.operationName,
style: context.textTheme.bodyMedium,
),
trailing: const Icon(
Icons.arrow_forward_ios,
size: 16,
color: ColorsManager.textGray,
),
onTap: () => onFunctionSelected(
function.code,
function.operationName,
),
);
},
);
}
static Widget _buildValueSelector({
required BuildContext context,
required String selectedFunction,
required DeviceFunctionData? selectedFunctionData,
required List<ControlCurtainFunction> controlFunctions,
AllDevicesModel? device,
required String operationName,
}) {
final selectedFn =
controlFunctions.firstWhere((f) => f.code == selectedFunction);
// Rest of your existing code for other value selectors
final values = selectedFn.getOperationalValues();
return _buildOperationalValuesList(
context: context,
values: values,
selectedValue: selectedFunctionData?.value,
device: device,
operationName: operationName,
selectCode: selectedFunction,
selectedFunctionData: selectedFunctionData,
);
}
static Widget _buildOperationalValuesList({
required BuildContext context,
required List<CurtainOperationalValue> values,
required dynamic selectedValue,
AllDevicesModel? device,
required String operationName,
required String selectCode,
DeviceFunctionData? selectedFunctionData,
// required Function(dynamic) onValueChanged,
}) {
return ListView.builder(
shrinkWrap: false,
physics: const AlwaysScrollableScrollPhysics(),
itemCount: values.length,
itemBuilder: (context, index) {
final value = values[index];
final isSelected = selectedValue == value.value;
return ListTile(
leading: SvgPicture.asset(
value.icon,
width: 24,
height: 24,
placeholderBuilder: (BuildContext context) => Container(
width: 24,
height: 24,
color: Colors.transparent,
),
),
title: Text(
value.description,
style: context.textTheme.bodyMedium,
),
trailing: Icon(
isSelected
? Icons.radio_button_checked
: Icons.radio_button_unchecked,
size: 24,
color: isSelected
? ColorsManager.primaryColorWithOpacity
: ColorsManager.textGray,
),
onTap: () {
if (!isSelected) {
context.read<FunctionBloc>().add(
AddFunction(
functionData: DeviceFunctionData(
entityId: device?.uuid ?? '',
functionCode: selectCode,
operationName: operationName,
value: value.value,
condition: selectedFunctionData?.condition,
valueDescription:
selectedFunctionData?.valueDescription,
),
),
);
}
},
);
},
);
}
}
@@ -192,9 +192,18 @@ class _WallPresenceSensorState extends State<FlushPresenceSensor> {
onCancel: () => Navigator.pop(context),
onConfirm: state.addedFunctions.isNotEmpty
? () {
final selectedFunctionData = state.addedFunctions.firstWhere(
(f) => f.functionCode == state.selectedFunction,
orElse: () => DeviceFunctionData(
entityId: '',
functionCode: state.selectedFunction ?? '',
operationName: '',
value: null,
),
);
context.read<RoutineBloc>().add(
AddFunctionToRoutine(
state.addedFunctions,
[selectedFunctionData],
widget.uniqueCustomId!,
),
);
@@ -115,9 +115,18 @@ class _GatewayDialogState extends State<GatewayDialog> {
onCancel: () => Navigator.pop(context),
onConfirm: state.addedFunctions.isNotEmpty
? () {
final selectedFunctionData = state.addedFunctions.firstWhere(
(f) => f.functionCode == state.selectedFunction,
orElse: () => DeviceFunctionData(
entityId: '',
functionCode: state.selectedFunction ?? '',
operationName: '',
value: null,
),
);
context.read<RoutineBloc>().add(
AddFunctionToRoutine(
state.addedFunctions,
[selectedFunctionData],
widget.uniqueCustomId ?? '-1',
),
);
@@ -147,7 +147,7 @@ class OneGangSwitchHelper {
// }
context.read<RoutineBloc>().add(
AddFunctionToRoutine(
state.addedFunctions,
[selectedFunctionData],
uniqueCustomId,
),
);
@@ -250,9 +250,18 @@ class _EnergyClampDialogState extends State<EnergyClampDialog> {
onCancel: () => Navigator.pop(context),
onConfirm: state.addedFunctions.isNotEmpty
? () {
final selectedFunctionData = state.addedFunctions.firstWhere(
(f) => f.functionCode == state.selectedFunction,
orElse: () => DeviceFunctionData(
entityId: '',
functionCode: state.selectedFunction ?? '',
operationName: '',
value: null,
),
);
context.read<RoutineBloc>().add(
AddFunctionToRoutine(
state.addedFunctions,
[selectedFunctionData],
widget.uniqueCustomId!,
),
);
@@ -27,17 +27,16 @@ class EnergyValueSelectorWidget extends StatelessWidget {
@override
Widget build(BuildContext context) {
final selectedFn =
functions.firstWhere((f) => f.code == selectedFunction);
final selectedFn = functions.firstWhere((f) => f.code == selectedFunction);
final values = selectedFn.getOperationalValues();
final step = selectedFn.step ?? 1.0;
final step = selectedFn.step;
final _unit = selectedFn.unit ?? '';
final (double, double) sliderRange =
(selectedFn.min ?? 0.0, selectedFn.max ?? 100.0);
if (_isSliderFunction(selectedFunction)) {
return CustomRoutinesTextbox(
withSpecialChar: false,
withSpecialChar: true,
currentCondition: functionData.condition,
dialogType: dialogType,
sliderRange: sliderRange,
@@ -60,14 +59,14 @@ class EnergyValueSelectorWidget extends StatelessWidget {
entityId: device?.uuid ?? '',
functionCode: selectedFunction,
operationName: functionData.operationName,
value: value.toInt(),
value: value,
condition: functionData.condition,
),
),
),
unit: _unit,
dividendOfRange: 1,
stepIncreaseAmount: step,
stepIncreaseAmount: step!,
);
}
@@ -145,9 +145,22 @@ class TwoGangSwitchHelper {
// ),
// );
// }
final selectedFunctionData =
state.addedFunctions.firstWhere(
(f) =>
f.functionCode ==
state.selectedFunction,
orElse: () => DeviceFunctionData(
entityId: '',
functionCode:
state.selectedFunction ?? '',
operationName: '',
value: null,
),
);
context.read<RoutineBloc>().add(
AddFunctionToRoutine(
state.addedFunctions,
[selectedFunctionData],
uniqueCustomId,
),
);
@@ -210,9 +210,18 @@ class _WallPresenceSensorState extends State<WallPresenceSensor> {
onCancel: () => Navigator.pop(context),
onConfirm: state.addedFunctions.isNotEmpty
? () {
final selectedFunctionData = state.addedFunctions.firstWhere(
(f) => f.functionCode == state.selectedFunction,
orElse: () => DeviceFunctionData(
entityId: '',
functionCode: state.selectedFunction ?? '',
operationName: '',
value: null,
),
);
context.read<RoutineBloc>().add(
AddFunctionToRoutine(
state.addedFunctions,
[selectedFunctionData],
widget.uniqueCustomId!,
),
);
@@ -188,9 +188,18 @@ class _WaterHeaterDialogRoutinesState extends State<WaterHeaterDialogRoutines> {
onCancel: () => Navigator.pop(context),
onConfirm: state.addedFunctions.isNotEmpty
? () {
final selectedFunctionData = state.addedFunctions.firstWhere(
(f) => f.functionCode == state.selectedFunction,
orElse: () => DeviceFunctionData(
entityId: '',
functionCode: state.selectedFunction ?? '',
operationName: '',
value: null,
),
);
context.read<RoutineBloc>().add(
AddFunctionToRoutine(
state.addedFunctions,
[selectedFunctionData],
widget.uniqueCustomId!,
),
);

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