Compare commits

..

131 Commits

Author SHA1 Message Date
932e99c0a9 Add build script and update package dependencies for CDK deployment 2025-07-09 05:27:46 -06:00
99b13ee062 Adds CDK deployment instructions and updates region
Enhances the README with CDK deployment commands.
2025-07-07 09:38:09 +03:00
337e79b770 a working web stack hosted at app.syncrow.me 2025-06-29 20:47:00 -04:00
774a1533f5 Dev (#314)
<!--
  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

Release


## 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-29 16:47:14 +03:00
0c220a1f34 [FE] UI Enhancement: Update Confirmation Dialog on "Create Visitor Password" Flow (#310)
… the requested ticket)

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

## Description
enhance UI in create visitor insure dialog as wanted in Ticket (in figma
not updated yet)

## 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-1660]:
https://syncrow.atlassian.net/browse/SP-1660?atlOrigin=eyJpIjoiNWRkNTljNzYxNjVmNDY3MDlhMDU5Y2ZhYzA5YTRkZjUiLCJwIjoiZ2l0aHViLWNvbS1KU1cifQ
2025-06-29 16:40:15 +03:00
a526fcbeee Merge branch 'dev' into SP-1660-fe-ui-enhancement-update-confirmation-dialog-on-create-visitor-password-flow 2025-06-29 16:10:43 +03:00
172e1d208a [FE] Preferences & Calibration (#312)
<!--
  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-1707](https://syncrow.atlassian.net/browse/SP-1707)

## Description

fix UI  like in figma

## 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-1707]:
https://syncrow.atlassian.net/browse/SP-1707?atlOrigin=eyJpIjoiNWRkNTljNzYxNjVmNDY3MDlhMDU5Y2ZhYzA5YTRkZjUiLCJwIjoiZ2l0aHViLWNvbS1KU1cifQ
2025-06-29 16:10:00 +03:00
2c254c1a91 Sp 1771 fe device name and subspace changes not reflected immediately after update on device management page (#313)
<!--
  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-1771](https://syncrow.atlassian.net/browse/SP-1771)

## Description

Synced state between settings and devices 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-1771]:
https://syncrow.atlassian.net/browse/SP-1771?atlOrigin=eyJpIjoiNWRkNTljNzYxNjVmNDY3MDlhMDU5Y2ZhYzA5YTRkZjUiLCJwIjoiZ2l0aHViLWNvbS1KU1cifQ
2025-06-29 16:09:37 +03:00
480e183b91 Merge branch 'dev' of https://github.com/SyncrowIOT/web into SP-1771-FE-Device-name-and-subspace-changes-not-reflected-immediately-after-update-on-Device-Management-page 2025-06-29 16:02:24 +03:00
d8bb234537 SP-1771 2025-06-29 16:00:15 +03:00
354d61dfa2 UI Enhancement 2025-06-29 15:50:37 +03:00
8916efcebb fixed aqi filter bugs. 2025-06-29 15:39:30 +03:00
175d1e662b Revert "Sp 1589 fe when user navigates to devices page the devices ar… (#311)
…e already listed although no community is selected also when we select
a community the api is being called repeatedly too many times (#305)"

This reverts commit 034a5ef908, reversing
changes made to b97183fb61.

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

## Description

revert sp:1589 cuz have problems in routin

## 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 


[SP-1589]:
https://syncrow.atlassian.net/browse/SP-1589?atlOrigin=eyJpIjoiNWRkNTljNzYxNjVmNDY3MDlhMDU5Y2ZhYzA5YTRkZjUiLCJwIjoiZ2l0aHViLWNvbS1KU1cifQ
2025-06-29 15:26:29 +03:00
57bd4b8527 Revert "Sp 1589 fe when user navigates to devices page the devices are already listed although no community is selected also when we select a community the api is being called repeatedly too many times (#305)"
This reverts commit 034a5ef908, reversing
changes made to b97183fb61.
2025-06-29 14:45:54 +03:00
df308fd12a Merge branch 'dev' of https://github.com/SyncrowIOT/web into SP-1771-FE-Device-name-and-subspace-changes-not-reflected-immediately-after-update-on-Device-Management-page 2025-06-29 14:14:00 +03:00
e0cfe541dd name changes in table when changed. 2025-06-29 14:13:25 +03:00
814cbf787f edit the UI as wanted in ticket (note: in figma is not updated yet to the requested ticket) 2025-06-29 13:58:57 +03:00
df8eff895e [FE] Create Scheduling UI (#309)
and funtion name in dialog was olways keep close now it is really take
the real value

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

## Description
in curtain Module
fix edit issue and insure function name in  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-1705]:
https://syncrow.atlassian.net/browse/SP-1705?atlOrigin=eyJpIjoiNWRkNTljNzYxNjVmNDY3MDlhMDU5Y2ZhYzA5YTRkZjUiLCJwIjoiZ2l0aHViLWNvbS1KU1cifQ
2025-06-29 13:14:08 +03:00
9514200892 sp:1728 [FE] Build Curtain Dialog Component (#307)
<!--
  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

change the name  to curtain functions and conditions

## 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-1728]:
https://syncrow.atlassian.net/browse/SP-1728?atlOrigin=eyJpIjoiNWRkNTljNzYxNjVmNDY3MDlhMDU5Y2ZhYzA5YTRkZjUiLCJwIjoiZ2l0aHViLWNvbS1KU1cifQ
2025-06-29 13:13:47 +03:00
cf4bfc41f6 [FE] Disable AC Control Button When AC is Off (#308)
<!--
  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

fix bug to dont stack snackbars

## 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-29 13:11:36 +03:00
01f55c14de Add update events for device and subspace names
implement copyWith methods in models
2025-06-29 13:03:33 +03:00
19cdd371f8 fix edit problem
and funtion name in dialog was olways keep close now it is really take the real value
2025-06-29 12:43:23 +03:00
388391eec4 stop stacking snackbars 2025-06-29 11:29:43 +03:00
23cfee1490 fix curtain name in curtain if then containers dialogs 2025-06-29 11:12:28 +03:00
403b45c826 show aqi tab on analytics page. 2025-06-29 10:58:16 +03:00
ed2b91d380 MVP release (#306)
<!--
  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

MVP Release

## 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-29 10:56:48 +03:00
6dd3329288 Sp 1703 fe build device overview page curtain module (#303)
<!--
  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-1703](https://syncrow.atlassian.net/browse/SP-1703)
[SP-1704](https://syncrow.atlassian.net/browse/SP-1704)
[SP-1706](https://syncrow.atlassian.net/browse/SP-1706)
[SP-1705](https://syncrow.atlassian.net/browse/SP-1705)
[SP-1707](https://syncrow.atlassian.net/browse/SP-1707)

## Description

all about curtain module (UI + logic + integrate with API +
control/batch)
all is ready

## 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-1703]:
https://syncrow.atlassian.net/browse/SP-1703?atlOrigin=eyJpIjoiNWRkNTljNzYxNjVmNDY3MDlhMDU5Y2ZhYzA5YTRkZjUiLCJwIjoiZ2l0aHViLWNvbS1KU1cifQ
[SP-1704]:
https://syncrow.atlassian.net/browse/SP-1704?atlOrigin=eyJpIjoiNWRkNTljNzYxNjVmNDY3MDlhMDU5Y2ZhYzA5YTRkZjUiLCJwIjoiZ2l0aHViLWNvbS1KU1cifQ
[SP-1706]:
https://syncrow.atlassian.net/browse/SP-1706?atlOrigin=eyJpIjoiNWRkNTljNzYxNjVmNDY3MDlhMDU5Y2ZhYzA5YTRkZjUiLCJwIjoiZ2l0aHViLWNvbS1KU1cifQ
[SP-1705]:
https://syncrow.atlassian.net/browse/SP-1705?atlOrigin=eyJpIjoiNWRkNTljNzYxNjVmNDY3MDlhMDU5Y2ZhYzA5YTRkZjUiLCJwIjoiZ2l0aHViLWNvbS1KU1cifQ
[SP-1707]:
https://syncrow.atlassian.net/browse/SP-1707?atlOrigin=eyJpIjoiNWRkNTljNzYxNjVmNDY3MDlhMDU5Y2ZhYzA5YTRkZjUiLCJwIjoiZ2l0aHViLWNvbS1KU1cifQ
2025-06-29 10:54:14 +03:00
d82a050422 Merge branch 'SP-1703-fe-build-device-overview-page_curtain_module' of https://github.com/SyncrowIOT/web into SP-1703-fe-build-device-overview-page_curtain_module 2025-06-29 10:50:57 +03:00
a1562110d5 add close open if it is curtain module for schdule 2025-06-29 10:50:51 +03:00
46aa5e2ddb Merge branch 'SP-1703-fe-build-device-overview-page_curtain_module' of https://github.com/SyncrowIOT/web into SP-1703-fe-build-device-overview-page_curtain_module 2025-06-29 10:49:04 +03:00
ec1bb5b609 added curtain icons. 2025-06-29 10:49:00 +03:00
5827ba4296 Merge branch 'SP-1703-fe-build-device-overview-page_curtain_module' of https://github.com/SyncrowIOT/web into SP-1703-fe-build-device-overview-page_curtain_module 2025-06-29 10:42:34 +03:00
b96f65d2c2 fix the open close states when curtain module 2025-06-29 10:42:18 +03:00
034a5ef908 Sp 1589 fe when user navigates to devices page the devices are already listed although no community is selected also when we select a community the api is being called repeatedly too many times (#305)
<!--
  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-1589](https://syncrow.atlassian.net/browse/SP-1589)

## Description

[SP-1589]now only one API made with needed filters

## 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-1589]:
https://syncrow.atlassian.net/browse/SP-1589?atlOrigin=eyJpIjoiNWRkNTljNzYxNjVmNDY3MDlhMDU5Y2ZhYzA5YTRkZjUiLCJwIjoiZ2l0aHViLWNvbS1KU1cifQ
[SP-1589]:
https://syncrow.atlassian.net/browse/SP-1589?atlOrigin=eyJpIjoiNWRkNTljNzYxNjVmNDY3MDlhMDU5Y2ZhYzA5YTRkZjUiLCJwIjoiZ2l0aHViLWNvbS1KU1cifQ
2025-06-29 09:46:17 +03:00
c198047165 Merge branch 'dev' into SP-1703-fe-build-device-overview-page_curtain_module 2025-06-29 09:23:40 +03:00
1828ffb87a remove print statment 2025-06-29 09:17:57 +03:00
bd53388438 make one API with new QP to filter on spacesId 2025-06-29 09:17:22 +03:00
b97183fb61 SP-1801 2025-06-28 15:47:42 +04:00
07dfe6b206 Merge branch 'dev' of https://github.com/SyncrowIOT/web into dev 2025-06-28 13:16:16 +03:00
c4fd90b3bc testing heatmap fixes. 2025-06-28 13:16:06 +03:00
bbcb947313 uses UTC dates as an attempt to fix heat-map's rendering bug. (#304)
<!--
  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

Uses UTC dates as an attempt to fix heatmap's rendering bug 

## 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-27 22:15:05 +03:00
13e9a808ab uses UTC dates as an attempt to fix heatmap's rendering bug. 2025-06-27 22:09:00 +03:00
32208c1e81 send code and value to schdual from curtain module 2025-06-27 17:57:04 +03:00
1d95915f57 fix the string for motor without underscore 2025-06-27 17:56:41 +03:00
e365aa3faa edite event and block of schdual to accept code and functionOn as dynamic 2025-06-27 17:56:12 +03:00
26e8ff7ee2 use dynamic instead of bool to accept mant types and fix schedual view to accept curtain code and value 2025-06-27 17:55:27 +03:00
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
0c0bf96c07 add bloc builder to use the context 2025-06-27 17:16:41 +03:00
4744009cb6 Update TotalEnergyConsumptionChart to adjust Y-axis limits and intervals for better data representation 2025-06-27 17:11:17 +03:00
1a4ced195a Merge branch 'dev' of https://github.com/SyncrowIOT/web into SP-1703-fe-build-device-overview-page_curtain_module 2025-06-27 16:55:35 +03:00
812c51400b add listener to batch 2025-06-27 16:53:40 +03:00
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
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
ca41aa6224 all dates in heatmap are utc. 2025-06-27 16:37:09 +03:00
396ce3dad8 now batch is working 2025-06-27 16:26:39 +03:00
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
475462301f manually parse event date for heatmap date object. 2025-06-27 15:29:11 +03:00
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
7fda564ee4 hotfixes. 2025-06-27 14:07:24 +03:00
11e2853403 Enhance AnalyticsDeviceDropdown to show loading indicator during loading state. 2025-06-27 11:56:43 +03:00
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
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
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
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
f43826a824 now it is rendering the chages for motors and control back 2025-06-25 17:07:59 +03:00
0b372e1ed8 use read instead of watch 2025-06-25 16:49:11 +03:00
4e9bcbdcea build UI and integrate with back 2025-06-25 16:28:15 +03:00
eee6a80c50 add events and states and models 2025-06-25 16:27:40 +03:00
03ba506294 add bloc nd logic 2025-06-25 16:27:15 +03:00
6c268754a9 add icons and the basic route to show curtain module 2025-06-25 16:26:57 +03:00
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
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
3c9494963d Add generated configuration files for Flutter integration across platforms 2025-06-25 15:58:58 +03:00
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
f38ac58442 Add bloc closure handling and improve device status updates in AcBloc 2025-06-25 14:45:10 +03:00
30e940fdfc Reads the correct date to load aqi data. 2025-06-25 14:34:23 +03:00
520b73717a Doesnt load devices when date changes. 2025-06-25 14:34:07 +03:00
e1bb67d7bd reads correct value for TVOC. 2025-06-25 14:26:05 +03:00
5e0df09cb6 Changed tvoc unit to match the device. 2025-06-25 14:25:53 +03:00
22070ca04a removed unused comment. 2025-06-25 13:26:07 +03:00
6d667af7dc increased size of OccupancyEndSideBar in medium sized screens. 2025-06-25 13:25:46 +03:00
3b4952db0a fixed thrown exceptions inAnalyticsDevice. 2025-06-25 13:25:30 +03:00
5f59583696 Supported NCPS device type in occupancy devices dropdown. 2025-06-25 13:25:12 +03:00
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
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
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
562c67a958 Add deviceName field to FailedOperation and SuccessOperation models 2025-06-25 12:24:09 +03:00
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
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
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
c649044a1f Enhance navigation buttons in SmartPowerDeviceControl for better user experience 2025-06-24 16:40:42 +03:00
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
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
c6e98fa245 Refactor visitor password dialog navigation to return specific values on pop 2025-06-24 16:06:53 +03:00
277a9ce4f0 Add countdown seconds to schedule management 2025-06-24 15:38:16 +03:00
f901983aa5 Implemented ranges for the values in the AQI chart based on the selected pollutant. 2025-06-24 15:01:25 +03:00
010403f1fa Added day of month axis name to all charts. 2025-06-24 14:50:22 +03:00
ee1ebeae2e Changed energy management charts titles for a more clear name. 2025-06-24 14:45:15 +03:00
6e6ef79ed0 enhanced heat map tooltip's message. 2025-06-24 14:44:56 +03:00
7e5825de45 Fixed typo in occupancy sidebar. 2025-06-24 14:44:45 +03:00
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
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
2a2fb7ffca Add responsive input fields and radio groups for visitor password setup 2025-06-24 11:36:50 +03:00
df34ded153 Add responsive input fields and radio groups for visitor password setup 2025-06-24 11:35:03 +03:00
5a2299ea2f navigates to initial create space dialog from the respective buttons. 2025-06-24 10:47:48 +03:00
90f8305aa1 shows tooltip on SpaceCell. 2025-06-24 10:45:13 +03:00
329b2ba472 selects the space from and to connection when selecting a space. 2025-06-24 10:36:13 +03:00
0fb9149613 Selects all children of a space when selecting a parent. 2025-06-24 10:30:56 +03:00
87b45fff1d removed expanded widget that caused a size exception. 2025-06-24 10:21:25 +03:00
95ae50d12d navigates to selected space when changed on sidebar in space management canvas. 2025-06-24 10:16:03 +03:00
95d6e1ecda if online go green with online status else red with offline status 2025-06-23 16:33:45 +03:00
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
75efc595b4 reverted to old import to avoid confusion with QA team. 2025-06-23 16:22:11 +03:00
ad00cf35ba added the PR notes 2025-06-23 16:05:16 +03:00
5276f4186c emit error state when catch error and send the real API exception 2025-06-23 15:55:56 +03:00
8bc7a3daa2 Implemented space management canvas. 2025-06-23 15:45:49 +03:00
1200a809c2 now cant use offline device to controll 2025-06-23 14:33:56 +03:00
ada7daf179 Switched from using Text to SelectableText in CreateCommunityDialog. 2025-06-23 10:13:30 +03:00
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
f8e4c89cdb uses correct error message that the api sends in RemoteCreateCommunityService. 2025-06-23 10:11:03 +03:00
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
a78b5993a9 Created SpaceManagementTemplatesView widget. 2025-06-23 10:05:53 +03:00
0e7109a19e Created CommunityTemplateCell widget. 2025-06-23 10:02:15 +03:00
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
5bb3688a51 Dev (#276)
<!--
  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:26:43 +03:00
d6fcf051c6 Dev (#273)
<!--
  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
remove duplicate Feature from space Management  

## Description

no need for feature of duplicate now

## 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:25:51 +03:00
5544430efa Dev (#270)
<!--
  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

Includes a few critical hot fixes.

## 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-1661]:
https://syncrow.atlassian.net/browse/SP-1661?atlOrigin=eyJpIjoiNWRkNTljNzYxNjVmNDY3MDlhMDU5Y2ZhYzA5YTRkZjUiLCJwIjoiZ2l0aHViLWNvbS1KU1cifQ
2025-06-18 16:02:53 +03:00
ae89a24a4b Dev (#265)
<!--
  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: -->

- [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-18 11:01:09 +03:00
ecc01a1eb3 hides air quality tab. (#262)
<!--
  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

Hides AQI tab in analytics dashboard for release purposes.

## 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-17 13:30:36 +03:00
921da20d3f hides air quality tab. 2025-06-17 13:28:22 +03:00
2fed2d9de3 Sprint 20 STG Release (#261)
<!--
  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

Sprint 20 staging release.
## 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-17 13:26:01 +03:00
114 changed files with 5122 additions and 569 deletions

View File

@ -1,2 +1,2 @@
ENV_NAME=production
BASE_URL=https://syncrow-staging.azurewebsites.net
BASE_URL=https://api.syncrow.me

4
.gitignore vendored
View File

@ -42,3 +42,7 @@ app.*.map.json
/android/app/debug
/android/app/profile
/android/app/release
node_modules
cdk.out
web-cdk.out

View File

@ -24,4 +24,13 @@ samples, guidance on mobile development, and a full API reference.
- run command: `flutter run -d chrome --target=lib/main_dev.dart`
## CDK Deployment
• Bootstrap CDK (first time only): npx cdk bootstrap aws://482311766496/me-central-1
• List available stacks: npx cdk list
• Deploy web: npx cdk deploy --require-approval never
• View changes before deploy: npx cdk diff
• Generate CloudFormation template: npx cdk synth
• Destroy infrastructure: npx cdk destroy
• Web infrastructure is configured in infrastructure/web-stack.ts
• After code changes: build Flutter web app and deploy to hosting service

View File

@ -0,0 +1,8 @@
<svg width="23" height="13" viewBox="0 0 23 13" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M1.24512 2.00263V11L1.90308 11.278L7.5311 6.94877C7.82484 6.72277 7.82484 6.27987 7.5311 6.05388L1.90308 1.72461L1.24512 2.00263Z" fill="#023DFE" fill-opacity="0.7"/>
<path d="M1.90344 1.7231L1.68312 1.55364C1.31186 1.2681 0.774414 1.53272 0.774414 2.00105V10.9984C0.774414 11.4668 1.31186 11.7315 1.68312 11.4459L1.90344 11.2764V1.7231Z" fill="#023DFE"/>
<path d="M12.0646 0.855469H11.5001C11.1883 0.855469 10.9355 1.10819 10.9355 1.41998V11.5813H12.0646C12.3764 11.5813 12.6291 11.3285 12.6291 11.0167V1.41998C12.6291 1.10826 12.3764 0.855469 12.0646 0.855469Z" fill="#023DFE" fill-opacity="0.7"/>
<path d="M12.6291 11.0168C12.0056 11.0168 11.5001 10.5113 11.5001 9.88779V0.855469H10.9356C10.6238 0.855469 10.3711 1.10819 10.3711 1.41998V11.5813C10.3711 11.893 10.6238 12.1458 10.9356 12.1458H12.0646C12.3764 12.1458 12.6291 11.893 12.6291 11.5813V11.0168Z" fill="#023DFE"/>
<path d="M21.4247 2.01953L16.1094 6.50343L21.4247 11.1061L22.226 10.7315V2.27062L21.4247 2.01953Z" fill="#023DFE" fill-opacity="0.7"/>
<path d="M17.3084 6.94723C17.0147 6.7213 17.0147 6.27833 17.3084 6.05233L22.2263 2.26933V2.00108C22.2263 1.53275 21.6889 1.26807 21.3176 1.55367L15.4693 6.05233C15.1756 6.27833 15.1756 6.7213 15.4693 6.94723L21.3176 11.4459C21.6889 11.7314 22.2263 11.4668 22.2263 10.9985V10.7302L17.3084 6.94723Z" fill="#023DFE"/>
</svg>

After

Width:  |  Height:  |  Size: 1.4 KiB

View File

@ -0,0 +1,8 @@
<svg width="22" height="12" viewBox="0 0 22 12" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M15.2227 1.27411V10.2715L15.8806 10.5495L21.5086 6.22025C21.8024 5.99426 21.8024 5.55136 21.5086 5.32536L15.8806 0.996094L15.2227 1.27411Z" fill="#023DFE" fill-opacity="0.7"/>
<path d="M15.881 0.994589L15.6607 0.825126C15.2894 0.539589 14.752 0.804208 14.752 1.27254V10.2699C14.752 10.7383 15.2894 11.0029 15.6607 10.7173L15.881 10.5479V0.994589Z" fill="#023DFE"/>
<path d="M12.0646 0.128906H11.5001C11.1883 0.128906 10.9355 0.381631 10.9355 0.693418V10.8547H12.0646C12.3764 10.8547 12.6291 10.602 12.6291 10.2902V0.693418C12.6291 0.381699 12.3764 0.128906 12.0646 0.128906Z" fill="#023DFE" fill-opacity="0.7"/>
<path d="M12.6291 10.2903C12.0056 10.2903 11.5001 9.78474 11.5001 9.16123V0.128906H10.9356C10.6238 0.128906 10.3711 0.381631 10.3711 0.693418V10.8547C10.3711 11.1665 10.6238 11.4192 10.9356 11.4192H12.0646C12.3764 11.4192 12.6291 11.1665 12.6291 10.8547V10.2903Z" fill="#023DFE"/>
<path d="M6.95005 1.29297L1.63477 5.77687L6.95005 10.3795L7.75136 10.005V1.54405L6.95005 1.29297Z" fill="#023DFE" fill-opacity="0.7"/>
<path d="M2.83379 6.21871C2.54005 5.99278 2.54005 5.54981 2.83379 5.32382L7.7517 1.54081V1.27257C7.7517 0.804238 7.21426 0.539551 6.843 0.825156L0.994719 5.32382C0.700979 5.54981 0.700979 5.99278 0.994719 6.21871L6.843 10.7174C7.21426 11.0029 7.7517 10.7383 7.7517 10.27V10.0017L2.83379 6.21871Z" fill="#023DFE"/>
</svg>

After

Width:  |  Height:  |  Size: 1.4 KiB

View File

@ -0,0 +1,6 @@
<svg width="10" height="12" viewBox="0 0 10 12" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M8.81262 0.277344H8.24811C7.93632 0.277344 7.68359 0.530068 7.68359 0.841855V11.0031H8.81262C9.1244 11.0031 9.37713 10.7504 9.37713 10.4386V0.841855C9.37713 0.530136 9.1244 0.277344 8.81262 0.277344Z" fill="#023DFE" fill-opacity="0.7"/>
<path d="M9.37719 10.4387C8.75361 10.4387 8.24816 9.93317 8.24816 9.30967V0.277344H7.68365C7.37187 0.277344 7.11914 0.530068 7.11914 0.841855V11.0031C7.11914 11.3149 7.37187 11.5676 7.68365 11.5676H8.81268C9.12446 11.5676 9.37719 11.3149 9.37719 11.0031V10.4387Z" fill="#023DFE"/>
<path d="M2.5548 0.277344H1.99029C1.67851 0.277344 1.42578 0.530068 1.42578 0.841855V11.0031H2.5548C2.86659 11.0031 3.11932 10.7504 3.11932 10.4386V0.841855C3.11932 0.530136 2.86659 0.277344 2.5548 0.277344Z" fill="#023DFE" fill-opacity="0.7"/>
<path d="M3.11937 10.4387C2.4958 10.4387 1.99035 9.93317 1.99035 9.30967V0.277344H1.42584C1.11405 0.277344 0.861328 0.530068 0.861328 0.841855V11.0031C0.861328 11.3149 1.11405 11.5676 1.42584 11.5676H2.55486C2.86665 11.5676 3.11937 11.3149 3.11937 11.0031V10.4387Z" fill="#023DFE"/>
</svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

View File

@ -0,0 +1,10 @@
<svg width="17" height="17" viewBox="0 0 17 17" fill="none" xmlns="http://www.w3.org/2000/svg">
<g clip-path="url(#clip0_10119_2631)">
<path d="M16.4229 10.9077V14.4803C16.4229 15.1238 15.9015 15.6453 15.2579 15.6453C14.6143 15.6453 14.0928 15.1238 14.0928 14.4803V14.3684C12.644 15.7134 10.7197 16.5 8.65572 16.5C5.42291 16.5 2.52657 14.573 1.27576 11.5914C1.21425 11.4446 1.18535 11.2917 1.18535 11.1417C1.18535 10.6854 1.45378 10.2539 1.89977 10.0661C2.49302 9.81722 3.17574 10.0959 3.4246 10.6901C4.31098 12.804 6.36475 14.1699 8.65572 14.1699C10.3973 14.1699 11.9999 13.3804 13.0578 12.0728H11.6849C11.0413 12.0728 10.5198 11.5513 10.5198 10.9077C10.5198 10.2641 11.0413 9.74265 11.6849 9.74265H15.2574C15.901 9.74265 16.4229 10.2641 16.4229 10.9077ZM5.31572 7.413C5.9593 7.413 6.48078 6.89105 6.48078 6.24794C6.48078 5.60436 5.9593 5.08288 5.31572 5.08288H4.13342C5.18897 3.68388 6.84661 2.83012 8.65572 2.83012C10.9472 2.83012 13.0005 4.1965 13.8873 6.31039C14.1357 6.90364 14.8184 7.18278 15.4117 6.93439C16.0049 6.68554 16.2841 6.00328 16.0357 5.40956C14.7844 2.42701 11.8881 0.5 8.65572 0.5C6.4421 0.5 4.38554 1.40455 2.90824 2.93218V2.67493C2.90824 2.03135 2.3863 1.50987 1.74318 1.50987C1.09961 1.50987 0.578125 2.03135 0.578125 2.67493V6.24794C0.578125 6.55691 0.701155 6.8533 0.919255 7.07234C1.13782 7.2909 1.43375 7.413 1.74318 7.413H5.31572Z" fill="#023DFE" fill-opacity="0.7"/>
</g>
<defs>
<clipPath id="clip0_10119_2631">
<rect width="16" height="16" fill="white" transform="translate(0.5 0.5)"/>
</clipPath>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 1.5 KiB

14
bin/web-stack.ts Normal file
View File

@ -0,0 +1,14 @@
#!/usr/bin/env node
import 'source-map-support/register';
import * as cdk from 'aws-cdk-lib';
import { WebStack } from '../infrastructure/web-stack';
const app = new cdk.App();
new WebStack(app, 'SyncrowWebStack', {
env: {
account: process.env.CDK_DEFAULT_ACCOUNT,
region: 'me-central-1',
},
certificateArn: app.node.tryGetContext('certificateArn'),
});

17
build.sh Normal file
View File

@ -0,0 +1,17 @@
#!/bin/bash
set -e
REGION=${AWS_DEFAULT_REGION:-me-central-1}
ACCOUNT_ID=$(aws sts get-caller-identity --query Account --output text)
STACK_NAME=SyncrowWebStack
CERTIFICATE_ARN="arn:aws:acm:us-east-1:$ACCOUNT_ID:certificate/b3ea57be-9bf0-4c66-8b01-9672ef1e8530"
echo "🧱 Building Flutter Web for PRODUCTION..."
flutter build web --target=lib/main.dart --release
echo "🚀 Deploying CDK stack..."
npx cdk deploy $STACK_NAME \
--context certificateArn=$CERTIFICATE_ARN \
--require-approval never
echo "✅ Deployment complete. Access the app at: https://app.syncrow.me"

10
cdk.context.json Normal file
View File

@ -0,0 +1,10 @@
{
"hosted-zone:account=482311766496:domainName=syncrow.me:region=us-east-2": {
"Id": "/hostedzone/Z02085662NLJECF4DGJV3",
"Name": "syncrow.me."
},
"hosted-zone:account=482311766496:domainName=syncrow.me:region=me-central-1": {
"Id": "/hostedzone/Z02085662NLJECF4DGJV3",
"Name": "syncrow.me."
}
}

63
cdk.json Normal file
View File

@ -0,0 +1,63 @@
{
"app": "npx ts-node --prefer-ts-exts bin/web-stack.ts",
"watch": {
"include": [
"**"
],
"exclude": [
"README.md",
"cdk*.json",
"**/*.d.ts",
"**/*.js",
"tsconfig.json",
"package*.json",
"yarn.lock",
"node_modules",
"test"
]
},
"context": {
"@aws-cdk/aws-lambda:recognizeLayerVersion": true,
"@aws-cdk/core:checkSecretUsage": true,
"@aws-cdk/core:target-partitions": [
"aws",
"aws-cn"
],
"@aws-cdk-containers/ecs-service-extensions:enableDefaultLogDriver": true,
"@aws-cdk/aws-ec2:uniqueImdsv2TemplateName": true,
"@aws-cdk/aws-ecs:arnFormatIncludesClusterName": true,
"@aws-cdk/aws-iam:minimizePolicies": true,
"@aws-cdk/core:validateSnapshotRemovalPolicy": true,
"@aws-cdk/aws-codepipeline:crossAccountKeyAliasStackSafeResourceName": true,
"@aws-cdk/aws-s3:createDefaultLoggingPolicy": true,
"@aws-cdk/aws-sns-subscriptions:restrictSqsDescryption": true,
"@aws-cdk/aws-apigateway:disableCloudWatchRole": true,
"@aws-cdk/core:enablePartitionLiterals": true,
"@aws-cdk/aws-events:eventsTargetQueueSameAccount": true,
"@aws-cdk/aws-iam:standardizedServicePrincipals": true,
"@aws-cdk/aws-ecs:disableExplicitDeploymentControllerForCircuitBreaker": true,
"@aws-cdk/aws-iam:importedRoleStackSafeDefaultPolicyName": true,
"@aws-cdk/aws-s3:serverAccessLogsUseBucketPolicy": true,
"@aws-cdk/aws-route53-patters:useCertificate": true,
"@aws-cdk/customresources:installLatestAwsSdkDefault": false,
"@aws-cdk/aws-rds:databaseProxyUniqueResourceName": true,
"@aws-cdk/aws-codedeploy:removeAlarmsFromDeploymentGroup": true,
"@aws-cdk/aws-apigateway:authorizerChangeDeploymentLogicalId": true,
"@aws-cdk/aws-ec2:launchTemplateDefaultUserData": true,
"@aws-cdk/aws-secretsmanager:useAttachedSecretResourcePolicyForSecretTargetAttachments": true,
"@aws-cdk/aws-redshift:columnId": true,
"@aws-cdk/aws-stepfunctions-tasks:enableLogging": true,
"@aws-cdk/aws-ec2:restrictDefaultSecurityGroup": true,
"@aws-cdk/aws-apigateway:requestValidatorUniqueId": true,
"@aws-cdk/aws-kms:aliasNameRef": true,
"@aws-cdk/aws-autoscaling:generateLaunchTemplateInsteadOfLaunchConfig": true,
"@aws-cdk/core:includePrefixInUniqueNameGeneration": true,
"@aws-cdk/aws-efs:denyAnonymousAccess": true,
"@aws-cdk/aws-opensearchservice:enableLogging": true,
"@aws-cdk/aws-nordicapis-apigateway:authorizerChangeDeploymentLogicalId": true,
"@aws-cdk/aws-ec2:ebsDefaultGp3Volume": true,
"@aws-cdk/aws-ecs-patterns:removeDefaultDesiredCount": true,
"@aws-cdk/aws-rds:preventRenderingDeprecatedCredentials": true,
"@aws-cdk/aws-codepipeline-actions:useNewDefaultBranchForSourceAction": true
}
}

123
infrastructure/web-stack.ts Normal file
View File

@ -0,0 +1,123 @@
import * as cdk from "aws-cdk-lib";
import * as acm from "aws-cdk-lib/aws-certificatemanager";
import * as cloudfront from "aws-cdk-lib/aws-cloudfront";
import * as origins from "aws-cdk-lib/aws-cloudfront-origins";
import * as route53 from "aws-cdk-lib/aws-route53";
import * as targets from "aws-cdk-lib/aws-route53-targets";
import * as s3 from "aws-cdk-lib/aws-s3";
import * as s3deploy from "aws-cdk-lib/aws-s3-deployment";
import { Construct } from "constructs";
export interface WebStackProps extends cdk.StackProps {
certificateArn?: string;
}
export class WebStack extends cdk.Stack {
public readonly distributionUrl: string;
public readonly bucketName: string;
constructor(scope: Construct, id: string, props?: WebStackProps) {
super(scope, id, props);
const bucketName = `syncrow-web-${this.account}-${this.region}`;
const webBucket = new s3.Bucket(this, "SyncrowWebBucket", {
bucketName,
websiteIndexDocument: "index.html",
websiteErrorDocument: "index.html",
publicReadAccess: true,
blockPublicAccess: s3.BlockPublicAccess.BLOCK_ACLS,
removalPolicy: cdk.RemovalPolicy.DESTROY,
autoDeleteObjects: true,
});
// Use existing wildcard certificate in us-east-1 (required for CloudFront)
const webCertificate = props?.certificateArn
? acm.Certificate.fromCertificateArn(
this,
"WildcardCertificate",
props.certificateArn
)
: acm.Certificate.fromCertificateArn(
this,
"WildcardCertificate",
"arn:aws:acm:us-east-1:482311766496:certificate/b3ea57be-9bf0-4c66-8b01-9672ef1e8530"
);
// Get the hosted zone
const hostedZone = route53.HostedZone.fromLookup(this, "SyncrowZone", {
domainName: "syncrow.me",
});
const distribution = new cloudfront.Distribution(
this,
"SyncrowWebDistribution",
{
defaultBehavior: {
origin: new origins.S3Origin(webBucket),
viewerProtocolPolicy:
cloudfront.ViewerProtocolPolicy.REDIRECT_TO_HTTPS,
cachePolicy: cloudfront.CachePolicy.CACHING_OPTIMIZED,
},
domainNames: ["app.syncrow.me"],
certificate: webCertificate,
defaultRootObject: "index.html",
errorResponses: [
{
httpStatus: 404,
responseHttpStatus: 200,
responsePagePath: "/index.html",
},
{
httpStatus: 403,
responseHttpStatus: 200,
responsePagePath: "/index.html",
},
],
}
);
// Create Route 53 record for app.syncrow.me
new route53.ARecord(this, "WebAliasRecord", {
zone: hostedZone,
recordName: "app",
target: route53.RecordTarget.fromAlias(
new targets.CloudFrontTarget(distribution)
),
});
new s3deploy.BucketDeployment(this, "SyncrowWebDeployment", {
sources: [s3deploy.Source.asset("./build/web")],
destinationBucket: webBucket,
distribution,
distributionPaths: ["/*"],
});
this.distributionUrl = "https://app.syncrow.me";
this.bucketName = bucketName;
new cdk.CfnOutput(this, "WebsiteUrl", {
value: this.distributionUrl,
description: "Web Application URL",
exportName: `${this.stackName}-WebsiteUrl`,
});
new cdk.CfnOutput(this, "CloudFrontUrl", {
value: `https://${distribution.distributionDomainName}`,
description: "CloudFront Distribution URL",
exportName: `${this.stackName}-CloudFrontUrl`,
});
new cdk.CfnOutput(this, "BucketName", {
value: this.bucketName,
description: "S3 Bucket Name",
exportName: `${this.stackName}-BucketName`,
});
new cdk.CfnOutput(this, "WildcardCertificateArn", {
value: webCertificate.certificateArn,
description: "Wildcard SSL Certificate ARN (us-east-1)",
exportName: `${this.stackName}-WildcardCertificateArn`,
});
}
}

View File

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

View File

@ -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()}',
eventDate: DateTime.utc(
int.parse(year ?? '2025'),
int.parse(month ?? '1'),
int.parse(day ?? '1'),
),
countTotalPresenceDetected: json['count_total_presence_detected'] as int? ?? 0,
countTotalPresenceDetected: num.parse(
json['count_total_presence_detected']?.toString() ?? '0',
).toInt(),
);
}

View File

@ -46,11 +46,11 @@ class AirQualityDistributionBloc
}
}
Future<void> _onClearAirQualityDistribution(
void _onClearAirQualityDistribution(
ClearAirQualityDistribution event,
Emitter<AirQualityDistributionState> emit,
) async {
emit(const AirQualityDistributionState());
) {
emit(AirQualityDistributionState(selectedAqiType: state.selectedAqiType));
}
void _onUpdateAqiTypeEvent(

View File

@ -75,6 +75,6 @@ class RangeOfAqiBloc extends Bloc<RangeOfAqiEvent, RangeOfAqiState> {
ClearRangeOfAqiEvent event,
Emitter<RangeOfAqiState> emit,
) {
emit(const RangeOfAqiState());
emit(RangeOfAqiState(selectedAqiType: state.selectedAqiType));
}
}

View File

@ -4,7 +4,6 @@ import 'package:syncrow_web/pages/analytics/modules/air_quality/blocs/air_qualit
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/air_quality/widgets/aqi_type_dropdown.dart';
import 'package:syncrow_web/pages/analytics/modules/analytics/blocs/analytics_date_picker_bloc/analytics_date_picker_bloc.dart';
import 'package:syncrow_web/pages/analytics/modules/analytics/blocs/analytics_devices/analytics_devices_bloc.dart';
import 'package:syncrow_web/pages/analytics/modules/energy_management/blocs/realtime_device_changes/realtime_device_changes_bloc.dart';
import 'package:syncrow_web/pages/analytics/params/get_air_quality_distribution_param.dart';
@ -22,7 +21,6 @@ abstract final class FetchAirQualityDataHelper {
required String spaceUuid,
bool shouldFetchAnalyticsDevices = true,
}) {
final date = context.read<AnalyticsDatePickerBloc>().state.monthlyDate;
final aqiType = context.read<AirQualityDistributionBloc>().state.selectedAqiType;
if (shouldFetchAnalyticsDevices) {
loadAnalyticsDevices(

View File

@ -18,7 +18,11 @@ 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(
@ -39,11 +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(

View File

@ -65,7 +65,7 @@ class AqiDeviceInfo extends StatelessWidget {
);
final tvocValue = _getValueForStatus(
status,
'tvoc_value',
'voc_value',
formatter: (value) => (value / 100).toStringAsFixed(2),
);

View File

@ -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';
@ -149,6 +150,7 @@ class AqiDistributionChart extends StatelessWidget {
);
final bottomTitles = AxisTitles(
axisNameWidget: const ChartsXAxisTitle(),
sideTitles: SideTitles(
showTitles: chartData.isNotEmpty,
getTitlesWidget: (value, _) => FittedBox(

View File

@ -34,6 +34,7 @@ class AqiDistributionChartTitle extends StatelessWidget {
alignment: AlignmentDirectional.centerEnd,
fit: BoxFit.scaleDown,
child: AqiTypeDropdown(
selectedAqiType: context.watch<AirQualityDistributionBloc>().state.selectedAqiType,
onChanged: (value) {
if (value != null) {
final bloc = context.read<AirQualityDistributionBloc>();

View File

@ -6,8 +6,8 @@ enum AqiType {
aqi('AQI', '', 'aqi'),
pm25('PM2.5', 'µg/m³', 'pm25'),
pm10('PM10', 'µg/m³', 'pm10'),
hcho('HCHO', 'mg/m³', 'cho2'),
tvoc('TVOC', 'µg/m³', 'voc'),
hcho('HCHO', 'mg/m³', 'ch2o'),
tvoc('TVOC', 'mg/m³', 'voc'),
co2('CO2', 'ppm', 'co2');
const AqiType(this.value, this.unit, this.code);
@ -18,19 +18,20 @@ enum AqiType {
}
class AqiTypeDropdown extends StatefulWidget {
const AqiTypeDropdown({super.key, required this.onChanged});
const AqiTypeDropdown({
required this.onChanged,
this.selectedAqiType,
super.key,
});
final ValueChanged<AqiType?> onChanged;
final AqiType? selectedAqiType;
@override
State<AqiTypeDropdown> createState() => _AqiTypeDropdownState();
}
class _AqiTypeDropdownState extends State<AqiTypeDropdown> {
AqiType? _selectedItem = AqiType.aqi;
void _updateSelectedItem(AqiType? item) => setState(() => _selectedItem = item);
@override
Widget build(BuildContext context) {
return Container(
@ -41,8 +42,8 @@ class _AqiTypeDropdownState extends State<AqiTypeDropdown> {
width: 1,
),
),
child: DropdownButton<AqiType?>(
value: _selectedItem,
child: DropdownButton<AqiType>(
value: widget.selectedAqiType,
isDense: true,
borderRadius: BorderRadius.circular(16),
dropdownColor: ColorsManager.whiteColors,
@ -59,10 +60,7 @@ class _AqiTypeDropdownState extends State<AqiTypeDropdown> {
items: AqiType.values
.map((e) => DropdownMenuItem(value: e, child: Text(e.value)))
.toList(),
onChanged: (value) {
_updateSelectedItem(value);
widget.onChanged(value);
},
onChanged: widget.onChanged,
),
);
}

View File

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

View File

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

View File

@ -63,15 +63,15 @@ class RangeOfAqiChartTitle extends StatelessWidget {
fit: BoxFit.scaleDown,
alignment: AlignmentDirectional.centerEnd,
child: AqiTypeDropdown(
selectedAqiType: context.watch<RangeOfAqiBloc>().state.selectedAqiType,
onChanged: (value) {
final spaceTreeState = context.read<SpaceTreeBloc>().state;
final spaceUuid = spaceTreeState.selectedSpaces.firstOrNull;
if (spaceUuid == null) return;
if (value != null) {
context.read<RangeOfAqiBloc>().add(UpdateAqiTypeEvent(value));
}
if (spaceUuid == null) return;
},
),
),

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -23,10 +23,9 @@ class OccupancyEndSideBar extends StatelessWidget {
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const AnalyticsSidebarHeader(title: 'Presnce Sensor'),
const AnalyticsSidebarHeader(title: 'Presence Sensor'),
Expanded(
child: SizedBox(
// height: MediaQuery.sizeOf(context).height * 0.2,
child: PowerClampEnergyStatusWidget(
status: [
PowerClampEnergyStatus(

View File

@ -9,8 +9,13 @@ import 'package:syncrow_web/pages/analytics/modules/occupancy/widgets/occupancy_
import 'package:syncrow_web/utils/color_manager.dart';
class OccupancyHeatMap extends StatelessWidget {
const OccupancyHeatMap({required this.heatMapData, super.key});
const OccupancyHeatMap({
required this.heatMapData,
required this.selectedDate,
super.key,
});
final Map<DateTime, int> heatMapData;
final DateTime selectedDate;
static const _cellSize = 16.0;
static const _totalWeeks = 53;
@ -20,7 +25,7 @@ class OccupancyHeatMap extends StatelessWidget {
: 0;
DateTime _getStartingDate() {
final jan1 = DateTime(DateTime.now().year, 1, 1);
final jan1 = DateTime.utc(selectedDate.year, 1, 1);
final startOfWeek = jan1.subtract(Duration(days: jan1.weekday - 1));
return startOfWeek;
}

View File

@ -70,6 +70,8 @@ class OccupancyHeatMapBox extends StatelessWidget {
const SizedBox(height: 20),
Expanded(
child: OccupancyHeatMap(
selectedDate:
context.watch<AnalyticsDatePickerBloc>().state.yearlyDate,
heatMapData: state.heatMapData.asMap().map(
(_, value) => MapEntry(
value.eventDate,

View File

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

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(':');

View File

@ -68,24 +68,30 @@ 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 =
event.snapshot.value as Map<dynamic, dynamic>;
final usersMap = event.snapshot.value! as Map<dynamic, dynamic>;
List<Status> statusList = [];
final statusList = <Status>[];
usersMap['status'].forEach((element) {
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));
}
@ -105,22 +111,14 @@ class AcBloc extends Bloc<AcsEvent, AcsState> {
AcControlEvent event,
Emitter<AcsState> emit,
) async {
emit(AcsLoadingState());
_updateDeviceFunctionFromCode(event.code, event.value);
emit(ACStatusLoaded(status: deviceStatus));
try {
final success = await controlDeviceService.controlDevice(
_updateDeviceFunctionFromCode(event.code, event.value);
emit(ACStatusLoaded(status: deviceStatus));
await controlDeviceService.controlDevice(
deviceUuid: event.deviceId,
status: Status(code: event.code, value: event.value),
);
if (!success) {
emit(const AcsFailedState(error: 'Failed to control device'));
}
} catch (e) {
emit(AcsFailedState(error: e.toString()));
}
} catch (e) {}
}
FutureOr<void> _onFetchAcBatchStatus(
@ -141,23 +139,16 @@ class AcBloc extends Bloc<AcsEvent, AcsState> {
AcBatchControlEvent event,
Emitter<AcsState> emit,
) async {
emit(AcsLoadingState());
_updateDeviceFunctionFromCode(event.code, event.value);
emit(ACStatusLoaded(status: deviceStatus));
try {
final success = await batchControlDevicesService.batchControlDevices(
await batchControlDevicesService.batchControlDevices(
uuids: event.devicesIds,
code: event.code,
value: event.value,
);
if (!success) {
emit(const AcsFailedState(error: 'Failed to control devices'));
}
} catch (e) {
emit(AcsFailedState(error: e.toString()));
}
} catch (e) {}
}
Future<void> _onFactoryReset(
@ -190,8 +181,8 @@ class AcBloc extends Bloc<AcsEvent, AcsState> {
void _handleIncreaseTime(IncreaseTimeEvent event, Emitter<AcsState> emit) {
if (state is! ACStatusLoaded) return;
final currentState = state as ACStatusLoaded;
int newHours = scheduledHours;
int newMinutes = scheduledMinutes + 30;
var newHours = scheduledHours;
var newMinutes = scheduledMinutes + 30;
newHours += newMinutes ~/ 60;
newMinutes = newMinutes % 60;
if (newHours > 23) {
@ -213,7 +204,7 @@ class AcBloc extends Bloc<AcsEvent, AcsState> {
) {
if (state is! ACStatusLoaded) return;
final currentState = state as ACStatusLoaded;
int totalMinutes = (scheduledHours * 60) + scheduledMinutes;
var totalMinutes = (scheduledHours * 60) + scheduledMinutes;
totalMinutes = (totalMinutes - 30).clamp(0, 1440);
scheduledHours = totalMinutes ~/ 60;
scheduledMinutes = totalMinutes % 60;
@ -286,20 +277,24 @@ class AcBloc extends Bloc<AcsEvent, AcsState> {
void _startCountdownTimer(Emitter<AcsState> emit) {
_countdownTimer?.cancel();
int totalSeconds = (scheduledHours * 3600) + (scheduledMinutes * 60);
var totalSeconds = (scheduledHours * 3600) + (scheduledMinutes * 60);
_countdownTimer = Timer.periodic(const Duration(seconds: 1), (timer) {
if (totalSeconds > 0) {
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 +321,9 @@ class AcBloc extends Bloc<AcsEvent, AcsState> {
_startCountdownTimer(
emit,
);
add(UpdateTimerEvent());
if (!isClosed) {
add(UpdateTimerEvent());
}
}
}
@ -370,6 +367,8 @@ class AcBloc extends Bloc<AcsEvent, AcsState> {
@override
Future<void> close() {
add(OnClose());
_countdownTimer?.cancel();
_deviceStatusSubscription?.cancel();
return super.close();
}
}

View File

@ -16,11 +16,12 @@ class DeviceManagementBloc
int _onlineCount = 0;
int _offlineCount = 0;
int _lowBatteryCount = 0;
List<AllDevicesModel> _selectedDevices = [];
final List<AllDevicesModel> _selectedDevices = [];
List<AllDevicesModel> _filteredDevices = [];
String currentProductName = '';
String? currentCommunity;
String? currentUnitName;
String subSpaceName = '';
DeviceManagementBloc() : super(DeviceManagementInitial()) {
on<FetchDevices>(_onFetchDevices);
@ -31,25 +32,26 @@ class DeviceManagementBloc
on<ResetFilters>(_onResetFilters);
on<ResetSelectedDevices>(_onResetSelectedDevices);
on<UpdateSelection>(_onUpdateSelection);
on<UpdateDeviceName>(_onUpdateDeviceName);
on<UpdateSubSpaceName>(_onUpdateSubSpaceName);
}
Future<void> _onFetchDevices(
FetchDevices event, Emitter<DeviceManagementState> emit) async {
emit(DeviceManagementLoading());
try {
List<AllDevicesModel> devices = [];
var devices = <AllDevicesModel>[];
_devices.clear();
var spaceBloc = event.context.read<SpaceTreeBloc>();
final spaceBloc = event.context.read<SpaceTreeBloc>();
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 =
for (final community in spaceBloc.state.selectedCommunities) {
final spacesList =
spaceBloc.state.selectedCommunityAndSpaces[community] ?? [];
for (var space in spacesList) {
for (final space in spacesList) {
devices.addAll(await DevicesManagementApi()
.fetchDevices(community, space, projectUuid));
}
@ -74,7 +76,7 @@ class DeviceManagementBloc
}
}
void _onFilterDevices(
Future<void> _onFilterDevices(
FilterDevices event, Emitter<DeviceManagementState> emit) async {
if (_devices.isNotEmpty) {
_filteredDevices = List.from(_devices.where((device) {
@ -156,8 +158,7 @@ class DeviceManagementBloc
add(FilterDevices(_getFilterFromIndex(_selectedIndex)));
}
void _onSelectDevice(
SelectDevice event, Emitter<DeviceManagementState> emit) {
void _onSelectDevice(SelectDevice event, Emitter<DeviceManagementState> emit) {
final selectedUuid = event.selectedDevice.uuid;
if (_selectedDevices.any((device) => device.uuid == selectedUuid)) {
@ -166,9 +167,9 @@ class DeviceManagementBloc
_selectedDevices.add(event.selectedDevice);
}
List<AllDevicesModel> clonedSelectedDevices = List.from(_selectedDevices);
final clonedSelectedDevices = List<AllDevicesModel>.from(_selectedDevices);
bool isControlButtonEnabled =
final isControlButtonEnabled =
_checkIfControlButtonEnabled(clonedSelectedDevices);
if (state is DeviceManagementLoaded) {
@ -198,8 +199,8 @@ class DeviceManagementBloc
void _onUpdateSelection(
UpdateSelection event, Emitter<DeviceManagementState> emit) {
List<AllDevicesModel> selectedDevices = [];
List<AllDevicesModel> devicesToSelectFrom = [];
final selectedDevices = <AllDevicesModel>[];
var devicesToSelectFrom = <AllDevicesModel>[];
if (state is DeviceManagementLoaded) {
devicesToSelectFrom = (state as DeviceManagementLoaded).devices;
@ -207,7 +208,7 @@ class DeviceManagementBloc
devicesToSelectFrom = (state as DeviceManagementFiltered).filteredDevices;
}
for (int i = 0; i < event.selectedRows.length; i++) {
for (var i = 0; i < event.selectedRows.length; i++) {
if (event.selectedRows[i]) {
selectedDevices.add(devicesToSelectFrom[i]);
}
@ -253,8 +254,7 @@ class DeviceManagementBloc
_onlineCount = _devices.where((device) => device.online == true).length;
_offlineCount = _devices.where((device) => device.online == false).length;
_lowBatteryCount = _devices
.where((device) =>
device.batteryLevel != null && device.batteryLevel! < 20)
.where((device) => device.batteryLevel != null && device.batteryLevel! < 20)
.length;
}
@ -270,8 +270,8 @@ class DeviceManagementBloc
return 'All';
}
}
void _onSearchDevices(
SearchDevices event, Emitter<DeviceManagementState> emit) {
void _onSearchDevices(SearchDevices event, Emitter<DeviceManagementState> emit) {
if ((event.community == null || event.community!.isEmpty) &&
(event.unitName == null || event.unitName!.isEmpty) &&
(event.deviceNameOrProductName == null ||
@ -300,7 +300,7 @@ class DeviceManagementBloc
currentCommunity = event.community;
currentUnitName = event.unitName;
List<AllDevicesModel> devicesToSearch = _devices;
final devicesToSearch = _devices;
if (devicesToSearch.isNotEmpty) {
final searchText = event.deviceNameOrProductName?.toLowerCase() ?? '';
@ -343,5 +343,134 @@ class DeviceManagementBloc
}
}
void _onUpdateDeviceName(
UpdateDeviceName event, Emitter<DeviceManagementState> emit) {
final devices = _devices.map((device) {
if (device.uuid == event.deviceId) {
final modifiedDevice = device.copyWith(name: event.newName);
_selectedDevices.removeWhere((device) => device.uuid == event.deviceId);
_selectedDevices.add(modifiedDevice);
return modifiedDevice;
}
return device;
}).toList();
final filteredDevices = _filteredDevices.map((device) {
if (device.uuid == event.deviceId) {
final modifiedDevice = device.copyWith(name: event.newName);
_selectedDevices.removeWhere((device) => device.uuid == event.deviceId);
_selectedDevices.add(modifiedDevice);
return modifiedDevice;
}
return device;
}).toList();
_devices = devices;
_filteredDevices = filteredDevices;
if (state is DeviceManagementLoaded) {
final loaded = state as DeviceManagementLoaded;
final selectedDevices01 = _selectedDevices.map((device) {
if (device.uuid == event.deviceId) {
final modifiedDevice = device.copyWith(name: event.newName);
return modifiedDevice;
}
return device;
}).toList();
emit(DeviceManagementLoaded(
devices: devices,
selectedIndex: loaded.selectedIndex,
onlineCount: loaded.onlineCount,
offlineCount: loaded.offlineCount,
lowBatteryCount: loaded.lowBatteryCount,
selectedDevice: selectedDevices01,
isControlButtonEnabled: loaded.isControlButtonEnabled,
));
} else if (state is DeviceManagementFiltered) {
final filtered = state as DeviceManagementFiltered;
final selectedDevices01 = filtered.selectedDevice?.map((device) {
if (device.uuid == event.deviceId) {
final modifiedDevice = device.copyWith(name: event.newName);
return modifiedDevice;
}
return device;
}).toList();
emit(DeviceManagementFiltered(
filteredDevices: filteredDevices,
selectedIndex: filtered.selectedIndex,
onlineCount: filtered.onlineCount,
offlineCount: filtered.offlineCount,
lowBatteryCount: filtered.lowBatteryCount,
selectedDevice: selectedDevices01,
isControlButtonEnabled: filtered.isControlButtonEnabled,
));
}
}
void _onUpdateSubSpaceName(
UpdateSubSpaceName event, Emitter<DeviceManagementState> emit) {
final devices = _devices.map((device) {
if (device.uuid == event.deviceId) {
return device.copyWith(
subspace:
device.subspace?.copyWith(subspaceName: event.newSubSpaceName));
}
return device;
}).toList();
final filteredDevices = _filteredDevices.map((device) {
if (device.uuid == event.deviceId) {
return device.copyWith(
subspace:
device.subspace?.copyWith(subspaceName: event.newSubSpaceName));
}
return device;
}).toList();
_devices = devices;
_filteredDevices = filteredDevices;
if (state is DeviceManagementLoaded) {
final loaded = state as DeviceManagementLoaded;
final selectedDevices = loaded.selectedDevice?.map((device) {
if (device.uuid == event.deviceId) {
return device.copyWith(
subspace:
device.subspace?.copyWith(subspaceName: event.newSubSpaceName));
}
return device;
}).toList();
emit(DeviceManagementLoaded(
devices: _devices,
selectedIndex: loaded.selectedIndex,
onlineCount: loaded.onlineCount,
offlineCount: loaded.offlineCount,
lowBatteryCount: loaded.lowBatteryCount,
selectedDevice: selectedDevices,
isControlButtonEnabled: loaded.isControlButtonEnabled,
));
} else if (state is DeviceManagementFiltered) {
// final filtered = state as DeviceManagementFiltered;
// emit(DeviceManagementFiltered(
// filteredDevices: _filteredDevices,
// selectedIndex: filtered.selectedIndex,
// onlineCount: filtered.onlineCount,
// offlineCount: filtered.offlineCount,
// lowBatteryCount: filtered.lowBatteryCount,
// selectedDevice: filtered.selectedDevice,
// isControlButtonEnabled: filtered.isControlButtonEnabled,
// ));
}
}
void changeSubspaceName(
String deviceId, String newSubSpaceName, String subspaceId) {
add(UpdateSubSpaceName(
deviceId: deviceId,
newSubSpaceName: newSubSpaceName,
subspaceId: subspaceId,
));
}
List<AllDevicesModel> get selectedDevices => _selectedDevices;
}

View File

@ -70,3 +70,21 @@ class UpdateSelection extends DeviceManagementEvent {
const UpdateSelection(this.selectedRows);
}
class UpdateDeviceName extends DeviceManagementEvent {
final String deviceId;
final String newName;
const UpdateDeviceName({required this.deviceId, required this.newName});
}
class UpdateSubSpaceName extends DeviceManagementEvent {
final String deviceId;
final String newSubSpaceName;
final String subspaceId;
const UpdateSubSpaceName(
{required this.deviceId,
required this.newSubSpaceName,
required this.subspaceId});
}

View File

@ -7,6 +7,8 @@ import 'package:syncrow_web/pages/device_managment/ceiling_sensor/view/ceiling_s
import 'package:syncrow_web/pages/device_managment/ceiling_sensor/view/ceiling_sensor_controls.dart';
import 'package:syncrow_web/pages/device_managment/curtain/view/curtain_batch_status_view.dart';
import 'package:syncrow_web/pages/device_managment/curtain/view/curtain_status_view.dart';
import 'package:syncrow_web/pages/device_managment/curtain_module/view/curtain_module_batch.dart';
import 'package:syncrow_web/pages/device_managment/curtain_module/view/curtain_module_items.dart';
import 'package:syncrow_web/pages/device_managment/door_lock/view/door_lock_batch_control_view.dart';
import 'package:syncrow_web/pages/device_managment/door_lock/view/door_lock_control_view.dart';
import 'package:syncrow_web/pages/device_managment/flush_mounted_presence_sensor/views/flush_mounted_presence_sensor_batch_control_view.dart';
@ -18,6 +20,7 @@ import 'package:syncrow_web/pages/device_managment/gateway/view/gateway_view.dar
import 'package:syncrow_web/pages/device_managment/main_door_sensor/view/main_door_control_view.dart';
import 'package:syncrow_web/pages/device_managment/main_door_sensor/view/main_door_sensor_batch_view.dart';
import 'package:syncrow_web/pages/device_managment/one_g_glass_switch/view/one_gang_glass_batch_control_view.dart';
import 'package:syncrow_web/pages/device_managment/one_g_glass_switch/view/one_gang_glass_switch_control_view.dart';
import 'package:syncrow_web/pages/device_managment/one_gang_switch/view/wall_light_batch_control.dart';
import 'package:syncrow_web/pages/device_managment/one_gang_switch/view/wall_light_device_control.dart';
import 'package:syncrow_web/pages/device_managment/power_clamp/view/power_clamp_batch_control_view.dart';
@ -39,8 +42,6 @@ import 'package:syncrow_web/pages/device_managment/water_heater/view/water_heate
import 'package:syncrow_web/pages/device_managment/water_leak/view/water_leak_batch_control_view.dart';
import 'package:syncrow_web/pages/device_managment/water_leak/view/water_leak_control_view.dart';
import '../../one_g_glass_switch/view/one_gang_glass_switch_control_view.dart';
mixin RouteControlsBasedCode {
Widget routeControlsWidgets({required AllDevicesModel device}) {
switch (device.productType) {
@ -84,6 +85,10 @@ mixin RouteControlsBasedCode {
return CurtainStatusControlsView(
deviceId: device.uuid!,
);
case 'CUR_2':
return CurtainModuleItems(
deviceId: device.uuid!,
);
case 'AC':
return AcDeviceControlsView(device: device);
case 'WH':
@ -107,7 +112,7 @@ mixin RouteControlsBasedCode {
case 'SOS':
return SosDeviceControlsView(device: device);
case 'NCPS':
case 'NCPS':
return FlushMountedPresenceSensorControlView(device: device);
default:
return const SizedBox();
@ -132,76 +137,140 @@ mixin RouteControlsBasedCode {
switch (devices.first.productType) {
case '1G':
return WallLightBatchControlView(
deviceIds: devices.where((e) => (e.productType == '1G')).map((e) => e.uuid!).toList(),
deviceIds: devices
.where((e) => e.productType == '1G')
.map((e) => e.uuid!)
.toList(),
);
case '2G':
return TwoGangBatchControlView(
deviceIds: devices.where((e) => (e.productType == '2G')).map((e) => e.uuid!).toList(),
deviceIds: devices
.where((e) => e.productType == '2G')
.map((e) => e.uuid!)
.toList(),
);
case '3G':
return LivingRoomBatchControlsView(
deviceIds: devices.where((e) => (e.productType == '3G')).map((e) => e.uuid!).toList(),
deviceIds: devices
.where((e) => e.productType == '3G')
.map((e) => e.uuid!)
.toList(),
);
case '1GT':
return OneGangGlassSwitchBatchControlView(
deviceIds: devices.where((e) => (e.productType == '1GT')).map((e) => e.uuid!).toList(),
deviceIds: devices
.where((e) => e.productType == '1GT')
.map((e) => e.uuid!)
.toList(),
);
case '2GT':
return TwoGangGlassSwitchBatchControlView(
deviceIds: devices.where((e) => (e.productType == '2GT')).map((e) => e.uuid!).toList(),
deviceIds: devices
.where((e) => e.productType == '2GT')
.map((e) => e.uuid!)
.toList(),
);
case '3GT':
return ThreeGangGlassSwitchBatchControlView(
deviceIds: devices.where((e) => (e.productType == '3GT')).map((e) => e.uuid!).toList(),
deviceIds: devices
.where((e) => e.productType == '3GT')
.map((e) => e.uuid!)
.toList(),
);
case 'GW':
return GatewayBatchControlView(
gatewayIds: devices.where((e) => (e.productType == 'GW')).map((e) => e.uuid!).toList(),
gatewayIds: devices
.where((e) => e.productType == 'GW')
.map((e) => e.uuid!)
.toList(),
);
case 'DL':
return DoorLockBatchControlView(
devicesIds: devices.where((e) => (e.productType == 'DL')).map((e) => e.uuid!).toList());
devicesIds: devices
.where((e) => e.productType == 'DL')
.map((e) => e.uuid!)
.toList());
case 'WPS':
return WallSensorBatchControlView(
devicesIds: devices.where((e) => (e.productType == 'WPS')).map((e) => e.uuid!).toList());
devicesIds: devices
.where((e) => e.productType == 'WPS')
.map((e) => e.uuid!)
.toList());
case 'CPS':
return CeilingSensorBatchControlView(
devicesIds: devices.where((e) => (e.productType == 'CPS')).map((e) => e.uuid!).toList(),
devicesIds: devices
.where((e) => e.productType == 'CPS')
.map((e) => e.uuid!)
.toList(),
);
case 'CUR':
return CurtainBatchStatusView(
devicesIds: devices.where((e) => (e.productType == 'CUR')).map((e) => e.uuid!).toList(),
devicesIds: devices
.where((e) => e.productType == 'CUR')
.map((e) => e.uuid!)
.toList(),
);
case 'CUR_2':
return CurtainModuleBatchView(
devicesIds: devices
.where((e) => e.productType == 'CUR_2')
.map((e) => e.uuid!)
.toList(),
);
case 'AC':
return AcDeviceBatchControlView(
devicesIds: devices.where((e) => (e.productType == 'AC')).map((e) => e.uuid!).toList());
devicesIds: devices
.where((e) => e.productType == 'AC')
.map((e) => e.uuid!)
.toList());
case 'WH':
return WaterHEaterBatchControlView(
deviceIds: devices.where((e) => (e.productType == 'WH')).map((e) => e.uuid!).toList(),
deviceIds: devices
.where((e) => e.productType == 'WH')
.map((e) => e.uuid!)
.toList(),
);
case 'DS':
return MainDoorSensorBatchView(
devicesIds: devices.where((e) => (e.productType == 'DS')).map((e) => e.uuid!).toList(),
devicesIds: devices
.where((e) => e.productType == 'DS')
.map((e) => e.uuid!)
.toList(),
);
case 'GD':
return GarageDoorBatchControlView(
deviceIds: devices.where((e) => (e.productType == 'GD')).map((e) => e.uuid!).toList(),
deviceIds: devices
.where((e) => e.productType == 'GD')
.map((e) => e.uuid!)
.toList(),
);
case 'WL':
return WaterLeakBatchControlView(
deviceIds: devices.where((e) => (e.productType == 'WL')).map((e) => e.uuid!).toList(),
deviceIds: devices
.where((e) => e.productType == 'WL')
.map((e) => e.uuid!)
.toList(),
);
case 'PC':
return PowerClampBatchControlView(
deviceIds: devices.where((e) => (e.productType == 'PC')).map((e) => e.uuid!).toList(),
deviceIds: devices
.where((e) => e.productType == 'PC')
.map((e) => e.uuid!)
.toList(),
);
case 'SOS':
return SOSBatchControlView(
deviceIds: devices.where((e) => (e.productType == 'SOS')).map((e) => e.uuid!).toList(),
deviceIds: devices
.where((e) => e.productType == 'SOS')
.map((e) => e.uuid!)
.toList(),
);
case 'NCPS':
return FlushMountedPresenceSensorBatchControlView(
devicesIds: devices.where((e) => (e.productType == 'NCPS')).map((e) => e.uuid!).toList(),
devicesIds: devices
.where((e) => e.productType == 'NCPS')
.map((e) => e.uuid!)
.toList(),
);
default:
return const SizedBox();

View File

@ -44,4 +44,20 @@ class DeviceSubspace {
static List<Map<String, dynamic>> listToJson(List<DeviceSubspace> subspaces) {
return subspaces.map((subspace) => subspace.toJson()).toList();
}
DeviceSubspace copyWith({
String? uuid,
DateTime? createdAt,
DateTime? updatedAt,
String? subspaceName,
bool? disabled,
}) {
return DeviceSubspace(
uuid: uuid ?? this.uuid,
createdAt: createdAt ?? this.createdAt,
updatedAt: updatedAt ?? this.updatedAt,
subspaceName: subspaceName ?? this.subspaceName,
disabled: disabled ?? this.disabled,
);
}
}

View File

@ -588,4 +588,72 @@ SOS
"NCPS": DeviceType.NCPS,
"PC": DeviceType.PC,
};
AllDevicesModel copyWith({
DevicesModelRoom? room,
DeviceSubspace? subspace,
DevicesModelUnit? unit,
DeviceCommunityModel? community,
String? productUuid,
String? productType,
String? permissionType,
int? activeTime,
String? category,
String? categoryName,
int? createTime,
String? gatewayId,
String? icon,
String? ip,
String? lat,
String? localKey,
String? lon,
String? model,
String? name,
String? nodeId,
bool? online,
String? ownerId,
bool? sub,
String? timeZone,
int? updateTime,
String? uuid,
int? batteryLevel,
String? productName,
List<DeviceSpaceModel>? spaces,
List<DeviceTagModel>? deviceTags,
DeviceSubSpace? deviceSubSpace,
}) {
return AllDevicesModel(
room: room ?? this.room,
subspace: subspace ?? this.subspace,
unit: unit ?? this.unit,
community: community ?? this.community,
productUuid: productUuid ?? this.productUuid,
productType: productType ?? this.productType,
permissionType: permissionType ?? this.permissionType,
activeTime: activeTime ?? this.activeTime,
category: category ?? this.category,
categoryName: categoryName ?? this.categoryName,
createTime: createTime ?? this.createTime,
gatewayId: gatewayId ?? this.gatewayId,
icon: icon ?? this.icon,
ip: ip ?? this.ip,
lat: lat ?? this.lat,
localKey: localKey ?? this.localKey,
lon: lon ?? this.lon,
model: model ?? this.model,
name: name ?? this.name,
nodeId: nodeId ?? this.nodeId,
online: online ?? this.online,
ownerId: ownerId ?? this.ownerId,
sub: sub ?? this.sub,
timeZone: timeZone ?? this.timeZone,
updateTime: updateTime ?? this.updateTime,
uuid: uuid ?? this.uuid,
batteryLevel: batteryLevel ?? this.batteryLevel,
productName: productName ?? this.productName,
spaces: spaces ?? this.spaces,
deviceTags: deviceTags ?? this.deviceTags,
deviceSubSpace: deviceSubSpace ?? this.deviceSubSpace,
);
}
}

View File

@ -23,6 +23,7 @@ class DeviceManagementBody extends StatelessWidget with HelperResponsiveLayout {
@override
Widget build(BuildContext context) {
return BlocBuilder<DeviceManagementBloc, DeviceManagementState>(
buildWhen: (previous, current) => previous != current,
builder: (context, state) {
List<AllDevicesModel> devicesToShow = [];
int selectedIndex = 0;
@ -31,7 +32,6 @@ class DeviceManagementBody extends StatelessWidget with HelperResponsiveLayout {
int lowBatteryCount = 0;
bool isControlButtonEnabled = false;
List<AllDevicesModel> selectedDevices = [];
if (state is DeviceManagementLoaded) {
devicesToShow = state.devices;
selectedIndex = state.selectedIndex;
@ -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,28 @@ 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)
.clearSnackBars();
ScaffoldMessenger.of(context)
.showSnackBar(
const SnackBar(
content: Text(
'This Device is Offline',
),
duration:
Duration(seconds: 2),
),
);
return;
}
if (selectedDevices.length == 1) {
showDialog(
context: context,
@ -171,7 +192,7 @@ class DeviceManagementBody extends StatelessWidget with HelperResponsiveLayout {
'Product Name',
'Device ID',
'Space Name',
'location',
'Location',
'Battery Level',
'Installation Date and Time',
'Status',
@ -223,7 +244,7 @@ class DeviceManagementBody extends StatelessWidget with HelperResponsiveLayout {
.map((device) => device.uuid!)
.toList(),
isEmpty: devicesToShow.isEmpty,
onSettingsPressed: (rowIndex) {
onSettingsPressed: (rowIndex) async {
final device = devicesToShow[rowIndex];
showDeviceSettingsSidebar(context, device);
},
@ -245,7 +266,7 @@ class DeviceManagementBody extends StatelessWidget with HelperResponsiveLayout {
barrierDismissible: true,
barrierLabel: "Device Settings",
transitionDuration: const Duration(milliseconds: 300),
pageBuilder: (context, anim1, anim2) {
pageBuilder: (_, anim1, anim2) {
return Align(
alignment: Alignment.centerRight,
child: Material(
@ -255,6 +276,7 @@ class DeviceManagementBody extends StatelessWidget with HelperResponsiveLayout {
child: DeviceSettingsPanel(
device: device,
onClose: () => Navigator.of(context).pop(),
deviceManagementBloc: context.read<DeviceManagementBloc>(),
),
),
),

View File

@ -0,0 +1,379 @@
import 'dart:async';
import 'package:bloc/bloc.dart';
import 'package:equatable/equatable.dart';
import 'package:firebase_database/firebase_database.dart';
import 'package:syncrow_web/pages/device_managment/all_devices/models/device_status.dart';
import 'package:syncrow_web/pages/device_managment/all_devices/models/factory_reset_model.dart';
import 'package:syncrow_web/pages/device_managment/curtain_module/models/curtain_module_model.dart';
import 'package:syncrow_web/services/batch_control_devices_service.dart';
import 'package:syncrow_web/services/control_device_service.dart';
import 'package:syncrow_web/services/devices_mang_api.dart';
part 'curtain_module_event.dart';
part 'curtain_module_state.dart';
class CurtainModuleBloc extends Bloc<CurtainModuleEvent, CurtainModuleState> {
final ControlDeviceService controlDeviceService;
final BatchControlDevicesService batchControlDevicesService;
StreamSubscription<DatabaseEvent>? _firebaseSubscription;
CurtainModuleBloc({
required this.controlDeviceService,
required this.batchControlDevicesService,
}) : super(CurtainModuleInitial()) {
on<FetchCurtainModuleStatusEvent>(_onFetchCurtainModuleStatusEvent);
on<SendCurtainPercentToApiEvent>(_onSendCurtainPercentToApiEvent);
on<OpenCurtainEvent>(_onOpenCurtainEvent);
on<CloseCurtainEvent>(_onCloseCurtainEvent);
on<StopCurtainEvent>(_onStopCurtainEvent);
on<ChangeTimerControlEvent>(_onChangeTimerControlEvent);
on<CurCalibrationEvent>(_onChageCurCalibrationEvent);
on<ChangeElecMachineryModeEvent>(_onChangeElecMachineryModeEvent);
on<ChangeControlBackEvent>(_onChangeControlBackEvent);
on<ChangeControlBackModeEvent>(_onChangeControlBackModeEvent);
on<ChangeCurtainModuleStatusEvent>(_onChangeCurtainModuleStatusEvent);
//batch
on<CurtainModuleFetchBatchStatusEvent>(_onFetchCurtainModuleBatchStatus);
on<SendCurtainBatchPercentToApiEvent>(_onSendCurtainBatchPercentToApiEvent);
on<OpenCurtainBatchEvent>(_onOpenCurtainBatchEvent);
on<CloseCurtainBatchEvent>(_onCloseCurtainBatchEvent);
on<StopCurtainBatchEvent>(_onStopCurtainBatchEvent);
on<CurtainModuleFactoryReset>(_onFactoryReset);
}
Future<void> _onFetchCurtainModuleStatusEvent(
FetchCurtainModuleStatusEvent event,
Emitter<CurtainModuleState> emit,
) async {
emit(CurtainModuleLoading());
final status = await DevicesManagementApi().getDeviceStatus(event.deviceId);
final result = Map.fromEntries(
status.status.map((element) => MapEntry(element.code, element.value)),
);
emit(CurtainModuleStatusLoaded(
curtainModuleStatus: CurtainModuleStatusModel.fromJson(result),
));
Map<String, dynamic> statusMap = {};
final ref =
FirebaseDatabase.instance.ref('device-status/${event.deviceId}');
final stream = ref.onValue;
stream.listen((DatabaseEvent DatabaseEvent) async {
if (DatabaseEvent.snapshot.value == null) return;
Map<dynamic, dynamic> usersMap =
DatabaseEvent.snapshot.value as Map<dynamic, dynamic>;
List<Status> statusList = [];
usersMap['status'].forEach((element) {
statusList.add(Status(code: element['code'], value: element['value']));
});
statusMap = {
for (final element in statusList) element.code: element.value,
};
if (!isClosed) {
add(
ChangeCurtainModuleStatusEvent(
deviceId: event.deviceId,
status: CurtainModuleStatusModel.fromJson(statusMap),
),
);
}
});
}
Future<void> _onChangeCurtainModuleStatusEvent(
ChangeCurtainModuleStatusEvent event,
Emitter<CurtainModuleState> emit,
) async {
emit(CurtainModuleLoading());
emit(CurtainModuleStatusLoaded(curtainModuleStatus: event.status));
}
Future<void> _onSendCurtainPercentToApiEvent(
SendCurtainPercentToApiEvent event,
Emitter<CurtainModuleState> emit,
) async {
try {
await controlDeviceService.controlDevice(
deviceUuid: event.deviceId,
status: event.status,
);
} catch (e) {
emit(CurtainModuleError(message: 'Failed to send control command: $e'));
}
}
Future<void> _onOpenCurtainEvent(
OpenCurtainEvent event,
Emitter<CurtainModuleState> emit,
) async {
try {
await controlDeviceService.controlDevice(
deviceUuid: event.deviceId,
status: Status(code: 'control', value: 'open'),
);
} catch (e) {
emit(CurtainModuleError(message: 'Failed to open curtain: $e'));
}
}
Future<void> _onCloseCurtainEvent(
CloseCurtainEvent event,
Emitter<CurtainModuleState> emit,
) async {
try {
await controlDeviceService.controlDevice(
deviceUuid: event.deviceId,
status: Status(code: 'control', value: 'close'),
);
} catch (e) {
emit(CurtainModuleError(message: 'Failed to close curtain: $e'));
}
}
Future<void> _onStopCurtainEvent(
StopCurtainEvent event,
Emitter<CurtainModuleState> emit,
) async {
try {
await controlDeviceService.controlDevice(
deviceUuid: event.deviceId,
status: Status(code: 'control', value: 'stop'),
);
} catch (e) {
emit(CurtainModuleError(message: 'Failed to stop curtain: $e'));
}
}
Future<void> _onChangeTimerControlEvent(
ChangeTimerControlEvent event,
Emitter<CurtainModuleState> emit,
) async {
try {
if (event.timControl < 10 || event.timControl > 120) {
emit(const CurtainModuleError(
message: 'Timer control value must be between 10 and 120'));
return;
}
await controlDeviceService.controlDevice(
deviceUuid: event.deviceId,
status: Status(
code: 'tr_timecon',
value: event.timControl,
),
);
} catch (e) {
emit(CurtainModuleError(message: 'Failed to change timer control: $e'));
}
}
Future<void> _onChageCurCalibrationEvent(
CurCalibrationEvent event,
Emitter<CurtainModuleState> emit,
) async {
try {
await controlDeviceService.controlDevice(
deviceUuid: event.deviceId,
status: Status(code: 'cur_calibration', value: 'start'),
);
} catch (e) {
emit(CurtainModuleError(message: 'Failed to start calibration: $e'));
}
}
Future<void> _onChangeElecMachineryModeEvent(
ChangeElecMachineryModeEvent event,
Emitter<CurtainModuleState> emit,
) async {
try {
await controlDeviceService.controlDevice(
deviceUuid: event.deviceId,
status: Status(
code: 'elec_machinery_mode',
value: event.elecMachineryMode,
),
);
} catch (e) {
emit(CurtainModuleError(message: 'Failed to change mode: $e'));
}
}
Future<void> _onChangeControlBackEvent(
ChangeControlBackEvent event,
Emitter<CurtainModuleState> emit,
) async {
try {
await controlDeviceService.controlDevice(
deviceUuid: event.deviceId,
status: Status(
code: 'control_back',
value: event.controlBack,
),
);
} catch (e) {
emit(CurtainModuleError(message: 'Failed to change control back: $e'));
}
}
Future<void> _onChangeControlBackModeEvent(
ChangeControlBackModeEvent event,
Emitter<CurtainModuleState> emit,
) async {
try {
await controlDeviceService.controlDevice(
deviceUuid: event.deviceId,
status: Status(
code: 'control_back_mode',
value: event.controlBackMode,
),
);
} catch (e) {
emit(CurtainModuleError(
message: 'Failed to change control back mode: $e'));
}
}
FutureOr<void> _onFetchCurtainModuleBatchStatus(
CurtainModuleFetchBatchStatusEvent event,
Emitter<CurtainModuleState> emit,
) async {
emit(CurtainModuleLoading());
try {
final status =
await DevicesManagementApi().getBatchStatus(event.devicesIds);
final result = Map.fromEntries(
status.status.map((element) => MapEntry(element.code, element.value)),
);
emit(CurtainModuleStatusLoaded(
curtainModuleStatus: CurtainModuleStatusModel.fromJson(result),
));
Map<String, dynamic> statusMap = {};
final ref = FirebaseDatabase.instance
.ref('device-status/${event.devicesIds.first}');
final stream = ref.onValue;
stream.listen((DatabaseEvent DatabaseEvent) async {
if (DatabaseEvent.snapshot.value == null) return;
Map<dynamic, dynamic> usersMap =
DatabaseEvent.snapshot.value as Map<dynamic, dynamic>;
List<Status> statusList = [];
usersMap['status'].forEach((element) {
statusList
.add(Status(code: element['code'], value: element['value']));
});
statusMap = {
for (final element in statusList) element.code: element.value,
};
if (!isClosed) {
add(
ChangeCurtainModuleStatusEvent(
deviceId: event.devicesIds.first,
status: CurtainModuleStatusModel.fromJson(statusMap),
),
);
}
});
} catch (e) {
emit(CurtainModuleError(message: e.toString()));
}
}
Future<void> _onSendCurtainBatchPercentToApiEvent(
SendCurtainBatchPercentToApiEvent event,
Emitter<CurtainModuleState> emit,
) async {
try {
await batchControlDevicesService.batchControlDevices(
uuids: event.devicesId,
code: event.status.code,
value: event.status.value,
);
} catch (e) {
emit(CurtainModuleError(message: 'Failed to send control command: $e'));
}
}
Future<void> _onOpenCurtainBatchEvent(
OpenCurtainBatchEvent event,
Emitter<CurtainModuleState> emit,
) async {
try {
await batchControlDevicesService.batchControlDevices(
uuids: event.devicesId,
code: 'control',
value: 'open',
);
} catch (e) {
emit(CurtainModuleError(message: 'Failed to open curtain: $e'));
}
}
Future<void> _onCloseCurtainBatchEvent(
CloseCurtainBatchEvent event,
Emitter<CurtainModuleState> emit,
) async {
try {
await batchControlDevicesService.batchControlDevices(
uuids: event.devicesId,
code: 'control',
value: 'close',
);
} catch (e) {
emit(CurtainModuleError(message: 'Failed to close curtain: $e'));
}
}
Future<void> _onStopCurtainBatchEvent(
StopCurtainBatchEvent event,
Emitter<CurtainModuleState> emit,
) async {
try {
await batchControlDevicesService.batchControlDevices(
uuids: event.devicesId,
code: 'control',
value: 'stop',
);
} catch (e) {
emit(CurtainModuleError(message: 'Failed to stop curtain: $e'));
}
}
Future<void> _onFactoryReset(
CurtainModuleFactoryReset event,
Emitter<CurtainModuleState> emit,
) async {
emit(CurtainModuleLoading());
try {
final response = await DevicesManagementApi().factoryReset(
event.factoryReset,
event.deviceId,
);
if (!response) {
emit(const CurtainModuleError(message: 'Failed'));
} else {
add(
FetchCurtainModuleStatusEvent(deviceId: event.deviceId),
);
}
} catch (e) {
emit(CurtainModuleError(message: e.toString()));
}
}
@override
Future<void> close() async {
await _firebaseSubscription?.cancel();
return super.close();
}
}

View File

@ -0,0 +1,193 @@
part of 'curtain_module_bloc.dart';
sealed class CurtainModuleEvent extends Equatable {
const CurtainModuleEvent();
@override
List<Object> get props => [];
}
class FetchCurtainModuleStatusEvent extends CurtainModuleEvent {
final String deviceId;
const FetchCurtainModuleStatusEvent({required this.deviceId});
@override
List<Object> get props => [deviceId];
}
class SendCurtainPercentToApiEvent extends CurtainModuleEvent {
final String deviceId;
final Status status;
const SendCurtainPercentToApiEvent({
required this.deviceId,
required this.status,
});
@override
List<Object> get props => [deviceId, status];
}
class OpenCurtainEvent extends CurtainModuleEvent {
final String deviceId;
const OpenCurtainEvent({required this.deviceId});
@override
List<Object> get props => [deviceId];
}
class CloseCurtainEvent extends CurtainModuleEvent {
final String deviceId;
const CloseCurtainEvent({required this.deviceId});
@override
List<Object> get props => [deviceId];
}
class StopCurtainEvent extends CurtainModuleEvent {
final String deviceId;
const StopCurtainEvent({required this.deviceId});
@override
List<Object> get props => [deviceId];
}
class ChangeTimerControlEvent extends CurtainModuleEvent {
final String deviceId;
final int timControl;
const ChangeTimerControlEvent({
required this.deviceId,
required this.timControl,
});
@override
List<Object> get props => [deviceId, timControl];
}
class CurCalibrationEvent extends CurtainModuleEvent {
final String deviceId;
const CurCalibrationEvent({
required this.deviceId,
});
@override
List<Object> get props => [deviceId];
}
class ChangeElecMachineryModeEvent extends CurtainModuleEvent {
final String deviceId;
final String elecMachineryMode;
const ChangeElecMachineryModeEvent({
required this.deviceId,
required this.elecMachineryMode,
});
@override
List<Object> get props => [deviceId, elecMachineryMode];
}
class ChangeControlBackEvent extends CurtainModuleEvent {
final String deviceId;
final String controlBack;
const ChangeControlBackEvent({
required this.deviceId,
required this.controlBack,
});
@override
List<Object> get props => [deviceId, controlBack];
}
class ChangeControlBackModeEvent extends CurtainModuleEvent {
final String deviceId;
final String controlBackMode;
const ChangeControlBackModeEvent({
required this.deviceId,
required this.controlBackMode,
});
@override
List<Object> get props => [deviceId, controlBackMode];
}
class ChangeCurtainModuleStatusEvent extends CurtainModuleEvent {
final String deviceId;
final CurtainModuleStatusModel status;
const ChangeCurtainModuleStatusEvent({
required this.deviceId,
required this.status,
});
@override
List<Object> get props => [deviceId, status];
}
///batch
class CurtainModuleFetchBatchStatusEvent extends CurtainModuleEvent {
final List<String> devicesIds;
const CurtainModuleFetchBatchStatusEvent(this.devicesIds);
@override
List<Object> get props => [devicesIds];
}
class SendCurtainBatchPercentToApiEvent extends CurtainModuleEvent {
final List<String> devicesId;
final Status status;
const SendCurtainBatchPercentToApiEvent({
required this.devicesId,
required this.status,
});
@override
List<Object> get props => [devicesId, status];
}
class OpenCurtainBatchEvent extends CurtainModuleEvent {
final List<String> devicesId;
const OpenCurtainBatchEvent({required this.devicesId});
@override
List<Object> get props => [devicesId];
}
class CloseCurtainBatchEvent extends CurtainModuleEvent {
final List<String> devicesId;
const CloseCurtainBatchEvent({required this.devicesId});
@override
List<Object> get props => [devicesId];
}
class StopCurtainBatchEvent extends CurtainModuleEvent {
final List<String> devicesId;
const StopCurtainBatchEvent({required this.devicesId});
@override
List<Object> get props => [devicesId];
}
class CurtainModuleFactoryReset extends CurtainModuleEvent {
final String deviceId;
final FactoryResetModel factoryReset;
const CurtainModuleFactoryReset(
{required this.deviceId, required this.factoryReset});
@override
List<Object> get props => [deviceId, factoryReset];
}

View File

@ -0,0 +1,37 @@
part of 'curtain_module_bloc.dart';
sealed class CurtainModuleState extends Equatable {
const CurtainModuleState();
@override
List<Object> get props => [];
}
class CurtainModuleInitial extends CurtainModuleState {}
class CurtainModuleLoading extends CurtainModuleState {}
class CurtainModuleError extends CurtainModuleState {
final String message;
const CurtainModuleError({required this.message});
@override
List<Object> get props => [message];
}
class CurtainModuleStatusLoaded extends CurtainModuleState {
final CurtainModuleStatusModel curtainModuleStatus;
const CurtainModuleStatusLoaded({required this.curtainModuleStatus});
@override
List<Object> get props => [curtainModuleStatus];
}
class CurtainModuleStatusUpdated extends CurtainModuleState {
final CurtainModuleStatusModel curtainModuleStatus;
const CurtainModuleStatusUpdated({required this.curtainModuleStatus});
@override
List<Object> get props => [curtainModuleStatus];
}

View File

@ -0,0 +1,53 @@
enum CurtainModuleControl {
open,
close,
stop,
}
// enum CurtainControlBackMode {
// foward,
// backward,
// }
class CurtainModuleStatusModel {
CurtainModuleControl control;
int percentControl;
String curCalibration;
// CurtainControlBackMode controlBackmode;
int trTimeControl;
String elecMachineryMode;
String controlBack;
CurtainModuleStatusModel({
required this.control,
required this.percentControl,
required this.curCalibration,
// required this.controlBackmode,
required this.trTimeControl,
required this.controlBack,
required this.elecMachineryMode,
});
factory CurtainModuleStatusModel.zero() => CurtainModuleStatusModel(
control: CurtainModuleControl.stop,
percentControl: 0,
// controlBackmode: CurtainControlBackMode.foward,
curCalibration: '',
trTimeControl: 0,
controlBack: '',
elecMachineryMode: '',
);
factory CurtainModuleStatusModel.fromJson(Map<String, dynamic> json) {
return CurtainModuleStatusModel(
control: CurtainModuleControl.values.firstWhere(
(e) => e.toString() == json['control'] as String,
orElse: () => CurtainModuleControl.stop,
),
percentControl: json['percent_control'] as int? ?? 0,
curCalibration: json['cur_calibration'] as String? ?? '',
trTimeControl: json['tr_timecon'] as int? ?? 0,
elecMachineryMode: json['elec_machinery_mode'] as String? ?? '',
controlBack: json['control_back'] as String? ?? '',
);
}
}

View File

@ -0,0 +1,80 @@
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:syncrow_web/pages/device_managment/all_devices/models/factory_reset_model.dart';
import 'package:syncrow_web/pages/device_managment/curtain_module/bloc/curtain_module_bloc.dart';
import 'package:syncrow_web/pages/device_managment/curtain_module/widgets/curtain_movment_widget.dart';
import 'package:syncrow_web/pages/device_managment/shared/batch_control/factory_reset.dart';
import 'package:syncrow_web/pages/device_managment/shared/icon_name_status_container.dart';
import 'package:syncrow_web/services/batch_control_devices_service.dart';
import 'package:syncrow_web/services/control_device_service.dart';
import 'package:syncrow_web/utils/color_manager.dart';
import 'package:syncrow_web/utils/constants/assets.dart';
class CurtainModuleBatchView extends StatelessWidget {
final List<String> devicesIds;
const CurtainModuleBatchView({
super.key,
required this.devicesIds,
});
@override
Widget build(BuildContext context) {
return BlocProvider(
create: (context) => CurtainModuleBloc(
controlDeviceService: RemoteControlDeviceService(),
batchControlDevicesService: RemoteBatchControlDevicesService())
..add(CurtainModuleFetchBatchStatusEvent(devicesIds)),
child: _buildStatusControls(context),
);
}
Widget _buildStatusControls(BuildContext context) {
return Padding(
padding: const EdgeInsets.only(left: 30),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
ControlCurtainMovementWidget(
devicesId: devicesIds,
),
const SizedBox(
height: 10,
),
SizedBox(
height: 120,
// width: 350,
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
// Expanded(
// child:
FactoryResetWidget(
callFactoryReset: () {
context.read<CurtainModuleBloc>().add(
CurtainModuleFactoryReset(
deviceId: devicesIds.first,
factoryReset:
FactoryResetModel(devicesUuid: devicesIds),
),
);
},
),
// ),
// Expanded(
// child: IconNameStatusContainer(
// isFullIcon: false,
// name: 'Firmware Update',
// icon: Assets.firmware,
// onTap: () {},
// status: false,
// textColor: ColorsManager.blackColor,
// ),
// )
],
),
),
],
),
);
}
}

View File

@ -0,0 +1,120 @@
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:syncrow_web/pages/device_managment/curtain_module/bloc/curtain_module_bloc.dart';
import 'package:syncrow_web/pages/device_managment/curtain_module/widgets/curtain_movment_widget.dart';
import 'package:syncrow_web/pages/device_managment/curtain_module/widgets/prefrences_dialog.dart';
import 'package:syncrow_web/pages/device_managment/schedule_device/schedule_widgets/schedual_view.dart';
import 'package:syncrow_web/pages/device_managment/schedule_device/schedule_widgets/schedule_control_button.dart';
import 'package:syncrow_web/pages/device_managment/shared/icon_name_status_container.dart';
import 'package:syncrow_web/services/batch_control_devices_service.dart';
import 'package:syncrow_web/services/control_device_service.dart';
import 'package:syncrow_web/utils/color_manager.dart';
import 'package:syncrow_web/utils/constants/assets.dart';
import 'package:syncrow_web/utils/helpers/responsice_layout_helper/responsive_layout_helper.dart';
class CurtainModuleItems extends StatelessWidget with HelperResponsiveLayout {
final String deviceId;
const CurtainModuleItems({
super.key,
required this.deviceId,
});
@override
Widget build(BuildContext context) {
return BlocProvider(
create: (context) => CurtainModuleBloc(
controlDeviceService: RemoteControlDeviceService(),
batchControlDevicesService: RemoteBatchControlDevicesService())
..add(FetchCurtainModuleStatusEvent(deviceId: deviceId)),
child: BlocBuilder<CurtainModuleBloc, CurtainModuleState>(
builder: (context, state) {
return _buildStatusControls(context);
},
),
);
}
Widget _buildStatusControls(BuildContext context) {
return Padding(
padding: const EdgeInsets.only(left: 30),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
ControlCurtainMovementWidget(
devicesId: [deviceId],
),
const SizedBox(
height: 10,
),
SizedBox(
height: 140,
width: 350,
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Expanded(
child: ScheduleControlButton(
onTap: () {
showDialog<void>(
context: context,
builder: (ctx) => BlocProvider.value(
value:
BlocProvider.of<CurtainModuleBloc>(context),
child: BuildScheduleView(
deviceUuid: deviceId,
category: 'CUR_2',
code: 'control',
),
));
},
mainText: '',
subtitle: 'Scheduling',
iconPath: Assets.scheduling,
),
),
const SizedBox(
width: 10,
),
Expanded(
child: BlocBuilder<CurtainModuleBloc, CurtainModuleState>(
builder: (context, state) {
if (state is CurtainModuleLoading) {
return const Center(
child: CircularProgressIndicator(),
);
} else if (state is CurtainModuleStatusLoaded) {
return IconNameStatusContainer(
isFullIcon: false,
name: 'Preferences',
icon: Assets.preferences,
onTap: () => showDialog(
context: context,
builder: (_) => BlocProvider.value(
value: context.read<CurtainModuleBloc>(),
child: CurtainModulePrefrencesDialog(
curtainModuleBloc:
context.watch<CurtainModuleBloc>(),
deviceId: deviceId,
curtainModuleStatusModel:
state.curtainModuleStatus,
),
),
),
status: false,
textColor: ColorsManager.blackColor,
);
} else {
return const SizedBox();
}
},
),
),
],
),
),
],
),
);
}
}

View File

@ -0,0 +1,48 @@
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:syncrow_web/pages/device_managment/curtain_module/bloc/curtain_module_bloc.dart';
import 'package:syncrow_web/pages/device_managment/curtain_module/widgets/accurate_dialog_widget.dart';
import 'package:syncrow_web/pages/device_managment/curtain_module/widgets/calibrate_completed_dialog.dart';
import 'package:syncrow_web/pages/device_managment/curtain_module/widgets/normal_text_body_for_dialog.dart';
class AccurteCalibratingDialog extends StatelessWidget {
final String deviceId;
final BuildContext parentContext;
const AccurteCalibratingDialog({
super.key,
required this.deviceId,
required this.parentContext,
});
@override
Widget build(_) {
return AlertDialog(
contentPadding: EdgeInsets.zero,
content: AccurateDialogWidget(
title: 'Calibrating',
body: const NormalTextBodyForDialog(
title: '',
step1:
'Click Close Button to make the Curtain run to Full Close and Position.',
step2: 'click Next to complete the Calibration.',
),
leftOnTap: () => Navigator.of(parentContext).pop(),
rightOnTap: () {
parentContext.read<CurtainModuleBloc>().add(
CurCalibrationEvent(
deviceId: deviceId,
),
);
Navigator.of(parentContext).pop();
showDialog(
context: parentContext,
builder: (_) => CalibrateCompletedDialog(
parentContext: parentContext,
deviceId: deviceId,
),
);
},
),
);
}
}

View File

@ -0,0 +1,40 @@
import 'package:flutter/material.dart';
import 'package:syncrow_web/pages/device_managment/curtain_module/widgets/accurate_calibrating_dialog.dart';
import 'package:syncrow_web/pages/device_managment/curtain_module/widgets/accurate_dialog_widget.dart';
import 'package:syncrow_web/pages/device_managment/curtain_module/widgets/normal_text_body_for_dialog.dart';
class AccurateCalibrationDialog extends StatelessWidget {
final String deviceId;
final BuildContext parentContext;
const AccurateCalibrationDialog({
super.key,
required this.deviceId,
required this.parentContext,
});
@override
Widget build(_) {
return AlertDialog(
contentPadding: EdgeInsets.zero,
content: AccurateDialogWidget(
title: 'Accurate Calibration',
body: const NormalTextBodyForDialog(
title: 'Prepare Calibration:',
step1: 'Run The Curtain to the Fully Open Position,and pause.',
step2: 'click Next to Start accurate calibration.',
),
leftOnTap: () => Navigator.of(parentContext).pop(),
rightOnTap: () {
Navigator.of(parentContext).pop();
showDialog(
context: parentContext,
builder: (_) => AccurteCalibratingDialog(
deviceId: deviceId,
parentContext: parentContext,
),
);
},
),
);
}
}

View File

@ -0,0 +1,106 @@
import 'package:flutter/material.dart';
import 'package:syncrow_web/utils/color_manager.dart';
class AccurateDialogWidget extends StatelessWidget {
final String title;
final Widget body;
final void Function() leftOnTap;
final void Function() rightOnTap;
const AccurateDialogWidget({
super.key,
required this.title,
required this.body,
required this.leftOnTap,
required this.rightOnTap,
});
@override
Widget build(BuildContext context) {
return SizedBox(
height: 250,
width: 500,
child: Column(
children: [
Expanded(
child: Column(
children: [
Padding(
padding: const EdgeInsets.all(10),
child: Text(
title,
style: TextStyle(
fontSize: 24,
fontWeight: FontWeight.bold,
color: ColorsManager.dialogBlueTitle,
),
),
),
const Divider(
indent: 60,
endIndent: 60,
),
],
),
),
Expanded(
child: body,
),
Expanded(
child: Column(
mainAxisAlignment: MainAxisAlignment.end,
children: [
const Divider(),
Row(
children: [
Expanded(
child: InkWell(
onTap: leftOnTap,
child: Container(
height: 40,
alignment: Alignment.center,
decoration: const BoxDecoration(
border: Border(
right: BorderSide(
color: ColorsManager.grayBorder,
),
),
),
child: const Text(
'Cancel',
style: TextStyle(color: ColorsManager.grayBorder),
),
),
),
),
Expanded(
child: InkWell(
onTap: rightOnTap,
child: Container(
height: 40,
alignment: Alignment.center,
decoration: const BoxDecoration(
border: Border(
right: BorderSide(
color: ColorsManager.grayBorder,
),
),
),
child: const Text(
'Next',
style: TextStyle(
color: ColorsManager.blueColor,
),
),
),
),
)
],
)
],
),
)
],
),
);
}
}

View File

@ -0,0 +1,89 @@
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/curtain_module/bloc/curtain_module_bloc.dart';
import 'package:syncrow_web/utils/color_manager.dart';
import 'package:syncrow_web/utils/constants/assets.dart';
class CalibrateCompletedDialog extends StatelessWidget {
final BuildContext parentContext;
final String deviceId;
const CalibrateCompletedDialog({
super.key,
required this.parentContext,
required this.deviceId,
});
@override
Widget build(_) {
return AlertDialog(
contentPadding: EdgeInsets.zero,
content: SizedBox(
height: 250,
width: 400,
child: Column(
children: [
Expanded(
child: Column(
children: [
Padding(
padding: const EdgeInsets.all(10),
child: Text(
'Calibration Completed',
style: TextStyle(
fontSize: 24,
fontWeight: FontWeight.bold,
color: ColorsManager.dialogBlueTitle,
),
),
),
const SizedBox(height: 5),
const Divider(
indent: 10,
endIndent: 10,
),
],
),
),
Expanded(
child: SvgPicture.asset(Assets.completedDoneIcon),
),
Expanded(
child: Column(
mainAxisAlignment: MainAxisAlignment.end,
children: [
const Divider(
indent: 10,
endIndent: 10,
),
InkWell(
onTap: () {
parentContext.read<CurtainModuleBloc>().add(
FetchCurtainModuleStatusEvent(
deviceId: deviceId,
),
);
Navigator.of(parentContext).pop();
Navigator.of(parentContext).pop();
},
child: Container(
height: 40,
width: double.infinity,
alignment: Alignment.center,
child: const Text(
'Close',
style: TextStyle(
color: ColorsManager.grayBorder,
),
),
),
)
],
),
)
],
),
),
);
}
}

View File

@ -0,0 +1,41 @@
import 'package:flutter/material.dart';
import 'package:flutter_svg/svg.dart';
import 'package:syncrow_web/utils/color_manager.dart';
class CurtainActionWidget extends StatelessWidget {
final String icon;
final void Function() onTap;
const CurtainActionWidget({
super.key,
required this.icon,
required this.onTap,
});
@override
Widget build(BuildContext context) {
return InkWell(
onTap: onTap,
child: ClipOval(
child: Container(
height: 60,
width: 60,
padding: const EdgeInsets.all(8),
color: ColorsManager.whiteColors,
child: ClipOval(
child: Container(
height: 60,
width: 60,
padding: const EdgeInsets.all(8),
color: ColorsManager.graysColor,
child: SvgPicture.asset(
icon,
width: 35,
height: 35,
fit: BoxFit.contain,
),
),
),
)),
);
}
}

View File

@ -0,0 +1,219 @@
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:syncrow_web/pages/device_managment/all_devices/models/device_status.dart';
import 'package:syncrow_web/pages/device_managment/curtain_module/bloc/curtain_module_bloc.dart';
import 'package:syncrow_web/pages/device_managment/curtain_module/models/curtain_module_model.dart';
import 'package:syncrow_web/pages/device_managment/curtain_module/widgets/curtain_action_widget.dart';
import 'package:syncrow_web/pages/device_managment/shared/device_controls_container.dart';
import 'package:syncrow_web/utils/color_manager.dart';
import 'package:syncrow_web/utils/constants/assets.dart';
class ControlCurtainMovementWidget extends StatelessWidget {
final List<String> devicesId;
const ControlCurtainMovementWidget({
super.key,
required this.devicesId,
});
@override
Widget build(BuildContext context) {
return SizedBox(
width: 550,
child: DeviceControlsContainer(
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
CurtainActionWidget(
icon: Assets.openCurtain,
onTap: () {
if (devicesId.length == 1) {
context.read<CurtainModuleBloc>().add(
OpenCurtainEvent(deviceId: devicesId.first),
);
} else {
context.read<CurtainModuleBloc>().add(
OpenCurtainBatchEvent(devicesId: devicesId),
);
}
},
),
const SizedBox(
width: 30,
),
CurtainActionWidget(
icon: Assets.pauseCurtain,
onTap: () {
if (devicesId.length == 1) {
context.read<CurtainModuleBloc>().add(
StopCurtainEvent(deviceId: devicesId.first),
);
} else {
context.read<CurtainModuleBloc>().add(
StopCurtainBatchEvent(devicesId: devicesId),
);
}
},
),
const SizedBox(
width: 30,
),
CurtainActionWidget(
icon: Assets.closeCurtain,
onTap: () {
if (devicesId.length == 1) {
context.read<CurtainModuleBloc>().add(
CloseCurtainEvent(deviceId: devicesId.first),
);
} else {
context.read<CurtainModuleBloc>().add(
CloseCurtainBatchEvent(devicesId: devicesId),
);
}
},
),
BlocBuilder<CurtainModuleBloc, CurtainModuleState>(
builder: (context, state) {
if (state is CurtainModuleError) {
return Center(
child: Text(
state.message,
style: const TextStyle(
color: ColorsManager.minBlueDot,
fontSize: 16,
),
),
);
} else if (state is CurtainModuleLoading) {
return const Center(
child: CircularProgressIndicator(
color: ColorsManager.minBlueDot,
),
);
} else if (state is CurtainModuleInitial) {
return const Center(
child: Text(
'No data available',
style: TextStyle(
color: ColorsManager.minBlueDot,
fontSize: 16,
),
),
);
} else if (state is CurtainModuleStatusLoaded) {
return CurtainSliderWidget(
status: state.curtainModuleStatus,
devicesId: devicesId,
);
} else {
return const Center(
child: Text(
'Unknown state',
style: TextStyle(
color: ColorsManager.minBlueDot,
fontSize: 16,
),
),
);
}
},
)
],
),
),
);
}
}
class CurtainSliderWidget extends StatefulWidget {
final CurtainModuleStatusModel status;
final List<String> devicesId;
const CurtainSliderWidget({
super.key,
required this.status,
required this.devicesId,
});
@override
State<CurtainSliderWidget> createState() => _CurtainSliderWidgetState();
}
class _CurtainSliderWidgetState extends State<CurtainSliderWidget> {
double? _localValue; // For temporary drag state
@override
Widget build(BuildContext context) {
// If user is dragging, use local value. Otherwise, use Firebase-synced state
final double currentSliderValue =
_localValue ?? widget.status.percentControl / 100;
return Column(
children: [
Text(
'${(currentSliderValue * 100).round()}%',
style: const TextStyle(
color: ColorsManager.minBlueDot,
fontSize: 25,
fontWeight: FontWeight.bold,
),
),
Slider(
value: currentSliderValue,
min: 0,
max: 1,
divisions: 10, // 10% step
activeColor: ColorsManager.minBlueDot,
thumbColor: ColorsManager.primaryColor,
inactiveColor: ColorsManager.whiteColors,
// Start dragging — use local control
onChangeStart: (_) {
setState(() {
_localValue = currentSliderValue;
});
},
// While dragging — update temporary value
onChanged: (value) {
final steppedValue = (value * 10).roundToDouble() / 10;
setState(() {
_localValue = steppedValue;
});
},
// On release — send API and return to Firebase-controlled state
onChangeEnd: (value) {
final int targetPercent = (value * 100).round();
if (widget.devicesId.length == 1) {
context.read<CurtainModuleBloc>().add(
SendCurtainPercentToApiEvent(
deviceId: widget.devicesId.first,
status: Status(
code: 'percent_control',
value: targetPercent,
),
),
);
} else {
context.read<CurtainModuleBloc>().add(
SendCurtainBatchPercentToApiEvent(
devicesId: widget.devicesId,
status: Status(
code: 'percent_control',
value: targetPercent,
),
),
);
}
// Revert back to Firebase-synced stream
setState(() {
_localValue = null;
});
},
),
],
);
}
}

View File

@ -0,0 +1,86 @@
import 'package:flutter/material.dart';
import 'package:syncrow_web/utils/color_manager.dart';
class NormalTextBodyForDialog extends StatelessWidget {
final String title;
final String step1;
final String step2;
const NormalTextBodyForDialog({
super.key,
required this.title,
required this.step1,
required this.step2,
});
@override
Widget build(BuildContext context) {
return Padding(
padding: EdgeInsetsGeometry.only(left: 15),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
if (title.isEmpty)
const SizedBox()
else
Expanded(
child: Text(
title,
style: const TextStyle(
color: ColorsManager.grayColor,
fontSize: 17,
),
),
),
Expanded(
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const SizedBox(
width: 10,
),
const Text('1. ',
style: TextStyle(
color: ColorsManager.grayColor,
fontSize: 17,
)),
SizedBox(
width: 450,
child: Text(
step1,
style: const TextStyle(
color: ColorsManager.grayColor,
fontSize: 17,
),
),
),
],
),
),
Expanded(
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const SizedBox(
width: 10,
),
const Text('2. ',
style: TextStyle(
color: ColorsManager.grayColor,
fontSize: 17,
)),
Text(
step2,
style: const TextStyle(
color: ColorsManager.grayColor,
fontSize: 17,
),
),
],
),
)
],
),
);
}
}

View File

@ -0,0 +1,27 @@
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:syncrow_web/utils/color_manager.dart';
class NumberInputField extends StatelessWidget {
final TextEditingController controller;
const NumberInputField({super.key, required this.controller});
@override
Widget build(BuildContext context) {
return TextField(
controller: controller,
keyboardType: TextInputType.number,
inputFormatters: [FilteringTextInputFormatter.digitsOnly],
decoration: const InputDecoration(
border: InputBorder.none,
isDense: true,
contentPadding: EdgeInsets.zero,
),
style: const TextStyle(
fontSize: 15,
color: ColorsManager.blackColor,
),
);
}
}

View File

@ -0,0 +1,79 @@
import 'package:flutter/material.dart';
import 'package:flutter_svg/svg.dart';
import 'package:syncrow_web/utils/color_manager.dart';
import 'package:syncrow_web/utils/constants/assets.dart';
import 'package:syncrow_web/web_layout/default_container.dart';
class PrefReversCardWidget extends StatelessWidget {
final void Function() onTap;
final String title;
final String body;
const PrefReversCardWidget({
super.key,
required this.title,
required this.body,
required this.onTap,
});
@override
Widget build(BuildContext context) {
return DefaultContainer(
padding: const EdgeInsets.all(18),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.start,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Expanded(
flex: 8,
child: Text(
title,
style: const TextStyle(
color: ColorsManager.grayBorder,
fontSize: 15,
),
),
),
const SizedBox(
width: 20,
),
Expanded(
flex: 2,
child: InkWell(
onTap: onTap,
child: Container(
padding: const EdgeInsets.all(3),
decoration: BoxDecoration(
color: ColorsManager.whiteColors,
borderRadius: const BorderRadius.horizontal(
left: Radius.circular(10),
right: Radius.circular(10)),
border: Border.all(color: ColorsManager.grayBorder)),
child: SvgPicture.asset(
Assets.reverseArrows,
height: 15,
),
),
),
)
],
),
SizedBox(
width: 100,
child: Text(
body,
style: const TextStyle(
color: ColorsManager.blackColor,
fontWeight: FontWeight.w500,
fontSize: 18,
),
),
),
],
),
);
}
}

View File

@ -0,0 +1,149 @@
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:syncrow_web/pages/device_managment/curtain_module/bloc/curtain_module_bloc.dart';
import 'package:syncrow_web/pages/device_managment/curtain_module/models/curtain_module_model.dart';
import 'package:syncrow_web/pages/device_managment/curtain_module/widgets/accurate_calibration_dialog.dart';
import 'package:syncrow_web/pages/device_managment/curtain_module/widgets/pref_revers_card_widget.dart';
import 'package:syncrow_web/pages/device_managment/curtain_module/widgets/quick_calibration_dialog.dart';
import 'package:syncrow_web/utils/color_manager.dart';
import 'package:syncrow_web/web_layout/default_container.dart';
class CurtainModulePrefrencesDialog extends StatelessWidget {
final CurtainModuleStatusModel curtainModuleStatusModel;
final String deviceId;
final CurtainModuleBloc curtainModuleBloc;
const CurtainModulePrefrencesDialog({
super.key,
required this.curtainModuleStatusModel,
required this.deviceId,
required this.curtainModuleBloc,
});
@override
Widget build(_) {
return AlertDialog(
backgroundColor: ColorsManager.CircleImageBackground,
contentPadding: const EdgeInsets.all(20),
title: Center(
child: Text(
'Preferences',
style: TextStyle(
color: ColorsManager.dialogBlueTitle,
fontSize: 24,
fontWeight: FontWeight.bold,
),
)),
content: BlocBuilder<CurtainModuleBloc, CurtainModuleState>(
bloc: curtainModuleBloc,
builder: (context, state) {
if (state is CurtainModuleLoading) {
return const Center(
child: CircularProgressIndicator(),
);
} else if (state is CurtainModuleStatusLoaded) {
return SizedBox(
height: 300,
width: 400,
child: GridView(
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 2,
childAspectRatio: 1.5,
mainAxisSpacing: 10,
crossAxisSpacing: 10,
),
children: [
PrefReversCardWidget(
title: state.curtainModuleStatus.controlBack,
body: 'Motor Steering',
onTap: () {
context.read<CurtainModuleBloc>().add(
ChangeControlBackEvent(
deviceId: deviceId,
controlBack:
state.curtainModuleStatus.controlBack ==
'forward'
? 'back'
: 'forward',
),
);
},
),
PrefReversCardWidget(
title: formatDeviceType(
state.curtainModuleStatus.elecMachineryMode),
body: 'Motor Mode',
onTap: () => context.read<CurtainModuleBloc>().add(
ChangeElecMachineryModeEvent(
deviceId: deviceId,
elecMachineryMode:
state.curtainModuleStatus.elecMachineryMode ==
'dry_contact'
? 'strong_power'
: 'dry_contact',
),
),
),
DefaultContainer(
padding: const EdgeInsets.all(12),
child: InkWell(
onTap: () => showDialog(
context: context,
builder: (_) => AccurateCalibrationDialog(
deviceId: deviceId,
parentContext: context,
),
),
child: const Column(
mainAxisAlignment: MainAxisAlignment.end,
children: [
Text('Accurte Calibration',
style: TextStyle(
fontSize: 18,
color: ColorsManager.blackColor,
)),
],
),
),
),
DefaultContainer(
padding: const EdgeInsets.all(12),
child: InkWell(
onTap: () => showDialog(
context: context,
builder: (_) => QuickCalibrationDialog(
timControl: state.curtainModuleStatus.trTimeControl,
deviceId: deviceId,
parentContext: context),
),
child: const Column(
mainAxisAlignment: MainAxisAlignment.end,
children: [
Text('Quick Calibration',
style: TextStyle(
fontSize: 18,
color: ColorsManager.blackColor,
)),
],
),
),
),
],
),
);
} else {
return const SizedBox();
}
},
),
);
}
String formatDeviceType(String raw) {
return raw
.split('_')
.map((word) => word.isNotEmpty
? '${word[0].toUpperCase()}${word.substring(1)}'
: '')
.join(' ');
}
}

View File

@ -0,0 +1,144 @@
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:syncrow_web/pages/device_managment/curtain_module/bloc/curtain_module_bloc.dart';
import 'package:syncrow_web/pages/device_managment/curtain_module/widgets/accurate_dialog_widget.dart';
import 'package:syncrow_web/pages/device_managment/curtain_module/widgets/calibrate_completed_dialog.dart';
import 'package:syncrow_web/pages/device_managment/curtain_module/widgets/number_input_textfield.dart';
import 'package:syncrow_web/utils/color_manager.dart';
class QuickCalibratingDialog extends StatefulWidget {
final int timControl;
final String deviceId;
final BuildContext parentContext;
const QuickCalibratingDialog({
super.key,
required this.timControl,
required this.deviceId,
required this.parentContext,
});
@override
State<QuickCalibratingDialog> createState() => _QuickCalibratingDialogState();
}
class _QuickCalibratingDialogState extends State<QuickCalibratingDialog> {
late TextEditingController _controller;
String? _errorText;
void _onRightTap() {
final value = int.tryParse(_controller.text);
if (value == null || value < 10 || value > 120) {
setState(() {
_errorText = 'Number should be between 10 and 120';
});
return;
}
setState(() {
_errorText = null;
});
widget.parentContext.read<CurtainModuleBloc>().add(
ChangeTimerControlEvent(
deviceId: widget.deviceId,
timControl: value,
),
);
Navigator.of(widget.parentContext).pop();
showDialog(
context: widget.parentContext,
builder: (_) => CalibrateCompletedDialog(
parentContext: widget.parentContext,
deviceId: widget.deviceId,
),
);
}
@override
void initState() {
_controller = TextEditingController(text: widget.timControl.toString());
super.initState();
}
@override
Widget build(_) {
return AlertDialog(
contentPadding: EdgeInsets.zero,
content: AccurateDialogWidget(
title: 'Calibrating',
body: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Expanded(
child: Align(
alignment: Alignment.center,
child: Padding(
padding: EdgeInsets.only(right: 75),
child: Text(
'1.please Enter the Travel Time:',
style: TextStyle(color: ColorsManager.lightGrayColor),
),
),
),
),
Expanded(
child: Align(
alignment: Alignment.center,
child: Container(
width: 110,
padding: const EdgeInsets.all(5),
decoration: BoxDecoration(
color: ColorsManager.whiteColors,
borderRadius: BorderRadius.circular(12),
),
child: Row(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Expanded(
child: NumberInputField(controller: _controller),
),
Expanded(
child: Text(
'seconds',
style: TextStyle(
fontSize: 12,
color: ColorsManager.dialogBlueTitle,
fontWeight: FontWeight.bold,
),
),
),
],
),
),
),
),
if (_errorText != null)
Expanded(
child: Padding(
padding: const EdgeInsets.only(top: 8.0),
child: Text(
_errorText!,
style: const TextStyle(
color: ColorsManager.red,
fontSize: 14,
),
),
),
),
const Expanded(
child: Align(
alignment: Alignment.center,
child: Text(
'2.click Next to Complete the calibration',
style: TextStyle(color: ColorsManager.lightGrayColor),
),
),
)
],
),
leftOnTap: () => Navigator.of(widget.parentContext).pop(),
rightOnTap: _onRightTap,
),
);
}
}

View File

@ -0,0 +1,44 @@
import 'package:flutter/material.dart';
import 'package:syncrow_web/pages/device_managment/curtain_module/widgets/accurate_dialog_widget.dart';
import 'package:syncrow_web/pages/device_managment/curtain_module/widgets/normal_text_body_for_dialog.dart';
import 'package:syncrow_web/pages/device_managment/curtain_module/widgets/quick_calibrating_dialog.dart';
class QuickCalibrationDialog extends StatelessWidget {
final int timControl;
final String deviceId;
final BuildContext parentContext;
const QuickCalibrationDialog({
super.key,
required this.timControl,
required this.deviceId,
required this.parentContext,
});
@override
Widget build(_) {
return AlertDialog(
contentPadding: EdgeInsets.zero,
content: AccurateDialogWidget(
title: 'Quick Calibration',
body: const NormalTextBodyForDialog(
title: 'Prepare Calibration:',
step1:
'Confirm that the curtain is in the fully closed and suspended state.',
step2: 'click Next to Start calibration.',
),
leftOnTap: () => Navigator.of(parentContext).pop(),
rightOnTap: () {
Navigator.of(parentContext).pop();
showDialog(
context: parentContext,
builder: (_) => QuickCalibratingDialog(
timControl: timControl,
deviceId: deviceId,
parentContext: parentContext,
),
);
},
),
);
}
}

View File

@ -19,11 +19,14 @@ class DeviceManagementContent extends StatelessWidget {
required this.device,
required this.subSpaces,
required this.deviceInfo,
required this.deviceManagementBloc,
});
final AllDevicesModel device;
final List<SubSpaceModel> subSpaces;
final DeviceInfoModel deviceInfo;
final DeviceManagementBloc deviceManagementBloc;
@override
Widget build(BuildContext context) {
@ -87,6 +90,11 @@ class DeviceManagementContent extends StatelessWidget {
),
);
});
deviceManagementBloc.add(UpdateSubSpaceName(
subspaceId: selectedSubSpace.id!,
deviceId: device.uuid!,
newSubSpaceName: selectedSubSpace.name ?? ''));
}
},
child: infoRow(

View File

@ -1,13 +1,15 @@
import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_svg/flutter_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/bloc/setting_bloc_state.dart';
import 'package:syncrow_web/pages/device_managment/device_setting/device_icon_type_helper.dart';
import 'package:syncrow_web/pages/device_managment/device_setting/device_management_content.dart';
import 'package:syncrow_web/pages/device_managment/device_setting/remove_device_widget.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/bloc/setting_bloc_bloc.dart';
import 'package:syncrow_web/pages/device_managment/device_setting/bloc/setting_bloc_state.dart';
import 'package:syncrow_web/pages/device_managment/device_setting/settings_model/sub_space_model.dart';
import 'package:syncrow_web/utils/color_manager.dart';
import 'package:syncrow_web/utils/constants/assets.dart';
@ -17,7 +19,13 @@ import 'package:syncrow_web/web_layout/default_container.dart';
class DeviceSettingsPanel extends StatelessWidget {
final VoidCallback? onClose;
final AllDevicesModel device;
const DeviceSettingsPanel({super.key, this.onClose, required this.device});
final DeviceManagementBloc deviceManagementBloc;
const DeviceSettingsPanel({
super.key,
this.onClose,
required this.device,
required this.deviceManagementBloc,
});
@override
Widget build(BuildContext context) {
@ -71,10 +79,10 @@ class DeviceSettingsPanel extends StatelessWidget {
'Device Settings',
style: context.theme.textTheme.titleLarge!
.copyWith(
fontWeight: FontWeight.w700,
fontWeight: FontWeight.w700,
color: ColorsManager.vividBlue
.withOpacity(0.7),
fontSize: 24),
fontSize: 24),
),
],
),
@ -134,8 +142,14 @@ class DeviceSettingsPanel extends StatelessWidget {
onFieldSubmitted: (value) {
_bloc.add(const ChangeNameEvent(
value: false));
deviceManagementBloc
..add(UpdateDeviceName(
deviceId: device.uuid!,
newName: _bloc
.nameController
.text))..add(ResetSelectedDevices());
},
decoration: InputDecoration(
decoration:const InputDecoration(
isDense: true,
contentPadding: EdgeInsets.zero,
border: InputBorder.none,
@ -157,7 +171,7 @@ class DeviceSettingsPanel extends StatelessWidget {
onTap: () {
_bloc.add(
const ChangeNameEvent(
value: true));
value: true));
},
child: SvgPicture.asset(
Assets
@ -190,6 +204,7 @@ class DeviceSettingsPanel extends StatelessWidget {
device: device,
subSpaces: subSpaces.cast<SubSpaceModel>(),
deviceInfo: deviceInfo,
deviceManagementBloc: deviceManagementBloc,
),
const SizedBox(height: 32),
RemoveDeviceWidget(bloc: _bloc),

View File

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

View File

@ -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
));
}
}
@ -257,7 +265,7 @@ class ScheduleBloc extends Bloc<ScheduleEvent, ScheduleState> {
category: event.category,
deviceId: deviceId,
time: getTimeStampWithoutSeconds(dateTime).toString(),
code: event.category,
code: event.code ?? event.category,
value: event.functionOn,
days: event.selectedDays);
if (success) {
@ -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) {

View File

@ -70,17 +70,19 @@ class ScheduleAddEvent extends ScheduleEvent {
final String category;
final String time;
final List<String> selectedDays;
final bool functionOn;
final dynamic functionOn;
final String? code;
const ScheduleAddEvent({
required this.category,
required this.time,
required this.selectedDays,
required this.functionOn,
required this.code,
});
@override
List<Object> get props => [category, time, selectedDays, functionOn];
List<Object?> get props => [category, time, selectedDays, functionOn, code];
}
class ScheduleEditEvent extends ScheduleEvent {
@ -146,14 +148,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 {

View File

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

View File

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

View File

@ -1,5 +1,6 @@
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:syncrow_web/pages/device_managment/all_devices/models/device_status.dart';
import 'package:syncrow_web/pages/device_managment/schedule_device/bloc/schedule_bloc.dart';
import 'package:syncrow_web/pages/device_managment/schedule_device/schedule_widgets/count_down_button.dart';
import 'package:syncrow_web/pages/device_managment/schedule_device/schedule_widgets/count_down_inching_view.dart';
@ -9,13 +10,19 @@ import 'package:syncrow_web/pages/device_managment/schedule_device/schedule_widg
import 'package:syncrow_web/pages/device_managment/schedule_device/schedule_widgets/schedule_mode_buttons.dart';
import 'package:syncrow_web/pages/device_managment/schedule_device/schedule_widgets/schedule_mode_selector.dart';
import 'package:syncrow_web/pages/device_managment/water_heater/helper/add_schedule_dialog_helper.dart';
import 'package:syncrow_web/pages/device_managment/water_heater/models/schedule_entry.dart';
import 'package:syncrow_web/pages/device_managment/water_heater/models/water_heater_status_model.dart';
class BuildScheduleView extends StatelessWidget {
const BuildScheduleView(
{super.key, required this.deviceUuid, required this.category});
const BuildScheduleView({
super.key,
required this.deviceUuid,
required this.category,
this.code,
});
final String deviceUuid;
final String category;
final String? code;
@override
Widget build(BuildContext context) {
@ -57,13 +64,21 @@ class BuildScheduleView extends StatelessWidget {
final entry = await ScheduleDialogHelper
.showAddScheduleDialog(
context,
schedule: null,
schedule: ScheduleEntry(
category: category,
time: '',
function: Status(
code: code.toString(), value: null),
days: [],
),
isEdit: false,
code: code,
);
if (entry != null) {
context.read<ScheduleBloc>().add(
ScheduleAddEvent(
category: entry.category,
category: category,
code: entry.function.code,
time: entry.time,
functionOn: entry.function.value,
selectedDays: entry.days,
@ -74,7 +89,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(

View File

@ -162,11 +162,18 @@ class _ScheduleTableView extends StatelessWidget {
child: GestureDetector(
behavior: HitTestBehavior.opaque,
onTap: () {
bool temp;
if (schedule.category == 'CUR_2') {
temp = schedule.function.value == 'open' ? true : false;
} else {
temp = schedule.function.value as bool;
}
context.read<ScheduleBloc>().add(
ScheduleUpdateEntryEvent(
category: schedule.category,
scheduleId: schedule.scheduleId,
functionOn: schedule.function.value,
functionOn: temp,
// schedule.function.value,
enable: !schedule.enable,
),
);
@ -188,7 +195,10 @@ class _ScheduleTableView extends StatelessWidget {
child: Text(_getSelectedDays(
ScheduleModel.parseSelectedDays(schedule.days)))),
Center(child: Text(formatIsoStringToTime(schedule.time, context))),
Center(child: Text(schedule.function.value ? 'On' : 'Off')),
if (schedule.category == 'CUR_2')
Center(child: Text(schedule.function.value))
else
Center(child: Text(schedule.function.value ? 'On' : 'Off')),
Center(
child: Wrap(
runAlignment: WrapAlignment.center,

View File

@ -4,7 +4,8 @@ import 'package:syncrow_web/pages/device_managment/all_devices/models/devices_mo
import 'package:syncrow_web/utils/color_manager.dart';
import 'package:syncrow_web/utils/extension/build_context_x.dart';
class DeviceBatchControlDialog extends StatelessWidget with RouteControlsBasedCode {
class DeviceBatchControlDialog extends StatelessWidget
with RouteControlsBasedCode {
final List<AllDevicesModel> devices;
const DeviceBatchControlDialog({super.key, required this.devices});
@ -18,7 +19,7 @@ class DeviceBatchControlDialog extends StatelessWidget with RouteControlsBasedCo
borderRadius: BorderRadius.circular(20),
),
child: SizedBox(
width: devices.length < 2 ? 500 : 800,
width: devices.length < 2 ? 600 : 800,
// height: context.screenHeight * 0.7,
child: SingleChildScrollView(
child: Padding(

View File

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

View File

@ -17,14 +17,21 @@ class ScheduleDialogHelper {
BuildContext context, {
ScheduleEntry? schedule,
bool isEdit = false,
String? code,
}) {
bool temp;
if (schedule?.category == 'CUR_2') {
temp = schedule!.function.value == 'open' ? true : false;
} else {
temp = schedule!.function.value;
}
final initialTime = schedule != null
? _convertStringToTimeOfDay(schedule.time)
: TimeOfDay.now();
final initialDays = schedule != null
? _convertDaysStringToBooleans(schedule.days)
: List.filled(7, false);
bool? functionOn = schedule?.function.value ?? true;
bool? functionOn = temp;
TimeOfDay selectedTime = initialTime;
List<bool> selectedDays = List.of(initialDays);
@ -96,7 +103,8 @@ class ScheduleDialogHelper {
setState(() => selectedDays[i] = v);
}),
const SizedBox(height: 16),
_buildFunctionSwitch(ctx, functionOn!, (v) {
_buildFunctionSwitch(schedule!.category, ctx, functionOn!,
(v) {
setState(() => functionOn = v);
}),
],
@ -115,10 +123,21 @@ class ScheduleDialogHelper {
width: 100,
child: ElevatedButton(
onPressed: () {
dynamic temp;
if (schedule?.category == 'CUR_2') {
temp = functionOn! ? 'open' : 'close';
} else {
temp = functionOn;
}
print(temp);
final entry = ScheduleEntry(
category: schedule?.category ?? 'switch_1',
time: _formatTimeOfDayToISO(selectedTime),
function: Status(code: 'switch_1', value: functionOn),
function: Status(
code: code ?? 'switch_1',
value: temp,
// functionOn,
),
days: _convertSelectedDaysToStrings(selectedDays),
scheduleId: schedule?.scheduleId,
);
@ -185,7 +204,7 @@ class ScheduleDialogHelper {
}
static Widget _buildFunctionSwitch(
BuildContext ctx, bool isOn, Function(bool) onChanged) {
String categor, BuildContext ctx, bool isOn, Function(bool) onChanged) {
return Row(
children: [
Text(
@ -199,14 +218,14 @@ class ScheduleDialogHelper {
groupValue: isOn,
onChanged: (val) => onChanged(true),
),
const Text('On'),
Text(categor == 'CUR_2' ? 'open' : 'On'),
const SizedBox(width: 10),
Radio<bool>(
value: false,
groupValue: isOn,
onChanged: (val) => onChanged(false),
),
const Text('Off'),
Text(categor == 'CUR_2' ? 'close' : 'Off'),
],
);
}

View File

@ -58,7 +58,9 @@ class CurtainHelper {
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
const DialogHeader('AC Functions'),
DialogHeader(dialogType == 'THEN'
? 'Curtain Functions'
: 'Curtain Conditions'),
Expanded(
child: Row(
crossAxisAlignment: CrossAxisAlignment.stretch,

View File

@ -0,0 +1,6 @@
class SpaceConnectionModel {
final String from;
final String to;
const SpaceConnectionModel({required this.from, required this.to});
}

View File

@ -0,0 +1,61 @@
import 'package:flutter/material.dart';
import 'package:syncrow_web/pages/space_management_v2/main_module/models/space_connection_model.dart';
import 'package:syncrow_web/utils/color_manager.dart';
class SpacesConnectionsArrowPainter extends CustomPainter {
final List<SpaceConnectionModel> connections;
final Map<String, Offset> positions;
final double cardWidth = 150.0;
final double cardHeight = 90.0;
final Set<String> highlightedUuids;
SpacesConnectionsArrowPainter({
required this.connections,
required this.positions,
required this.highlightedUuids,
});
@override
void paint(Canvas canvas, Size size) {
for (final connection in connections) {
final isSelected = highlightedUuids.contains(connection.from) ||
highlightedUuids.contains(connection.to);
final paint = Paint()
..color = isSelected
? ColorsManager.blackColor
: ColorsManager.blackColor.withValues(alpha: 0.5)
..strokeWidth = 2.0
..style = PaintingStyle.stroke;
final from = positions[connection.from];
final to = positions[connection.to];
if (from != null && to != null) {
final startPoint =
Offset(from.dx + cardWidth / 2, from.dy + cardHeight - 10);
final endPoint = Offset(to.dx + cardWidth / 2, to.dy);
final path = Path()..moveTo(startPoint.dx, startPoint.dy);
final controlPoint1 = Offset(startPoint.dx, startPoint.dy + 20);
final controlPoint2 = Offset(endPoint.dx, endPoint.dy - 60);
path.cubicTo(controlPoint1.dx, controlPoint1.dy, controlPoint2.dx,
controlPoint2.dy, endPoint.dx, endPoint.dy);
canvas.drawPath(path, paint);
final circlePaint = Paint()
..color = isSelected
? ColorsManager.blackColor
: ColorsManager.blackColor.withValues(alpha: 0.5)
..style = PaintingStyle.fill
..blendMode = BlendMode.srcIn;
canvas.drawCircle(endPoint, 4, circlePaint);
}
}
}
@override
bool shouldRepaint(covariant CustomPainter oldDelegate) => true;
}

View File

@ -0,0 +1,24 @@
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:syncrow_web/pages/space_management_v2/modules/communities/presentation/bloc/communities_bloc.dart';
import 'package:syncrow_web/pages/space_management_v2/modules/communities/presentation/communities_tree_selection_bloc/communities_tree_selection_bloc.dart';
import 'package:syncrow_web/pages/space_management_v2/modules/create_community/presentation/create_community_dialog.dart';
abstract final class SpaceManagementCommunityDialogHelper {
static void showCreateDialog(BuildContext context) {
showDialog<void>(
context: context,
builder: (_) => CreateCommunityDialog(
title: const SelectableText('Community Name'),
onCreateCommunity: (community) {
context.read<CommunitiesBloc>().add(
InsertCommunity(community),
);
context.read<CommunitiesTreeSelectionBloc>().add(
SelectCommunityEvent(community: community),
);
},
),
);
}
}

View File

@ -0,0 +1,280 @@
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:syncrow_web/pages/space_management_v2/main_module/models/space_connection_model.dart';
import 'package:syncrow_web/pages/space_management_v2/main_module/painters/spaces_connections_arrow_painter.dart';
import 'package:syncrow_web/pages/space_management_v2/main_module/widgets/space_card_widget.dart';
import 'package:syncrow_web/pages/space_management_v2/main_module/widgets/space_cell.dart';
import 'package:syncrow_web/pages/space_management_v2/modules/communities/domain/models/community_model.dart';
import 'package:syncrow_web/pages/space_management_v2/modules/communities/domain/models/space_model.dart';
import 'package:syncrow_web/pages/space_management_v2/modules/communities/presentation/communities_tree_selection_bloc/communities_tree_selection_bloc.dart';
import 'package:syncrow_web/pages/space_management_v2/modules/space_details/presentation/helpers/space_details_dialog_helper.dart';
class CommunityStructureCanvas extends StatefulWidget {
const CommunityStructureCanvas({
required this.community,
required this.selectedSpace,
super.key,
});
final CommunityModel community;
final SpaceModel? selectedSpace;
@override
State<CommunityStructureCanvas> createState() => _CommunityStructureCanvasState();
}
class _CommunityStructureCanvasState extends State<CommunityStructureCanvas>
with SingleTickerProviderStateMixin {
final Map<String, Offset> _positions = {};
final double _cardWidth = 150.0;
final double _cardHeight = 90.0;
final double _horizontalSpacing = 150.0;
final double _verticalSpacing = 120.0;
late TransformationController _transformationController;
late AnimationController _animationController;
@override
void initState() {
_transformationController = TransformationController();
_animationController = AnimationController(
vsync: this,
duration: const Duration(milliseconds: 150),
);
super.initState();
}
@override
void didUpdateWidget(covariant CommunityStructureCanvas oldWidget) {
super.didUpdateWidget(oldWidget);
if (widget.selectedSpace?.uuid != oldWidget.selectedSpace?.uuid) {
WidgetsBinding.instance.addPostFrameCallback((_) {
if (mounted) {
_animateToSpace(widget.selectedSpace);
}
});
}
}
@override
void dispose() {
_transformationController.dispose();
_animationController.dispose();
super.dispose();
}
Set<String> _getAllDescendantUuids(SpaceModel space) {
final uuids = <String>{};
for (final child in space.children) {
uuids.add(child.uuid);
uuids.addAll(_getAllDescendantUuids(child));
}
return uuids;
}
void _runAnimation(Matrix4 target) {
final animation = Matrix4Tween(
begin: _transformationController.value,
end: target,
).animate(_animationController);
void listener() {
_transformationController.value = animation.value;
}
animation.addListener(listener);
_animationController.forward(from: 0).whenCompleteOrCancel(() {
animation.removeListener(listener);
});
}
void _animateToSpace(SpaceModel? space) {
if (space == null) {
_runAnimation(Matrix4.identity());
return;
}
final position = _positions[space.uuid];
if (position == null) return;
const scale = 1.5;
final viewSize = context.size;
if (viewSize == null) return;
final x = -position.dx * scale + (viewSize.width / 2) - (_cardWidth * scale / 2);
final y =
-position.dy * scale + (viewSize.height / 2) - (_cardHeight * scale / 2);
final matrix = Matrix4.identity()
..translate(x, y)
..scale(scale);
_runAnimation(matrix);
}
void _onSpaceTapped(SpaceModel? space) {
context.read<CommunitiesTreeSelectionBloc>().add(
SelectSpaceEvent(community: widget.community, space: space),
);
}
void _resetSelectionAndZoom() {
context.read<CommunitiesTreeSelectionBloc>().add(
SelectSpaceEvent(
community: widget.community,
space: null,
),
);
}
void _calculateLayout(
List<SpaceModel> spaces,
int depth,
Map<int, double> levelXOffset,
) {
for (final space in spaces) {
double childSubtreeWidth = 0;
if (space.children.isNotEmpty) {
_calculateLayout(space.children, depth + 1, levelXOffset);
final firstChildPos = _positions[space.children.first.uuid];
final lastChildPos = _positions[space.children.last.uuid];
if (firstChildPos != null && lastChildPos != null) {
childSubtreeWidth = (lastChildPos.dx + _cardWidth) - firstChildPos.dx;
}
}
final currentX = levelXOffset.putIfAbsent(depth, () => 0.0);
double? x;
if (space.children.isNotEmpty) {
final firstChildPos = _positions[space.children.first.uuid]!;
x = firstChildPos.dx + (childSubtreeWidth - _cardWidth) / 2;
} else {
x = currentX;
}
if (x < currentX) {
final shiftX = currentX - x;
_shiftSubtree(space, shiftX);
final keysToShift = levelXOffset.keys.where((d) => d > depth).toList();
for (final key in keysToShift) {
levelXOffset[key] = levelXOffset[key]! + shiftX;
}
x += shiftX;
}
final y = depth * (_verticalSpacing + _cardHeight);
_positions[space.uuid] = Offset(x, y);
levelXOffset[depth] = x + _cardWidth + _horizontalSpacing;
}
}
void _shiftSubtree(SpaceModel space, double shiftX) {
if (_positions.containsKey(space.uuid)) {
_positions[space.uuid] = _positions[space.uuid]!.translate(shiftX, 0);
}
for (final child in space.children) {
_shiftSubtree(child, shiftX);
}
}
List<Widget> _buildTreeWidgets() {
_positions.clear();
final community = widget.community;
_calculateLayout(community.spaces, 0, {});
final selectedSpace = widget.selectedSpace;
final highlightedUuids = <String>{};
if (selectedSpace != null) {
highlightedUuids.add(selectedSpace.uuid);
highlightedUuids.addAll(_getAllDescendantUuids(selectedSpace));
}
final widgets = <Widget>[];
final connections = <SpaceConnectionModel>[];
_generateWidgets(community.spaces, widgets, connections, highlightedUuids);
return [
CustomPaint(
painter: SpacesConnectionsArrowPainter(
connections: connections,
positions: _positions,
highlightedUuids: highlightedUuids,
),
child: Stack(alignment: AlignmentDirectional.center, children: widgets),
),
];
}
void _generateWidgets(
List<SpaceModel> spaces,
List<Widget> widgets,
List<SpaceConnectionModel> connections,
Set<String> highlightedUuids,
) {
for (final space in spaces) {
final position = _positions[space.uuid];
if (position == null) continue;
final isHighlighted = highlightedUuids.contains(space.uuid);
final hasNoSelectedSpace = widget.selectedSpace == null;
widgets.add(
Positioned(
left: position.dx,
top: position.dy,
width: _cardWidth,
height: _cardHeight,
child: SpaceCardWidget(
buildSpaceContainer: () {
return Opacity(
opacity: hasNoSelectedSpace || isHighlighted ? 1.0 : 0.5,
child: Tooltip(
message: space.spaceName,
preferBelow: false,
child: SpaceCell(
onTap: () => _onSpaceTapped(space),
icon: space.icon,
name: space.spaceName,
),
),
);
},
onTap: () => SpaceDetailsDialogHelper.showCreate(context),
),
),
);
for (final child in space.children) {
connections.add(
SpaceConnectionModel(from: space.uuid, to: child.uuid),
);
}
_generateWidgets(space.children, widgets, connections, highlightedUuids);
}
}
@override
Widget build(BuildContext context) {
final treeWidgets = _buildTreeWidgets();
return InteractiveViewer(
transformationController: _transformationController,
boundaryMargin: EdgeInsets.symmetric(
horizontal: MediaQuery.sizeOf(context).width * 0.3,
vertical: MediaQuery.sizeOf(context).height * 0.3,
),
minScale: 0.5,
maxScale: 3.0,
constrained: false,
child: GestureDetector(
onTap: _resetSelectionAndZoom,
child: SizedBox(
width: MediaQuery.sizeOf(context).width * 5,
height: MediaQuery.sizeOf(context).height * 5,
child: Stack(children: treeWidgets),
),
),
);
}
}

View File

@ -0,0 +1,50 @@
import 'package:flutter/material.dart';
import 'package:syncrow_web/utils/color_manager.dart';
import 'package:syncrow_web/utils/extension/build_context_x.dart';
class CommunityTemplateCell extends StatelessWidget {
const CommunityTemplateCell({
super.key,
required this.onTap,
required this.title,
});
final void Function() onTap;
final Widget title;
@override
Widget build(BuildContext context) {
return GestureDetector(
onTap: onTap,
child: Column(
spacing: 8,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Expanded(
child: AspectRatio(
aspectRatio: 2.0,
child: Container(
decoration: ShapeDecoration(
shape: RoundedRectangleBorder(
side: const BorderSide(
width: 4,
strokeAlign: BorderSide.strokeAlignOutside,
color: ColorsManager.borderColor,
),
borderRadius: BorderRadius.circular(5),
),
),
),
),
),
DefaultTextStyle(
style: context.textTheme.bodyLarge!.copyWith(
color: ColorsManager.blackColor,
),
child: title,
),
],
),
);
}
}

View File

@ -0,0 +1,43 @@
import 'package:flutter/material.dart';
import 'package:syncrow_web/pages/space_management_v2/modules/space_details/presentation/helpers/space_details_dialog_helper.dart';
import 'package:syncrow_web/utils/color_manager.dart';
class CreateSpaceButton extends StatelessWidget {
const CreateSpaceButton({super.key});
@override
Widget build(BuildContext context) {
return GestureDetector(
onTap: () => SpaceDetailsDialogHelper.showCreate(context),
child: Container(
height: 60,
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(20),
boxShadow: [
BoxShadow(
color: Colors.grey.withValues(alpha: 0.5),
spreadRadius: 5,
blurRadius: 7,
offset: const Offset(0, 3),
),
],
),
child: Center(
child: Container(
width: 40,
height: 40,
decoration: const BoxDecoration(
color: ColorsManager.boxColor,
shape: BoxShape.circle,
),
child: const Icon(
Icons.add,
color: Colors.blue,
),
),
),
),
);
}
}

View File

@ -0,0 +1,33 @@
import 'package:flutter/material.dart';
import 'package:syncrow_web/utils/color_manager.dart';
class PlusButtonWidget extends StatelessWidget {
final Offset offset;
final void Function() onButtonTap;
const PlusButtonWidget({
super.key,
required this.offset,
required this.onButtonTap,
});
@override
Widget build(BuildContext context) {
return GestureDetector(
onTap: onButtonTap,
child: Container(
width: 30,
height: 30,
decoration: const BoxDecoration(
color: ColorsManager.spaceColor,
shape: BoxShape.circle,
),
child: const Icon(
Icons.add,
color: ColorsManager.whiteColors,
size: 20,
),
),
);
}
}

View File

@ -0,0 +1,44 @@
import 'package:flutter/material.dart';
import 'package:syncrow_web/pages/space_management_v2/main_module/widgets/plus_button_widget.dart';
class SpaceCardWidget extends StatefulWidget {
final void Function() onTap;
final Widget Function() buildSpaceContainer;
const SpaceCardWidget({
required this.onTap,
required this.buildSpaceContainer,
super.key,
});
@override
State<SpaceCardWidget> createState() => _SpaceCardWidgetState();
}
class _SpaceCardWidgetState extends State<SpaceCardWidget> {
bool isHovered = false;
@override
Widget build(BuildContext context) {
return MouseRegion(
onEnter: (_) => setState(() => isHovered = true),
onExit: (_) => setState(() => isHovered = false),
child: SizedBox(
child: Stack(
clipBehavior: Clip.none,
alignment: Alignment.center,
children: [
widget.buildSpaceContainer(),
if (isHovered)
Positioned(
bottom: 0,
child: PlusButtonWidget(
offset: Offset.zero,
onButtonTap: widget.onTap,
),
),
],
),
),
);
}
}

View File

@ -0,0 +1,85 @@
import 'package:flutter/material.dart';
import 'package:flutter_svg/svg.dart';
import 'package:syncrow_web/utils/color_manager.dart';
import 'package:syncrow_web/utils/extension/build_context_x.dart';
class SpaceCell extends StatelessWidget {
final String icon;
final String name;
final VoidCallback? onTap;
const SpaceCell({
super.key,
required this.icon,
required this.name,
required this.onTap,
});
@override
Widget build(BuildContext context) {
return GestureDetector(
onTap: onTap,
child: Container(
width: 150,
height: 70,
decoration: _containerDecoration(),
child: Row(
children: [
_buildIconContainer(),
const SizedBox(width: 10),
Expanded(
child: Text(
name,
style: context.textTheme.bodyLarge?.copyWith(
fontWeight: FontWeight.bold,
color: ColorsManager.blackColor,
),
overflow: TextOverflow.ellipsis,
),
),
],
),
),
);
}
Widget _buildIconContainer() {
return Container(
width: 40,
height: double.infinity,
decoration: const BoxDecoration(
color: ColorsManager.spaceColor,
borderRadius: BorderRadius.only(
topLeft: Radius.circular(15),
bottomLeft: Radius.circular(15),
),
),
child: Center(
child: SvgPicture.asset(
icon,
colorFilter: const ColorFilter.mode(
ColorsManager.whiteColors,
BlendMode.srcIn,
),
width: 24,
height: 24,
),
),
);
}
BoxDecoration _containerDecoration() {
return BoxDecoration(
color: ColorsManager.whiteColors,
borderRadius: BorderRadius.circular(15),
boxShadow: [
BoxShadow(
color: ColorsManager.lightGrayColor.withValues(alpha: 0.5),
spreadRadius: 2,
blurRadius: 5,
offset: const Offset(0, 3),
),
],
);
}
}

View File

@ -1,4 +1,8 @@
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:syncrow_web/pages/space_management_v2/main_module/widgets/space_management_community_structure.dart';
import 'package:syncrow_web/pages/space_management_v2/main_module/widgets/space_management_templates_view.dart';
import 'package:syncrow_web/pages/space_management_v2/modules/communities/presentation/communities_tree_selection_bloc/communities_tree_selection_bloc.dart';
import 'package:syncrow_web/pages/space_management_v2/modules/communities/presentation/widgets/space_management_communities_tree.dart';
class SpaceManagementBody extends StatelessWidget {
@ -6,9 +10,21 @@ class SpaceManagementBody extends StatelessWidget {
@override
Widget build(BuildContext context) {
return const Row(
return Row(
children: [
SpaceManagementCommunitiesTree(),
const SpaceManagementCommunitiesTree(),
Expanded(
child: BlocBuilder<CommunitiesTreeSelectionBloc,
CommunitiesTreeSelectionState>(
buildWhen: (previous, current) =>
previous.selectedCommunity != current.selectedCommunity,
builder: (context, state) => Visibility(
visible: state.selectedCommunity == null,
replacement: const SpaceManagementCommunityStructure(),
child: const SpaceManagementTemplatesView(),
),
),
),
],
);
}

View File

@ -0,0 +1,27 @@
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:syncrow_web/pages/space_management_v2/main_module/widgets/community_structure_canvas.dart';
import 'package:syncrow_web/pages/space_management_v2/main_module/widgets/create_space_button.dart';
import 'package:syncrow_web/pages/space_management_v2/modules/communities/presentation/communities_tree_selection_bloc/communities_tree_selection_bloc.dart';
class SpaceManagementCommunityStructure extends StatelessWidget {
const SpaceManagementCommunityStructure({super.key});
@override
Widget build(BuildContext context) {
final selectionBloc = context.watch<CommunitiesTreeSelectionBloc>().state;
final selectedCommunity = selectionBloc.selectedCommunity;
final selectedSpace = selectionBloc.selectedSpace;
const spacer = Spacer(flex: 10);
return Visibility(
visible: selectedCommunity!.spaces.isNotEmpty,
replacement: const Row(
children: [spacer, Expanded(child: CreateSpaceButton()), spacer],
),
child: CommunityStructureCanvas(
community: selectedCommunity,
selectedSpace: selectedSpace,
),
);
}
}

View File

@ -0,0 +1,50 @@
import 'package:flutter/material.dart';
import 'package:syncrow_web/pages/space_management_v2/main_module/shared/helpers/space_management_community_dialog_helper.dart';
import 'package:syncrow_web/pages/space_management_v2/main_module/widgets/community_template_cell.dart';
import 'package:syncrow_web/utils/color_manager.dart';
class SpaceManagementTemplatesView extends StatelessWidget {
const SpaceManagementTemplatesView({super.key});
@override
Widget build(BuildContext context) {
return ColoredBox(
color: ColorsManager.whiteColors,
child: GridView.builder(
padding: const EdgeInsets.symmetric(horizontal: 40, vertical: 20),
gridDelegate: const SliverGridDelegateWithMaxCrossAxisExtent(
maxCrossAxisExtent: 400,
mainAxisSpacing: 10,
crossAxisSpacing: 10,
childAspectRatio: 2.0,
),
itemCount: _gridItems(context).length,
itemBuilder: (context, index) {
final model = _gridItems(context)[index];
return CommunityTemplateCell(
onTap: model.onTap,
title: model.title,
);
},
),
);
}
List<_CommunityTemplateModel> _gridItems(BuildContext context) {
return [
_CommunityTemplateModel(
title: const Text('Blank'),
onTap: () => SpaceManagementCommunityDialogHelper.showCreateDialog(context),
),
];
}
}
class _CommunityTemplateModel {
final Widget title;
final void Function() onTap;
_CommunityTemplateModel({
required this.title,
required this.onTap,
});
}

View File

@ -32,7 +32,7 @@ class CommunitiesTreeSelectionBloc
) {
emit(
CommunitiesTreeSelectionState(
selectedCommunity: null,
selectedCommunity: event.community,
selectedSpace: event.space,
),
);

View File

@ -8,7 +8,7 @@ sealed class CommunitiesTreeSelectionEvent extends Equatable {
}
final class SelectCommunityEvent extends CommunitiesTreeSelectionEvent {
final CommunityModel? community;
final CommunityModel community;
const SelectCommunityEvent({required this.community});
@override
@ -17,8 +17,9 @@ final class SelectCommunityEvent extends CommunitiesTreeSelectionEvent {
final class SelectSpaceEvent extends CommunitiesTreeSelectionEvent {
final SpaceModel? space;
final CommunityModel community;
const SelectSpaceEvent({required this.space});
const SelectSpaceEvent({required this.space, required this.community});
@override
List<Object?> get props => [space];

View File

@ -30,7 +30,7 @@ class SpaceManagementCommunitiesTreeSpaceTile extends StatelessWidget {
initiallyExpanded: spaceIsExpanded,
onExpansionChanged: (expanded) {},
onItemSelected: () => context.read<CommunitiesTreeSelectionBloc>().add(
SelectSpaceEvent(space: space),
SelectSpaceEvent(community: community, space: space),
),
children: space.children
.map(

View File

@ -1,9 +1,8 @@
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:syncrow_web/pages/space_management_v2/modules/communities/presentation/bloc/communities_bloc.dart';
import 'package:syncrow_web/pages/space_management_v2/main_module/shared/helpers/space_management_community_dialog_helper.dart';
import 'package:syncrow_web/pages/space_management_v2/modules/communities/presentation/communities_tree_selection_bloc/communities_tree_selection_bloc.dart';
import 'package:syncrow_web/pages/space_management_v2/modules/communities/presentation/widgets/space_management_sidebar_add_community_button.dart';
import 'package:syncrow_web/pages/space_management_v2/modules/create_community/presentation/create_community_dialog.dart';
import 'package:syncrow_web/utils/color_manager.dart';
import 'package:syncrow_web/utils/extension/build_context_x.dart';
import 'package:syncrow_web/utils/style.dart';
@ -41,7 +40,7 @@ class SpaceManagementSidebarHeader extends StatelessWidget {
if (isSelected) {
_clearSelection(context);
} else {
_showCreateCommunityDialog(context);
SpaceManagementCommunityDialogHelper.showCreateDialog(context);
}
}
@ -50,19 +49,4 @@ class SpaceManagementSidebarHeader extends StatelessWidget {
const ClearCommunitiesTreeSelectionEvent(),
);
}
void _showCreateCommunityDialog(BuildContext context) => showDialog<void>(
context: context,
builder: (_) => CreateCommunityDialog(
title: const Text('Community Name'),
onCreateCommunity: (community) {
context.read<CommunitiesBloc>().add(
InsertCommunity(community),
);
context.read<CommunitiesTreeSelectionBloc>().add(
SelectCommunityEvent(community: community),
);
},
),
);
}

View File

@ -53,7 +53,7 @@ class RemoteCreateCommunityService implements CreateCommunityService {
return _defaultErrorMessage;
}
final error = body['error'] as Map<String, dynamic>?;
final errorMessage = error?['error'] as String? ?? '';
final errorMessage = error?['message'] as String? ?? '';
return errorMessage;
}

View File

@ -41,11 +41,8 @@ class CreateCommunityDialog extends StatelessWidget {
);
onCreateCommunity.call(community);
break;
case CreateCommunityFailure(:final message):
case CreateCommunityFailure():
Navigator.of(context).pop();
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text(message)),
);
break;
default:
break;

View File

@ -0,0 +1,11 @@
import 'package:flutter/material.dart';
import 'package:syncrow_web/pages/space_management_v2/modules/space_details/presentation/widgets/space_details_dialog.dart';
abstract final class SpaceDetailsDialogHelper {
static void showCreate(BuildContext context) {
showDialog<void>(
context: context,
builder: (context) => const SpaceDetailsDialog(),
);
}
}

View File

@ -0,0 +1,12 @@
import 'package:flutter/material.dart';
class SpaceDetailsDialog extends StatelessWidget {
const SpaceDetailsDialog({super.key});
@override
Widget build(BuildContext context) {
return const Dialog(
child: Text('Create Space'),
);
}
}

View File

@ -289,7 +289,6 @@ class SpaceTreeBloc extends Bloc<SpaceTreeEvent, SpaceTreeState> {
selectedSpaces: updatedSelectedSpaces,
soldCheck: updatedSoldChecks,
selectedCommunityAndSpaces: communityAndSpaces));
emit(state.copyWith(selectedSpaces: updatedSelectedSpaces));
} catch (e) {
emit(const SpaceTreeErrorState('Something went wrong'));
}
@ -445,10 +444,12 @@ class SpaceTreeBloc extends Bloc<SpaceTreeEvent, SpaceTreeState> {
List<String> _getThePathToChild(String communityId, String selectedSpaceId) {
List<String> ids = [];
for (var community in state.communityList) {
final communityDataSource =
state.searchQuery.isNotEmpty ? state.filteredCommunity : state.communityList;
for (final community in communityDataSource) {
if (community.uuid == communityId) {
for (var space in community.spaces) {
List<String> list = [];
for (final space in community.spaces) {
final list = <String>[];
list.add(space.uuid!);
ids = _getAllParentsIds(space, selectedSpaceId, List.from(list));
if (ids.isNotEmpty) {

View File

@ -68,7 +68,7 @@ class VisitorPasswordBloc
DateTime? startTime = DateTime.now();
DateTime? endTime;
String startTimeAccess = 'Start Time';
String startTimeAccess = DateTime.now().toString().split('.').first;
String endTimeAccess = 'End Time';
PasswordStatus? passwordStatus;
selectAccessType(
@ -136,6 +136,27 @@ class VisitorPasswordBloc
);
return;
}
if(selectedTimestamp < DateTime.now().millisecondsSinceEpoch ~/ 1000) {
if(selectedTimestamp < DateTime.now().millisecondsSinceEpoch ~/ 1000) {
await showDialog<void>(
context: event.context,
builder: (context) => AlertDialog(
title: const Text('Effective Time cannot be earlier than current time.'),
actionsAlignment: MainAxisAlignment.center,
content:
FilledButton(
onPressed: () {
Navigator.of(event.context).pop();
add(SelectTimeVisitorPassword(context: event.context, isStart: true, isRepeat: false));
},
child: const Text('OK'),
),
),
);
}
return;
}
effectiveTimeTimeStamp = selectedTimestamp;
startTimeAccess = selectedDateTime.toString().split('.').first;
} else {

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