Compare commits

...

82 Commits

Author SHA1 Message Date
bb846f797f Implement Tag Assignment and Device Addition Features:
- Introduced AssignTagsDialog for assigning tags to devices, enhancing user interaction and organization.
- Added AddDeviceTypeWidget for adding new device types, improving the flexibility of device management.
- Created ProductTypeCard and ProductTypeCardCounter for better representation and interaction with device types.
- Enhanced AssignTagsTable for displaying and managing product allocations, improving maintainability and user experience.
2025-07-06 16:54:15 +03:00
e234c9f3b2 Enhance SpaceDetailsActionButtons: Introduced customizable button labels for save and cancel actions, improving flexibility and user experience. Updated button implementations to utilize these new labels, enhancing maintainability and adherence to Clean Architecture principles. 2025-07-06 16:44:40 +03:00
bcd0ae4a2a Refactor Products Module:
- Introduced ProductsBloc and updated ProductsService to remove LoadProductsParam, simplifying the product loading process.
- Updated RemoteProductsService to utilize a new API endpoint for fetching products.
- Adjusted ProductsEvent and ProductsState to reflect changes in the loading mechanism, enhancing maintainability and clarity in the products management flow.
2025-07-06 16:44:26 +03:00
cebce2ce7f Update SpaceDetailsModel: Change default icon from villa to location for improved representation of space details. 2025-07-06 14:50:24 +03:00
97e3fb68bf Enhance Product Model and SpaceDetailsDevicesBox:
- Added 'productType' field to Product model for improved data representation.
- Updated JSON parsing in Product model to handle 'prodType'.
- Refactored SpaceDetailsDevicesBox to utilize productType for dynamic device icon rendering, enhancing UI clarity and maintainability.
2025-07-06 14:49:10 +03:00
a4024067c7 Sp 1708 fe implement create edit space (#339)
<!--
  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-1708](https://syncrow.atlassian.net/browse/SP-1708)

## Description

- Added Space Details module with complete BLOC architecture
- Implemented Community Structure Header with action buttons
- Enhanced Space Management page with new UI components
- Fixed typo in Home page ("Devices Management" → "Device Management")

## 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-1708]:
https://syncrow.atlassian.net/browse/SP-1708?atlOrigin=eyJpIjoiNWRkNTljNzYxNjVmNDY3MDlhMDU5Y2ZhYzA5YTRkZjUiLCJwIjoiZ2l0aHViLWNvbS1KU1cifQ
2025-07-06 09:24:45 +03:00
95cded4bf5 Enhance SubSpacesInput: Introduce FocusNode for improved text field focus management. This change allows the input field to regain focus after adding a subspace, enhancing user experience and maintaining clean state management practices. 2025-07-06 09:04:13 +03:00
757a96ed9f Refactor SpaceDetailsActionButtons and SpaceIconPicker: Adjust layout properties for improved responsiveness and user experience. Set mainAxisSize to min in SpaceDetailsActionButtons and simplify the layout in SpaceIconPicker for better alignment and interaction. This enhances maintainability and adheres to Clean Architecture principles. 2025-07-03 16:15:31 +03:00
b857736e10 Refactor SpaceNameTextField: Update text styling and border handling to utilize context-based theming. This improves consistency with the app's theme and enhances maintainability by centralizing border styling logic. 2025-07-03 15:37:06 +03:00
1fccd51440 Refactor SubspaceNameDisplayWidget: Update Chip border radius for improved aesthetics and add delete functionality to remove subspaces. This enhances user interaction and aligns with maintainability principles. 2025-07-03 15:33:34 +03:00
c07ddb0ccd Refactor SpaceDetailsDevicesBox: Improve readability by extracting variables for product allocations and subspaces. This change enhances code clarity and maintainability in line with Clean Architecture principles. 2025-07-03 15:27:06 +03:00
58e99f95b2 removed comments. 2025-07-03 15:25:04 +03:00
227df6fe3d Refactor SpaceDetailsWidgets: Simplify layout and improve responsiveness in SpaceDetailsDevicesBox and SpaceSubSpacesBox. Update SpaceDetailsForm to enhance dialog width for better user experience. This refactor enhances maintainability and aligns with Clean Architecture principles. 2025-07-03 15:23:00 +03:00
9451ec0cc4 Update SpaceDetailsDialog to utilize SelectableText for error messages and ensure proper Bloc context usage. This enhances user experience by allowing text selection for easier copying of error information. 2025-07-03 13:19:42 +03:00
fc797c2646 Refactor SpaceDetailsModel and ProductAllocation: Update JSON parsing for clarity and remove unused location field. Change subspace name mapping for consistency with API response. 2025-07-03 13:19:34 +03:00
318e1d9af7 Implement Space Management Header and Action Buttons; integrate SpaceDetailsBloc for improved space management functionality. Add CommunityStructureHeader, CommunityStructureHeaderActionButtons, and CommunityStructureHeaderButton widgets to enhance UI and user interactions. Update SpaceManagementCommunityStructure to include the new header and refactor space details service for better endpoint handling. 2025-07-03 13:09:43 +03:00
d47dc349bc Enhance SubspaceNameDisplayWidget to handle duplicate names during editing. Introduced logic to check for existing subspace names and provide user feedback. Refactored state management for editing and submission processes, improving overall user experience and code clarity. 2025-07-03 12:04:03 +03:00
c221c8499f Add factory method empty to SpaceModel for default instance creation. Refactor SpaceDetailsDialog and related widgets to utilize SpaceModel, enhancing parameter handling and state management in space creation and editing flows. 2025-07-02 17:05:56 +03:00
71cf4b9feb Update LoadSpaceDetailsParam to require spaceUuid and refactor SpaceDetailsDialog to enhance clarity in parameter handling. 2025-07-02 16:30:23 +03:00
c43cf9347f Remove unnecessary deactivate method from SpaceDetailsDialog to streamline state management and improve code clarity. 2025-07-02 16:29:02 +03:00
9990b1805e Fix typo in HomeBloc: change 'Devices Management' to 'Device Management' for consistency in naming. 2025-07-02 16:27:03 +03:00
50f8158830 Add booking page and related routes, icons, and button widget (#338)
<!--
  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 booking page and related routes, icons, and button widget

## 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-07-02 15:58:28 +03:00
009b7c0316 Refactor SpaceDetails feature to replace LoadSpacesParam with LoadSpaceDetailsParam, enhancing clarity in parameter handling. Introduce ClearSpaceDetails event in SpaceDetailsBloc for better state management. Update SpaceDetailsDialog and SpaceDetailsForm to utilize new parameter and improve dialog functionality. 2025-07-02 15:53:54 +03:00
72af55ef98 Add booking page and related routes, icons, and button widget 2025-07-02 15:50:46 +03:00
779c0fe916 Refactor SpaceDetailsDialog to improve code readability and structure by simplifying widget hierarchy and enhancing the use of Bloc for state management. 2025-07-02 15:42:19 +03:00
e448eabda6 Refactor SpaceSubSpacesDialog to use SubSpacesInput for managing subspaces, enhancing state management and UI structure. Update SpaceDetailsActionButtons to handle optional save callback. 2025-07-02 15:41:13 +03:00
9dfb3ed369 Refactor SpaceDetailsDialog and SpaceIconPicker to integrate Bloc for state management, enhancing icon selection and dialog functionality. 2025-07-02 15:20:52 +03:00
63353af38b Add SpaceDetails dialog and related widgets for creating and editing spaces, including SpaceDetailsDevicesBox and SpaceSubSpacesBox for managing devices and subspaces. 2025-07-02 15:03:23 +03:00
68b6c9b18c Refactor SpaceDetailsBloc to move SpaceDetailsService declaration for improved clarity 2025-07-02 15:03:07 +03:00
fa6ee9a0af Add factory method empty to SpaceDetailsModel for creating default instances 2025-07-02 15:03:00 +03:00
3601b02bc3 Add SpaceDetailsModelBloc and events for managing space details state 2025-07-02 15:02:55 +03:00
fdd0526c78 added copyWith to SpaceDetailsModel and its property models. 2025-07-02 14:17:27 +03:00
b888f516e2 Fix device status display in Control modal to reflect actual status (#337)
<!--
  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 -->
table enhancement

## 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-07-02 11:28:50 +03:00
bdeec7d325 Add SpaceIconPicker widget for selecting and displaying space icons with a dialog option. 2025-07-02 11:27:36 +03:00
50ff17a0c1 Add SpaceIconSelectionDialog widget for selecting space icons in a dialog. 2025-07-02 11:27:26 +03:00
87c2e3261d Add SpaceDetailsActionButtons widget for improved action handling in space details. 2025-07-02 11:27:13 +03:00
62a6f9c993 Add ButtonContentWidget for customizable button UI in space details. 2025-07-02 11:27:03 +03:00
c1e61ee61d [FE] Community and Space Dialog Redesign in the routine tab (#336)
<!--
  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-1601](https://syncrow.atlassian.net/browse/SP-1601)

## Description

i made the fixes requested by Yazan

## 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-1601]:
https://syncrow.atlassian.net/browse/SP-1601?atlOrigin=eyJpIjoiNWRkNTljNzYxNjVmNDY3MDlhMDU5Y2ZhYzA5YTRkZjUiLCJwIjoiZ2l0aHViLWNvbS1KU1cifQ
2025-07-02 11:21:34 +03:00
7750290be4 Fix device status display in Control modal to reflect actual status 2025-07-02 10:14:54 +03:00
f7e4d6ff07 added default dialog background color to be white. 2025-07-02 09:33:45 +03:00
7f26c773a7 [FE] Preferences & Calibration (#332)
<!--
  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

change the color of completed dialog

## 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-07-02 09:05:44 +03:00
1adbae6735 Clarification on Default Value for Start Date in Door Lock Online Tile Limited Password repeat section (#331)
…t with dialog information showing the error and if the init start date
is null fill it with the needed 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-368](https://syncrow.atlassian.net/browse/SP-368)

## Description

now if user change end time into value before start time it prevent it
and give init start date value to start date

## 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-368]:
https://syncrow.atlassian.net/browse/SP-368?atlOrigin=eyJpIjoiNWRkNTljNzYxNjVmNDY3MDlhMDU5Y2ZhYzA5YTRkZjUiLCJwIjoiZ2l0aHViLWNvbS1KU1cifQ
2025-07-02 08:58:47 +03:00
ede2da6632 hot fix thermostat string (#334)
<!--
  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
no ticket

## Description

hot fix thermostat string

## 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-07-02 08:32:48 +03:00
b06e4bd2ba hot fix thermostat string 2025-07-02 08:27:09 +03:00
0847cb8a41 fix UI 2025-07-02 08:19:56 +03:00
818bdee745 change calibration completed dialog color 2025-07-01 15:04:50 +03:00
0a022d8a8d [FE] On devices management page when we search for a device then select a space that has devices and try to search again it does not work (#327)
<!--
  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-1805](https://syncrow.atlassian.net/browse/SP-1805)

## Description

should reset filters when selecting any community 

## 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-1805]:
https://syncrow.atlassian.net/browse/SP-1805?atlOrigin=eyJpIjoiNWRkNTljNzYxNjVmNDY3MDlhMDU5Y2ZhYzA5YTRkZjUiLCJwIjoiZ2l0aHViLWNvbS1KU1cifQ
2025-07-01 11:34:12 +03:00
f33b3e8bd2 now if user change end time into value before start time it prevent it with dialog information showing the error and if the init start date is null fill it with the needed value 2025-07-01 11:19:35 +03:00
8f0eb88567 remove countdownRemaining from ScheduleLoaded state (#330)
<!--
  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
remove countdownRemaining from ScheduleLoaded state
<!--- 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-07-01 11:18:17 +03:00
19739c6e4d Add EnergyConsumptionPage to SmartPowerDeviceControl for enhanced ene… (#329)
…rgy data visualization

<!--
  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 -->
The phases and the chart should be synced.

## 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-07-01 11:18:00 +03:00
9f86b8d638 remove countdownRemaining from ScheduleLoaded state 2025-07-01 11:14:02 +03:00
95907661d2 align bar charts to start. (#328)
<!--
  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

Align AQI Distribution, and Occupancy charts bars to start.

## 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-07-01 10:44:20 +03:00
9c9b7d99dc enhance sizing of energy management view. 2025-07-01 09:55:38 +03:00
037895844a Add EnergyConsumptionPage to SmartPowerDeviceControl for enhanced energy data visualization 2025-07-01 09:44:59 +03:00
c07bae5cbc align bar charts to start. 2025-07-01 09:32:00 +03:00
e6fe9f35b0 problem fixed should reset filters when select space or community 2025-07-01 08:41:38 +03:00
8cb6c13cd5 Changed timer codes in curtain module to match what the API expects. 2025-06-30 15:53:23 +03:00
949c27938a Analytics empty state (#325)
<!--
  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-30 15:32:12 +03:00
4c582b865d Sp 1611 in user management if email address already exists the error message does not go away until the user clicks next the error message should clear if a good email is entered (#321)
<!--
  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-1611
](https://syncrow.atlassian.net/jira/software/projects/SP/boards/5?assignee=712020%3A71e88a7f-7752-44b3-8177-4ab51a950811&selectedIssue=SP-1611)

## Description

use textfield validator on chaging value not only with next button 

## 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-30 15:31:04 +03:00
d7467adeda Add countdown functionality and device type support across device man… (#323)
…agement views

<!--
  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-30 15:22:28 +03:00
5486f0832d Remove unused copyWith method from Status class and simplify status assignment in ScheduleBloc 2025-06-30 15:22:02 +03:00
fd239a3907 Merge branch 'dev' of https://github.com/SyncrowIOT/web into fix-schedule 2025-06-30 15:17:37 +03:00
e2d6f5eea8 Update device type from '1GT' to '2GT' in TwoGangGlassSwitchControlView 2025-06-30 15:11:17 +03:00
289922071a Add countdown functionality and device type support across device management views 2025-06-30 15:05:59 +03:00
8594168548 hardcoded device location to dubai for demo purposes. 2025-06-30 14:22:54 +03:00
bd9a74b380 fix touch gangs realtime. 2025-06-30 13:58:10 +03:00
15ee79688d reComite 2025-06-30 13:36:52 +03:00
e5e88385e9 change autoValidae mode to userInteraction and give some time to check validate when typing on keyboard with debouncer 2025-06-30 13:31:38 +03:00
62d5bbce7e add isValid to basic step (1) and insure that user can go to another step using next button without filling the form 2025-06-30 13:22:04 +03:00
05d784ec11 Merge branch 'dev' of https://github.com/SyncrowIOT/web into analytics-empty-state 2025-06-30 12:53:42 +03:00
9ebf474a60 analytics-empty-state. 2025-06-30 12:52:22 +03:00
af48bbead5 fix-occupancy-devices-bug (#318)
<!--
  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

handle more cases when decoding analytics devices.

## 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-30 11:28:38 +03:00
3c80724c1e Sp 1707 fe preferences calibration fix UI (#317)
<!--
  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 the UI as wanted 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-30 11:21:26 +03:00
db05331e9a update AnalyticsChartEmptyStateWidget to use new icons. 2025-06-30 11:18:08 +03:00
cdc76c2c8e handle more cases when decoding analytics devices. 2025-06-30 11:04:22 +03:00
44c88fb1c4 added empty charts icons. 2025-06-30 10:54:33 +03:00
dfb120e7cf [FE] Smart curtain module device Icon and other devices icons are appearing as a sensor Icon on Add device dialog on space management (#316)
<!--
  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-1804](https://syncrow.atlassian.net/browse/SP-1804)

## Description

add the new product types and map to the icons

## 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-1804]:
https://syncrow.atlassian.net/browse/SP-1804?atlOrigin=eyJpIjoiNWRkNTljNzYxNjVmNDY3MDlhMDU5Y2ZhYzA5YTRkZjUiLCJwIjoiZ2l0aHViLWNvbS1KU1cifQ
2025-06-30 10:47:37 +03:00
8c3861e83c fix the button border Raduis when hovering 2025-06-30 10:44:16 +03:00
b90f25f7b0 fix all UI notes make white dialogs with fix calibrating textfield for seconds && fix icon of completed action dialog 2025-06-30 10:39:23 +03:00
4d51321675 add the new devices to mapIconToProduct func 2025-06-30 10:05:15 +03:00
b5e7776ccb Sp 1705 fe create scheduling UI fixes (#315)
<!--
  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

i fixed edit and insure integrating with backend for schedule and remove
countdown from UI if category cur module

## 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-30 09:34:11 +03:00
2f5ad03431 created empty charts widget. 2025-06-29 10:57:18 +03:00
112 changed files with 4129 additions and 1149 deletions

View File

@ -0,0 +1,8 @@
<svg width="175" height="134" viewBox="0 0 175 134" fill="none" xmlns="http://www.w3.org/2000/svg">
<line x1="0.5" y1="2.18557e-08" x2="0.499994" y2="132.759" stroke="#B9C0C5"/>
<line x1="175" y1="133.259" x2="-4.37114e-08" y2="133.259" stroke="#B9C0C5"/>
<rect x="16.0922" y="66.3794" width="28.1609" height="66.3793" fill="#C7CDD1"/>
<rect x="54.3105" y="24.1379" width="28.1609" height="108.621" fill="#ABB4BA"/>
<rect x="92.5288" y="78.4484" width="28.1609" height="54.3103" fill="#C7CDD1"/>
<rect x="130.747" y="48.2759" width="28.1609" height="84.4828" fill="#ABB4BA"/>
</svg>

After

Width:  |  Height:  |  Size: 583 B

View File

@ -0,0 +1,5 @@
<svg width="175" height="134" viewBox="0 0 175 134" fill="none" xmlns="http://www.w3.org/2000/svg">
<line x1="0.5" y1="3.05394e-05" x2="0.499994" y2="132.759" stroke="#B9C0C5"/>
<line x1="175" y1="133.259" x2="-4.37114e-08" y2="133.259" stroke="#B9C0C5"/>
<path d="M1.5 132.5C13 132.5 6.58852 66.5 29.5 66.5C46.5 66.5 46.1214 24.9349 68.5 24.5C94.2816 23.999 80.7136 78.5065 106.5 78.5C125.715 78.4952 131.5 48.5 145.5 48.5C159.5 48.5 156.5 96 171.5 96" stroke="#ABB4BA" stroke-width="2" stroke-linecap="round"/>
</svg>

After

Width:  |  Height:  |  Size: 520 B

View File

@ -0,0 +1,7 @@
<svg width="175" height="134" viewBox="0 0 175 134" fill="none" xmlns="http://www.w3.org/2000/svg">
<line x1="0.5" y1="2.18557e-08" x2="0.499994" y2="132.759" stroke="#B9C0C5"/>
<line x1="175" y1="133.259" x2="-4.37114e-08" y2="133.259" stroke="#B9C0C5"/>
<path d="M1.5 132.5C13 132.5 6.58852 66.5 29.5 66.5C46.5 66.5 46.1214 24.9348 68.5 24.5C94.2816 23.999 80.7136 78.5064 106.5 78.5C125.715 78.4951 131.5 48.5 145.5 48.5C159.5 48.5 156.5 95.9999 171.5 95.9999" stroke="#ABB4BA" stroke-width="1.5" stroke-linecap="round" stroke-dasharray="4 4"/>
<path d="M1.5 132.5C13 132.5 6.58852 78.4999 29.5 78.4999C46.5 78.4999 45.6214 44.9349 68 44.5C93.7816 43.999 80.7136 27.0065 106.5 27C125.715 26.9952 131.5 63.5 145.5 63.5C159.5 63.5 156.5 113.5 171.5 113.5" stroke="#C7CDD1" stroke-width="1.5" stroke-linecap="round" stroke-dasharray="4 4"/>
<path d="M1.5 132.5C13 132.5 6.58852 85.9999 29.5 85.9999C46.5 85.9999 45.6214 11.9348 68 11.4999C93.7816 10.9989 80.7136 43.5064 106.5 43.4999C125.715 43.4951 131.5 35.4999 145.5 35.4999C159.5 35.4999 156.5 105.5 171.5 105.5" stroke="#D5D5D5" stroke-width="1.5" stroke-linecap="round" stroke-dasharray="4 4"/>
</svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

View File

@ -0,0 +1,99 @@
<svg width="181" height="121" viewBox="0 0 181 121" fill="none" xmlns="http://www.w3.org/2000/svg">
<line x1="15.5" y1="-2.52181e-08" x2="15.5" y2="120" stroke="#B9B9B9"/>
<line x1="45.5" y1="-2.52181e-08" x2="45.5" y2="120" stroke="#B9B9B9"/>
<line x1="75.5" y1="-2.52181e-08" x2="75.5" y2="120" stroke="#B9B9B9"/>
<line x1="105.5" y1="-2.52181e-08" x2="105.5" y2="120" stroke="#B9B9B9"/>
<line x1="135.5" y1="-2.52181e-08" x2="135.5" y2="120" stroke="#B9B9B9"/>
<line x1="165.5" y1="-2.52181e-08" x2="165.5" y2="120" stroke="#B9B9B9"/>
<line x1="30.5" y1="-2.52181e-08" x2="30.5" y2="120" stroke="#B9B9B9"/>
<line x1="60.5" y1="-2.52181e-08" x2="60.5" y2="120" stroke="#B9B9B9"/>
<line x1="90.5" y1="-2.52181e-08" x2="90.5" y2="120" stroke="#B9B9B9"/>
<line x1="120.5" y1="-2.52181e-08" x2="120.5" y2="120" stroke="#B9B9B9"/>
<line x1="150.5" y1="-2.52181e-08" x2="150.5" y2="120" stroke="#B9B9B9"/>
<line x1="180.5" y1="-2.52181e-08" x2="180.5" y2="120" stroke="#B9B9B9"/>
<line x1="181" y1="120.5" x2="-4.52101e-08" y2="120.5" stroke="#B9B9B9"/>
<line x1="181" y1="60.5" x2="-4.52101e-08" y2="60.5" stroke="#B9B9B9"/>
<line x1="181" y1="90.5" x2="-4.52101e-08" y2="90.5" stroke="#B9B9B9"/>
<line x1="181" y1="30.5" x2="-4.52101e-08" y2="30.5" stroke="#B9B9B9"/>
<line x1="181" y1="105.5" x2="-4.52101e-08" y2="105.5" stroke="#B9B9B9"/>
<line x1="181" y1="45.5" x2="-4.52101e-08" y2="45.5" stroke="#B9B9B9"/>
<line x1="181" y1="75.5" x2="-4.52101e-08" y2="75.5" stroke="#B9B9B9"/>
<line x1="181" y1="15.5" x2="-4.52101e-08" y2="15.5" stroke="#B9B9B9"/>
<rect x="16" y="16" width="14" height="14" fill="#F2F2F2"/>
<rect x="31" y="16" width="14" height="14" fill="#F2F2F2"/>
<rect x="46" y="16" width="14" height="14" fill="#F2F2F2"/>
<rect x="61" y="16" width="14" height="14" fill="#F2F2F2"/>
<rect x="76" y="16" width="14" height="14" fill="#F2F2F2"/>
<rect x="91" y="16" width="14" height="14" fill="#F2F2F2"/>
<rect x="106" y="16" width="14" height="14" fill="#F2F2F2"/>
<rect x="121" y="16" width="14" height="14" fill="#F2F2F2"/>
<rect x="136" y="16" width="14" height="14" fill="#F2F2F2"/>
<rect x="151" y="16" width="14" height="14" fill="#F2F2F2"/>
<rect x="166" y="16" width="14" height="14" fill="#F2F2F2"/>
<rect x="16" y="31" width="14" height="14" fill="#F2F2F2"/>
<rect x="31" y="31" width="14" height="14" fill="#D5D5D5"/>
<rect x="46" y="31" width="14" height="14" fill="#D5D5D5"/>
<rect x="61" y="31" width="14" height="14" fill="#D5D5D5"/>
<rect x="76" y="31" width="14" height="14" fill="#D5D5D5"/>
<rect x="91" y="31" width="14" height="14" fill="#D5D5D5"/>
<rect x="106" y="31" width="14" height="14" fill="#D5D5D5"/>
<rect x="121" y="31" width="14" height="14" fill="#D5D5D5"/>
<rect x="136" y="31" width="14" height="14" fill="#D5D5D5"/>
<rect x="151" y="31" width="14" height="14" fill="#D5D5D5"/>
<rect x="166" y="31" width="14" height="14" fill="#F2F2F2"/>
<rect x="16" y="46" width="14" height="14" fill="#F2F2F2"/>
<rect x="31" y="46" width="14" height="14" fill="#D5D5D5"/>
<rect x="46" y="46" width="14" height="14" fill="#D5D5D5"/>
<rect x="61" y="46" width="14" height="14" fill="#D5D5D5"/>
<rect x="76" y="46" width="14" height="14" fill="#D5D5D5"/>
<rect x="91" y="46" width="14" height="14" fill="#D5D5D5"/>
<rect x="106" y="46" width="14" height="14" fill="#D5D5D5"/>
<rect x="121" y="46" width="14" height="14" fill="#D5D5D5"/>
<rect x="136" y="46" width="14" height="14" fill="#D5D5D5"/>
<rect x="151" y="46" width="14" height="14" fill="#D5D5D5"/>
<rect x="166" y="46" width="14" height="14" fill="#F2F2F2"/>
<rect x="16" y="61" width="14" height="14" fill="#F2F2F2"/>
<rect x="31" y="61" width="14" height="14" fill="#D5D5D5"/>
<rect x="46" y="61" width="14" height="14" fill="#D5D5D5"/>
<rect x="61" y="61" width="14" height="14" fill="#B1B1B1"/>
<rect x="76" y="61" width="14" height="14" fill="#B1B1B1"/>
<rect x="91" y="61" width="14" height="14" fill="#B1B1B1"/>
<rect x="106" y="61" width="14" height="14" fill="#B1B1B1"/>
<rect x="121" y="61" width="14" height="14" fill="#B1B1B1"/>
<rect x="136" y="61" width="14" height="14" fill="#D5D5D5"/>
<rect x="151" y="61" width="14" height="14" fill="#D5D5D5"/>
<rect x="166" y="61" width="14" height="14" fill="#F2F2F2"/>
<rect x="16" y="76" width="14" height="14" fill="#F2F2F2"/>
<rect x="31" y="76" width="14" height="14" fill="#D5D5D5"/>
<rect x="46" y="76" width="14" height="14" fill="#D5D5D5"/>
<rect x="61" y="76" width="14" height="14" fill="#D5D5D5"/>
<rect x="76" y="76" width="14" height="14" fill="#D5D5D5"/>
<rect x="91" y="76" width="14" height="14" fill="#D5D5D5"/>
<rect x="106" y="76" width="14" height="14" fill="#D5D5D5"/>
<rect x="121" y="76" width="14" height="14" fill="#D5D5D5"/>
<rect x="136" y="76" width="14" height="14" fill="#D5D5D5"/>
<rect x="151" y="76" width="14" height="14" fill="#D5D5D5"/>
<rect x="166" y="76" width="14" height="14" fill="#F2F2F2"/>
<rect x="16" y="91" width="14" height="14" fill="#F2F2F2"/>
<rect x="31" y="91" width="14" height="14" fill="#D5D5D5"/>
<rect x="46" y="91" width="14" height="14" fill="#D5D5D5"/>
<rect x="61" y="91" width="14" height="14" fill="#D5D5D5"/>
<rect x="76" y="91" width="14" height="14" fill="#D5D5D5"/>
<rect x="91" y="91" width="14" height="14" fill="#D5D5D5"/>
<rect x="106" y="91" width="14" height="14" fill="#D5D5D5"/>
<rect x="121" y="91" width="14" height="14" fill="#D5D5D5"/>
<rect x="136" y="91" width="14" height="14" fill="#D5D5D5"/>
<rect x="151" y="91" width="14" height="14" fill="#D5D5D5"/>
<rect x="166" y="91" width="14" height="14" fill="#F2F2F2"/>
<rect x="16" y="106" width="14" height="14" fill="#F2F2F2"/>
<rect x="31" y="106" width="14" height="14" fill="#F2F2F2"/>
<rect x="46" y="106" width="14" height="14" fill="#F2F2F2"/>
<rect x="61" y="106" width="14" height="14" fill="#F2F2F2"/>
<rect x="76" y="106" width="14" height="14" fill="#F2F2F2"/>
<rect x="91" y="106" width="14" height="14" fill="#F2F2F2"/>
<rect x="106" y="106" width="14" height="14" fill="#F2F2F2"/>
<rect x="121" y="106" width="14" height="14" fill="#F2F2F2"/>
<rect x="136" y="106" width="14" height="14" fill="#F2F2F2"/>
<rect x="151" y="106" width="14" height="14" fill="#F2F2F2"/>
<rect x="166" y="106" width="14" height="14" fill="#F2F2F2"/>
</svg>

After

Width:  |  Height:  |  Size: 6.1 KiB

View File

@ -0,0 +1,7 @@
<svg width="175" height="134" viewBox="0 0 175 134" fill="none" xmlns="http://www.w3.org/2000/svg">
<line x1="0.5" y1="2.18557e-08" x2="0.499994" y2="132.759" stroke="#B9C0C5"/>
<line x1="175" y1="133.259" x2="-4.37114e-08" y2="133.259" stroke="#B9C0C5"/>
<path d="M1.5 95.9999C13 95.9999 6.58853 66.4999 29.5 66.4999C46.5 66.4999 46.1214 34.9348 68.5 34.4999C94.2816 33.9989 80.7136 65.0065 106.5 65C125.715 64.9952 131.5 50.5 145.5 50.5C159.5 50.5 156.5 70.5 171.5 70.5" stroke="#C7CDD1" stroke-width="2" stroke-linecap="round"/>
<path d="M1.5 106C13 106 6.58853 76.4999 29.5 76.4999C46.5 76.4999 46.1214 44.9348 68.5 44.4999C94.2816 43.9989 80.7136 75.0065 106.5 75C125.715 74.9952 131.5 60.5 145.5 60.5C159.5 60.5 156.5 80.5 171.5 80.5" stroke="#F2F2F2" stroke-width="2" stroke-linecap="round"/>
<path d="M1.5 116C13 116 6.58853 86.4999 29.5 86.4999C46.5 86.4999 46.1214 54.9348 68.5 54.4999C94.2816 53.9989 80.7136 85.0065 106.5 85C125.715 84.9952 131.5 70.5 145.5 70.5C159.5 70.5 156.5 90.5 171.5 90.5" stroke="#ABB4BA" stroke-width="2" stroke-linecap="round"/>
</svg>

After

Width:  |  Height:  |  Size: 1.0 KiB

View File

@ -0,0 +1,15 @@
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<g clip-path="url(#clip0_9717_7433)">
<path d="M17.1131 10.6766H15.5664C15.7241 11.1083 15.8102 11.5741 15.8102 12.0596V17.9053C15.8102 18.1077 15.775 18.302 15.7109 18.4827H18.2679C19.2231 18.4827 20.0002 17.7056 20.0002 16.7505V13.5637C20.0002 11.9718 18.7051 10.6766 17.1131 10.6766Z" fill="#023DFE" fill-opacity="0.7"/>
<path d="M4.19005 12.0596C4.19005 11.5741 4.27618 11.1083 4.43384 10.6766H2.88712C1.29516 10.6766 0 11.9718 0 13.5637V16.7505C0 17.7057 0.777072 18.4828 1.73227 18.4828H4.28938C4.22528 18.302 4.19005 18.1077 4.19005 17.9053V12.0596Z" fill="#023DFE" fill-opacity="0.7"/>
<path d="M11.7679 9.17249H8.23184C6.63989 9.17249 5.34473 10.4676 5.34473 12.0596V17.9053C5.34473 18.2242 5.60324 18.4827 5.92215 18.4827H14.0776C14.3965 18.4827 14.655 18.2242 14.655 17.9053V12.0596C14.655 10.4676 13.3598 9.17249 11.7679 9.17249Z" fill="#023DFE" fill-opacity="0.7"/>
<path d="M9.99995 1.51721C8.08541 1.51721 6.52783 3.07479 6.52783 4.98937C6.52783 6.288 7.24459 7.42218 8.30311 8.01765C8.80518 8.30008 9.38401 8.46148 9.99995 8.46148C10.6159 8.46148 11.1947 8.30008 11.6968 8.01765C12.7553 7.42218 13.4721 6.28796 13.4721 4.98937C13.4721 3.07483 11.9145 1.51721 9.99995 1.51721Z" fill="#023DFE" fill-opacity="0.7"/>
<path d="M3.90284 4.75354C2.471 4.75354 1.30615 5.91839 1.30615 7.35022C1.30615 8.78206 2.471 9.94691 3.90284 9.94691C4.26604 9.94691 4.6119 9.87168 4.92608 9.73644C5.46929 9.50257 5.91718 9.08859 6.19433 8.57003C6.38886 8.20609 6.49952 7.79089 6.49952 7.35022C6.49952 5.91843 5.33468 4.75354 3.90284 4.75354Z" fill="#023DFE" fill-opacity="0.7"/>
<path d="M16.0972 4.75354C14.6653 4.75354 13.5005 5.91839 13.5005 7.35022C13.5005 7.79093 13.6112 8.20612 13.8057 8.57003C14.0828 9.08863 14.5307 9.50261 15.0739 9.73644C15.3881 9.87168 15.734 9.94691 16.0972 9.94691C17.529 9.94691 18.6939 8.78206 18.6939 7.35022C18.6939 5.91839 17.529 4.75354 16.0972 4.75354Z" fill="#023DFE" fill-opacity="0.7"/>
</g>
<defs>
<clipPath id="clip0_9717_7433">
<rect width="20" height="20" fill="white"/>
</clipPath>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 2.1 KiB

View File

@ -0,0 +1,4 @@
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M10.0002 5.97498L3.12109 11.2683V18.3601H8.64871V13.163H11.5852V18.3601H16.8794V11.2683L10.0002 5.97498Z" fill="#023DFE" fill-opacity="0.7"/>
<path d="M17.1673 7.15356V3.52759H14.2702V4.92485L10 1.63989L0 9.33274L1.38043 11.1271L10 4.49458L18.6196 11.1272L20 9.33278L17.1673 7.15356Z" fill="#023DFE" fill-opacity="0.7"/>
</svg>

After

Width:  |  Height:  |  Size: 433 B

View File

@ -0,0 +1,4 @@
<svg width="50" height="50" viewBox="0 0 50 50" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M37.8033 16.3567C37.2313 15.7848 36.3039 15.7847 35.7318 16.3568L21.5532 30.5353L14.2681 23.2504C13.6962 22.6784 12.7686 22.6783 12.1966 23.2505C11.6246 23.8226 11.6246 24.75 12.1966 25.3221L20.5174 33.6427C20.8034 33.9287 21.1783 34.0717 21.5531 34.0717C21.928 34.0717 22.3029 33.9287 22.5888 33.6426L37.8033 18.4283C38.3754 17.8563 38.3754 16.9288 37.8033 16.3567Z" fill="#023DFE" fill-opacity="0.7"/>
<path d="M42.6776 7.32236C37.9558 2.60049 31.6776 0 25 0C18.3223 0 12.0442 2.60049 7.32236 7.32236C2.60039 12.0443 0 18.3224 0 25C0 31.6778 2.60039 37.9559 7.32236 42.6777C12.0441 47.3996 18.3223 50 25 50C31.6777 50 37.9558 47.3996 42.6776 42.6777C47.3995 37.9559 50 31.6778 50 25C50 18.3224 47.3995 12.0443 42.6776 7.32236ZM25 47.0703C12.8304 47.0703 2.92969 37.1696 2.92969 25C2.92969 12.8304 12.8304 2.92969 25 2.92969C37.1696 2.92969 47.0703 12.8304 47.0703 25C47.0703 37.1696 37.1696 47.0703 25 47.0703Z" fill="#023DFE" fill-opacity="0.7"/>
</svg>

After

Width:  |  Height:  |  Size: 1.0 KiB

View File

@ -0,0 +1,52 @@
import 'package:flutter/material.dart';
import 'package:syncrow_web/pages/access_management/booking_system/view/widgets/icon_text_button.dart';
import 'package:syncrow_web/utils/constants/assets.dart';
class BookingPage extends StatelessWidget {
const BookingPage({super.key});
@override
Widget build(BuildContext context) {
return Container(
child: Row(
children: [
Expanded(
child: Container(
color: Colors.blueGrey[100],
child: const Center(
child: Text(
'Side bar',
style: TextStyle(fontSize: 24, fontWeight: FontWeight.bold),
),
),
)),
Expanded(
flex: 4,
child: Padding(
padding: const EdgeInsets.all(20.0),
child: SizedBox(
child: Column(
children: [
Row(
mainAxisAlignment: MainAxisAlignment.start,
children: [
SvgTextButton(
svgAsset: Assets.homeIcon,
label: 'Manage Bookable Spaces',
onPressed: () {}),
SizedBox(width: 20),
SvgTextButton(
svgAsset: Assets.groupIcon,
label: 'Manage Users',
onPressed: () {})
],
)
],
),
),
))
],
),
);
}
}

View File

@ -0,0 +1,73 @@
import 'package:flutter/material.dart';
import 'package:flutter_svg/flutter_svg.dart';
import 'package:syncrow_web/utils/color_manager.dart';
class SvgTextButton extends StatelessWidget {
final String svgAsset;
final String label;
final VoidCallback onPressed;
final Color backgroundColor;
final Color svgColor;
final Color labelColor;
final double borderRadius;
final List<BoxShadow> boxShadow;
final double svgSize;
const SvgTextButton({
super.key,
required this.svgAsset,
required this.label,
required this.onPressed,
this.backgroundColor = ColorsManager.circleRolesBackground,
this.svgColor = const Color(0xFF496EFF),
this.labelColor = Colors.black87,
this.borderRadius = 10.0,
this.boxShadow = const [
BoxShadow(
color: ColorsManager.textGray,
blurRadius: 12,
offset: Offset(0, 4),
),
],
this.svgSize = 24.0,
});
@override
Widget build(BuildContext context) {
return Material(
color: Colors.transparent,
child: InkWell(
borderRadius: BorderRadius.circular(borderRadius),
onTap: onPressed,
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 12),
decoration: BoxDecoration(
color: backgroundColor,
borderRadius: BorderRadius.circular(borderRadius),
boxShadow: boxShadow,
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
SvgPicture.asset(
svgAsset,
width: svgSize,
height: svgSize,
color: svgColor,
),
const SizedBox(width: 12),
Text(
label,
style: TextStyle(
color: labelColor,
fontSize: 16,
fontWeight: FontWeight.w500,
),
),
],
),
),
),
);
}
}

View File

@ -2,302 +2,86 @@ import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:syncrow_web/pages/access_management/bloc/access_bloc.dart';
import 'package:syncrow_web/pages/access_management/bloc/access_event.dart';
import 'package:syncrow_web/pages/access_management/bloc/access_state.dart';
import 'package:syncrow_web/pages/common/buttons/default_button.dart';
import 'package:syncrow_web/pages/common/buttons/search_reset_buttons.dart';
import 'package:syncrow_web/pages/common/custom_table.dart';
import 'package:syncrow_web/pages/common/date_time_widget.dart';
import 'package:syncrow_web/pages/common/filter/filter_widget.dart';
import 'package:syncrow_web/pages/common/text_field/custom_web_textfield.dart';
import 'package:syncrow_web/pages/access_management/booking_system/view/booking_page.dart';
import 'package:syncrow_web/pages/access_management/view/access_overview_content.dart';
import 'package:syncrow_web/pages/device_managment/shared/navigate_home_grid_view.dart';
import 'package:syncrow_web/pages/visitor_password/view/visitor_password_dialog.dart';
// import 'package:syncrow_web/utils/color_manager.dart';
import 'package:syncrow_web/utils/constants/app_enum.dart';
import 'package:syncrow_web/utils/constants/assets.dart';
import 'package:syncrow_web/utils/extension/build_context_x.dart';
import 'package:syncrow_web/utils/helpers/responsice_layout_helper/responsive_layout_helper.dart';
import 'package:syncrow_web/utils/style.dart';
import 'package:syncrow_web/utils/theme/responsive_text_theme.dart';
import 'package:syncrow_web/web_layout/web_scaffold.dart';
class AccessManagementPage extends StatelessWidget with HelperResponsiveLayout {
class AccessManagementPage extends StatefulWidget {
const AccessManagementPage({super.key});
@override
Widget build(BuildContext context) {
final isLargeScreen = isLargeScreenSize(context);
final isSmallScreen = isSmallScreenSize(context);
final isHalfMediumScreen = isHafMediumScreenSize(context);
final padding =
isLargeScreen ? const EdgeInsets.all(30) : const EdgeInsets.all(15);
State<AccessManagementPage> createState() => _AccessManagementPageState();
}
return WebScaffold(
class _AccessManagementPageState extends State<AccessManagementPage>
with HelperResponsiveLayout {
final PageController _pageController = PageController(initialPage: 0);
int _currentPageIndex = 0;
@override
void dispose() {
_pageController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return BlocProvider(
create: (BuildContext context) => AccessBloc()..add(FetchTableData()),
child: WebScaffold(
enableMenuSidebar: false,
appBarTitle: Text(
'Access Management',
style: ResponsiveTextTheme.of(context).deviceManagementTitle,
),
rightBody: const NavigateHomeGridView(),
scaffoldBody: BlocProvider(
create: (BuildContext context) =>
AccessBloc()..add(FetchTableData()),
child: BlocConsumer<AccessBloc, AccessState>(
listener: (context, state) {},
builder: (context, state) {
final accessBloc = BlocProvider.of<AccessBloc>(context);
final filteredData = accessBloc.filteredData;
return state is AccessLoaded
? const Center(child: CircularProgressIndicator())
: Container(
padding: padding,
height: MediaQuery.of(context).size.height,
width: MediaQuery.of(context).size.width,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
FilterWidget(
size: MediaQuery.of(context).size,
tabs: accessBloc.tabs,
selectedIndex: accessBloc.selectedIndex,
onTabChanged: (index) {
accessBloc.add(TabChangedEvent(index));
},
),
const SizedBox(height: 20),
if (isSmallScreen || isHalfMediumScreen)
_buildSmallSearchFilters(context, accessBloc)
else
_buildNormalSearchWidgets(context, accessBloc),
const SizedBox(height: 20),
_buildVisitorAdminPasswords(context, accessBloc),
const SizedBox(height: 20),
Expanded(
child: DynamicTable(
tableName: 'AccessManagement',
uuidIndex: 1,
withSelectAll: true,
isEmpty: filteredData.isEmpty,
withCheckBox: false,
size: MediaQuery.of(context).size,
cellDecoration: containerDecoration,
headers: const [
'Name',
'Access Type',
'Access Start',
'Access End',
'Accessible Device',
'Authorizer',
'Authorization Date & Time',
'Access Status'
],
data: filteredData.map((item) {
return [
item.passwordName,
item.passwordType.value,
accessBloc
.timestampToDate(item.effectiveTime),
accessBloc
.timestampToDate(item.invalidTime),
item.deviceName.toString(),
item.authorizerEmail.toString(),
accessBloc
.timestampToDate(item.invalidTime),
item.passwordStatus.value,
];
}).toList(),
)),
],
),
);
})));
}
Wrap _buildVisitorAdminPasswords(
BuildContext context, AccessBloc accessBloc) {
return Wrap(
spacing: 10,
runSpacing: 10,
children: [
Container(
width: 205,
height: 42,
decoration: containerDecoration,
child: DefaultButton(
onPressed: () {
showDialog(
context: context,
barrierDismissible: false,
builder: (BuildContext context) {
return const VisitorPasswordDialog();
},
).then((v) {
if (v != null) {
accessBloc.add(FetchTableData());
}
});
},
borderRadius: 8,
centerBody: Row(
mainAxisSize: MainAxisSize.min,
children: [
TextButton(
onPressed: () => _switchPage(0),
child: Text(
'Create Visitor Password ',
style: context.textTheme.titleSmall!
.copyWith(color: Colors.white, fontSize: 12),
)),
'Access Overview',
style: context.textTheme.titleMedium?.copyWith(
color: _currentPageIndex == 0 ? Colors.white : Colors.grey,
fontWeight: _currentPageIndex == 0
? FontWeight.w700
: FontWeight.w400,
),
),
),
TextButton(
onPressed: () => _switchPage(1),
child: Text(
'Booking System',
style: context.textTheme.titleMedium?.copyWith(
color: _currentPageIndex == 1 ? Colors.white : Colors.grey,
fontWeight: _currentPageIndex == 1
? FontWeight.w700
: FontWeight.w400,
),
),
),
],
),
// Container(
// width: 133,
// height: 42,
// decoration: containerDecoration,
// child: DefaultButton(
// borderRadius: 8,
// backgroundColor: ColorsManager.whiteColors,
// child: Text(
// 'Admin Password',
// style: context.textTheme.titleSmall!
// .copyWith(color: Colors.black, fontSize: 12),
// )),
// ),
],
rightBody: const NavigateHomeGridView(),
scaffoldBody: PageView(
controller: _pageController,
physics: const NeverScrollableScrollPhysics(),
children: const [
AccessOverviewContent(),
BookingPage(),
],
),
),
);
}
Row _buildNormalSearchWidgets(BuildContext context, AccessBloc accessBloc) {
// TimeOfDay _selectedTime = TimeOfDay.now();
return Row(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.end,
textBaseline: TextBaseline.ideographic,
children: [
SizedBox(
width: 250,
child: CustomWebTextField(
controller: accessBloc.passwordName,
height: 43,
isRequired: false,
textFieldName: 'Name',
description: '',
onSubmitted: (value) {
accessBloc.add(FilterDataEvent(
emailAuthorizer:
accessBloc.emailAuthorizer.text.toLowerCase(),
selectedTabIndex:
BlocProvider.of<AccessBloc>(context).selectedIndex,
passwordName: accessBloc.passwordName.text.toLowerCase(),
startTime: accessBloc.effectiveTimeTimeStamp,
endTime: accessBloc.expirationTimeTimeStamp));
},
),
),
const SizedBox(width: 15),
SizedBox(
width: 250,
child: CustomWebTextField(
controller: accessBloc.emailAuthorizer,
height: 43,
isRequired: false,
textFieldName: 'Authorizer',
description: '',
onSubmitted: (value) {
accessBloc.add(FilterDataEvent(
emailAuthorizer:
accessBloc.emailAuthorizer.text.toLowerCase(),
selectedTabIndex:
BlocProvider.of<AccessBloc>(context).selectedIndex,
passwordName: accessBloc.passwordName.text.toLowerCase(),
startTime: accessBloc.effectiveTimeTimeStamp,
endTime: accessBloc.expirationTimeTimeStamp));
},
),
),
const SizedBox(width: 15),
SizedBox(
child: DateTimeWebWidget(
icon: Assets.calendarIcon,
isRequired: false,
title: 'Access Time',
size: MediaQuery.of(context).size,
endTime: () {
accessBloc.add(SelectTime(context: context, isStart: false));
},
startTime: () {
accessBloc.add(SelectTime(context: context, isStart: true));
},
firstString: BlocProvider.of<AccessBloc>(context).startTime,
secondString: BlocProvider.of<AccessBloc>(context).endTime,
),
),
const SizedBox(width: 15),
SearchResetButtons(
onSearch: () {
accessBloc.add(FilterDataEvent(
emailAuthorizer: accessBloc.emailAuthorizer.text.toLowerCase(),
selectedTabIndex:
BlocProvider.of<AccessBloc>(context).selectedIndex,
passwordName: accessBloc.passwordName.text.toLowerCase(),
startTime: accessBloc.effectiveTimeTimeStamp,
endTime: accessBloc.expirationTimeTimeStamp));
},
onReset: () {
accessBloc.add(ResetSearch());
},
),
],
);
}
Widget _buildSmallSearchFilters(BuildContext context, AccessBloc accessBloc) {
return Wrap(
spacing: 20,
runSpacing: 10,
children: [
SizedBox(
width: 300,
child: CustomWebTextField(
controller: accessBloc.passwordName,
isRequired: true,
height: 40,
textFieldName: 'Name',
description: '',
onSubmitted: (value) {
accessBloc.add(FilterDataEvent(
emailAuthorizer:
accessBloc.emailAuthorizer.text.toLowerCase(),
selectedTabIndex:
BlocProvider.of<AccessBloc>(context).selectedIndex,
passwordName: accessBloc.passwordName.text.toLowerCase(),
startTime: accessBloc.effectiveTimeTimeStamp,
endTime: accessBloc.expirationTimeTimeStamp));
}),
),
DateTimeWebWidget(
icon: Assets.calendarIcon,
isRequired: false,
title: 'Access Time',
size: MediaQuery.of(context).size,
endTime: () {
accessBloc.add(SelectTime(context: context, isStart: false));
},
startTime: () {
accessBloc.add(SelectTime(context: context, isStart: true));
},
firstString: BlocProvider.of<AccessBloc>(context).startTime,
secondString: BlocProvider.of<AccessBloc>(context).endTime,
),
SearchResetButtons(
onSearch: () {
accessBloc.add(FilterDataEvent(
emailAuthorizer: accessBloc.emailAuthorizer.text.toLowerCase(),
selectedTabIndex:
BlocProvider.of<AccessBloc>(context).selectedIndex,
passwordName: accessBloc.passwordName.text.toLowerCase(),
startTime: accessBloc.effectiveTimeTimeStamp,
endTime: accessBloc.expirationTimeTimeStamp));
},
onReset: () {
accessBloc.add(ResetSearch());
},
),
],
);
void _switchPage(int index) {
setState(() => _currentPageIndex = index);
_pageController.jumpToPage(index);
}
}

View File

@ -0,0 +1,289 @@
import 'package:syncrow_web/pages/common/buttons/default_button.dart';
import 'package:syncrow_web/pages/common/buttons/search_reset_buttons.dart';
import 'package:syncrow_web/pages/common/custom_table.dart';
import 'package:syncrow_web/pages/common/date_time_widget.dart';
import 'package:syncrow_web/pages/common/filter/filter_widget.dart';
import 'package:syncrow_web/pages/common/text_field/custom_web_textfield.dart';
import 'package:flutter/material.dart';
import 'package:syncrow_web/pages/access_management/bloc/access_state.dart';
import 'package:syncrow_web/pages/visitor_password/view/visitor_password_dialog.dart';
import 'package:syncrow_web/utils/constants/app_enum.dart';
import 'package:syncrow_web/utils/constants/assets.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:syncrow_web/pages/access_management/bloc/access_bloc.dart';
import 'package:syncrow_web/pages/access_management/bloc/access_event.dart';
import 'package:syncrow_web/utils/extension/build_context_x.dart';
import 'package:syncrow_web/utils/helpers/responsice_layout_helper/responsive_layout_helper.dart';
import 'package:syncrow_web/utils/style.dart';
class AccessOverviewContent extends StatelessWidget
with HelperResponsiveLayout {
const AccessOverviewContent({super.key});
@override
Widget build(BuildContext context) {
final isLargeScreen = isLargeScreenSize(context);
final isSmallScreen = isSmallScreenSize(context);
final isHalfMediumScreen = isHafMediumScreenSize(context);
final padding =
isLargeScreen ? const EdgeInsets.all(30) : const EdgeInsets.all(15);
return BlocProvider(
create: (BuildContext context) => AccessBloc()..add(FetchTableData()),
child: BlocConsumer<AccessBloc, AccessState>(
listener: (context, state) {},
builder: (context, state) {
final accessBloc = BlocProvider.of<AccessBloc>(context);
final filteredData = accessBloc.filteredData;
return state is AccessLoaded
? const Center(child: CircularProgressIndicator())
: Container(
padding: padding,
height: MediaQuery.of(context).size.height,
width: MediaQuery.of(context).size.width,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
FilterWidget(
size: MediaQuery.of(context).size,
tabs: accessBloc.tabs,
selectedIndex: accessBloc.selectedIndex,
onTabChanged: (index) {
accessBloc.add(TabChangedEvent(index));
},
),
const SizedBox(height: 20),
if (isSmallScreen || isHalfMediumScreen)
_buildSmallSearchFilters(context, accessBloc)
else
_buildNormalSearchWidgets(context, accessBloc),
const SizedBox(height: 20),
_buildVisitorAdminPasswords(context, accessBloc),
const SizedBox(height: 20),
Expanded(
child: DynamicTable(
tableName: 'AccessManagement',
uuidIndex: 1,
withSelectAll: true,
isEmpty: filteredData.isEmpty,
withCheckBox: false,
size: MediaQuery.of(context).size,
cellDecoration: containerDecoration,
headers: const [
'Name',
'Access Type',
'Access Start',
'Access End',
'Accessible Device',
'Authorizer',
'Authorization Date & Time',
'Access Status'
],
data: filteredData.map((item) {
return [
item.passwordName,
item.passwordType.value,
accessBloc.timestampToDate(item.effectiveTime),
accessBloc.timestampToDate(item.invalidTime),
item.deviceName.toString(),
item.authorizerEmail.toString(),
accessBloc.timestampToDate(item.invalidTime),
item.passwordStatus.value,
];
}).toList(),
)),
],
),
);
}));
}
Wrap _buildVisitorAdminPasswords(
BuildContext context, AccessBloc accessBloc) {
return Wrap(
spacing: 10,
runSpacing: 10,
children: [
Container(
width: 205,
height: 42,
decoration: containerDecoration,
child: DefaultButton(
onPressed: () {
showDialog(
context: context,
barrierDismissible: false,
builder: (BuildContext context) {
return const VisitorPasswordDialog();
},
).then((v) {
if (v != null) {
accessBloc.add(FetchTableData());
}
});
},
borderRadius: 8,
child: Text(
'Create Visitor Password ',
style: context.textTheme.titleSmall!
.copyWith(color: Colors.white, fontSize: 12),
)),
),
// Container(
// width: 133,
// height: 42,
// decoration: containerDecoration,
// child: DefaultButton(
// borderRadius: 8,
// backgroundColor: ColorsManager.whiteColors,
// child: Text(
// 'Admin Password',
// style: context.textTheme.titleSmall!
// .copyWith(color: Colors.black, fontSize: 12),
// )),
// ),
],
);
}
Row _buildNormalSearchWidgets(BuildContext context, AccessBloc accessBloc) {
// TimeOfDay _selectedTime = TimeOfDay.now();
return Row(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.end,
textBaseline: TextBaseline.ideographic,
children: [
SizedBox(
width: 250,
child: CustomWebTextField(
controller: accessBloc.passwordName,
height: 43,
isRequired: false,
textFieldName: 'Name',
description: '',
onSubmitted: (value) {
accessBloc.add(FilterDataEvent(
emailAuthorizer:
accessBloc.emailAuthorizer.text.toLowerCase(),
selectedTabIndex:
BlocProvider.of<AccessBloc>(context).selectedIndex,
passwordName: accessBloc.passwordName.text.toLowerCase(),
startTime: accessBloc.effectiveTimeTimeStamp,
endTime: accessBloc.expirationTimeTimeStamp));
},
),
),
const SizedBox(width: 15),
SizedBox(
width: 250,
child: CustomWebTextField(
controller: accessBloc.emailAuthorizer,
height: 43,
isRequired: false,
textFieldName: 'Authorizer',
description: '',
onSubmitted: (value) {
accessBloc.add(FilterDataEvent(
emailAuthorizer:
accessBloc.emailAuthorizer.text.toLowerCase(),
selectedTabIndex:
BlocProvider.of<AccessBloc>(context).selectedIndex,
passwordName: accessBloc.passwordName.text.toLowerCase(),
startTime: accessBloc.effectiveTimeTimeStamp,
endTime: accessBloc.expirationTimeTimeStamp));
},
),
),
const SizedBox(width: 15),
SizedBox(
child: DateTimeWebWidget(
icon: Assets.calendarIcon,
isRequired: false,
title: 'Access Time',
size: MediaQuery.of(context).size,
endTime: () {
accessBloc.add(SelectTime(context: context, isStart: false));
},
startTime: () {
accessBloc.add(SelectTime(context: context, isStart: true));
},
firstString: BlocProvider.of<AccessBloc>(context).startTime,
secondString: BlocProvider.of<AccessBloc>(context).endTime,
),
),
const SizedBox(width: 15),
SearchResetButtons(
onSearch: () {
accessBloc.add(FilterDataEvent(
emailAuthorizer: accessBloc.emailAuthorizer.text.toLowerCase(),
selectedTabIndex:
BlocProvider.of<AccessBloc>(context).selectedIndex,
passwordName: accessBloc.passwordName.text.toLowerCase(),
startTime: accessBloc.effectiveTimeTimeStamp,
endTime: accessBloc.expirationTimeTimeStamp));
},
onReset: () {
accessBloc.add(ResetSearch());
},
),
],
);
}
Widget _buildSmallSearchFilters(BuildContext context, AccessBloc accessBloc) {
return Wrap(
spacing: 20,
runSpacing: 10,
children: [
SizedBox(
width: 300,
child: CustomWebTextField(
controller: accessBloc.passwordName,
isRequired: true,
height: 40,
textFieldName: 'Name',
description: '',
onSubmitted: (value) {
accessBloc.add(FilterDataEvent(
emailAuthorizer:
accessBloc.emailAuthorizer.text.toLowerCase(),
selectedTabIndex:
BlocProvider.of<AccessBloc>(context).selectedIndex,
passwordName: accessBloc.passwordName.text.toLowerCase(),
startTime: accessBloc.effectiveTimeTimeStamp,
endTime: accessBloc.expirationTimeTimeStamp));
}),
),
DateTimeWebWidget(
icon: Assets.calendarIcon,
isRequired: false,
title: 'Access Time',
size: MediaQuery.of(context).size,
endTime: () {
accessBloc.add(SelectTime(context: context, isStart: false));
},
startTime: () {
accessBloc.add(SelectTime(context: context, isStart: true));
},
firstString: BlocProvider.of<AccessBloc>(context).startTime,
secondString: BlocProvider.of<AccessBloc>(context).endTime,
),
SearchResetButtons(
onSearch: () {
accessBloc.add(FilterDataEvent(
emailAuthorizer: accessBloc.emailAuthorizer.text.toLowerCase(),
selectedTabIndex:
BlocProvider.of<AccessBloc>(context).selectedIndex,
passwordName: accessBloc.passwordName.text.toLowerCase(),
startTime: accessBloc.effectiveTimeTimeStamp,
endTime: accessBloc.expirationTimeTimeStamp));
},
onReset: () {
accessBloc.add(ResetSearch());
},
),
],
);
}
}

View File

@ -39,8 +39,12 @@ 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? ?? '0.0') : null,
longitude: json['lon'] != null ? double.parse(json['lon'] as String? ?? '0.0') : null,
latitude: json['lat'] != null && json['lat'] != ''
? double.tryParse(json['lat']?.toString() ?? '0.0')
: null,
longitude: json['lon'] != null && json['lon'] != ''
? double.tryParse(json['lon']?.toString() ?? '0.0')
: null,
);
}
}

View File

@ -20,6 +20,7 @@ class AqiDistributionChart extends StatelessWidget {
return BarChart(
BarChartData(
maxY: 100.1,
alignment: BarChartAlignment.start,
gridData: EnergyManagementChartsHelper.gridData(
horizontalInterval: 20,
),

View File

@ -3,7 +3,9 @@ import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:syncrow_web/pages/analytics/modules/air_quality/blocs/air_quality_distribution/air_quality_distribution_bloc.dart';
import 'package:syncrow_web/pages/analytics/modules/air_quality/widgets/aqi_distribution_chart.dart';
import 'package:syncrow_web/pages/analytics/modules/air_quality/widgets/aqi_distribution_chart_title.dart';
import 'package:syncrow_web/pages/analytics/widgets/analytics_chart_empty_state_widget.dart';
import 'package:syncrow_web/pages/analytics/widgets/analytics_error_widget.dart';
import 'package:syncrow_web/utils/constants/assets.dart';
import 'package:syncrow_web/utils/style.dart';
class AqiDistributionChartBox extends StatelessWidget {
@ -32,8 +34,20 @@ class AqiDistributionChartBox extends StatelessWidget {
const SizedBox(height: 10),
const Divider(),
const SizedBox(height: 20),
Expanded(
child: AqiDistributionChart(chartData: state.chartData),
Visibility(
visible: state.chartData.isNotEmpty,
replacement: AnalyticsChartEmptyStateWidget(
isLoading: state.status == AirQualityDistributionStatus.loading,
isError: state.status == AirQualityDistributionStatus.failure,
isInitial: state.status == AirQualityDistributionStatus.initial,
errorMessage: state.errorMessage,
iconPath: Assets.emptyBarredChart,
),
child: Expanded(
child: AqiDistributionChart(
chartData: state.chartData,
),
),
),
],
),

View File

@ -3,7 +3,9 @@ import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:syncrow_web/pages/analytics/modules/air_quality/blocs/range_of_aqi/range_of_aqi_bloc.dart';
import 'package:syncrow_web/pages/analytics/modules/air_quality/widgets/range_of_aqi_chart.dart';
import 'package:syncrow_web/pages/analytics/modules/air_quality/widgets/range_of_aqi_chart_title.dart';
import 'package:syncrow_web/pages/analytics/widgets/analytics_chart_empty_state_widget.dart';
import 'package:syncrow_web/pages/analytics/widgets/analytics_error_widget.dart';
import 'package:syncrow_web/utils/constants/assets.dart';
import 'package:syncrow_web/utils/style.dart';
class RangeOfAqiChartBox extends StatelessWidget {
@ -32,10 +34,20 @@ class RangeOfAqiChartBox extends StatelessWidget {
const SizedBox(height: 10),
const Divider(),
const SizedBox(height: 20),
Expanded(
child: RangeOfAqiChart(
chartData: state.filteredRangeOfAqi,
selectedAqiType: state.selectedAqiType,
Visibility(
visible: state.filteredRangeOfAqi.isNotEmpty,
replacement: AnalyticsChartEmptyStateWidget(
isLoading: state.status == RangeOfAqiStatus.loading,
isError: state.status == RangeOfAqiStatus.failure,
isInitial: state.status == RangeOfAqiStatus.initial,
errorMessage: state.errorMessage,
iconPath: Assets.emptyRangeOfAqi,
),
child: Expanded(
child: RangeOfAqiChart(
chartData: state.filteredRangeOfAqi,
selectedAqiType: state.selectedAqiType,
),
),
),
],

View File

@ -38,7 +38,7 @@ class AnalyticsEnergyManagementView extends StatelessWidget {
return SingleChildScrollView(
child: Container(
padding: _padding,
height: MediaQuery.sizeOf(context).height * 1,
height: MediaQuery.sizeOf(context).height * 1.05,
child: const Column(
children: [
Expanded(

View File

@ -5,8 +5,10 @@ import 'package:syncrow_web/pages/analytics/modules/energy_management/blocs/ener
import 'package:syncrow_web/pages/analytics/modules/energy_management/widgets/chart_title.dart';
import 'package:syncrow_web/pages/analytics/modules/energy_management/widgets/energy_consumption_per_device_chart.dart';
import 'package:syncrow_web/pages/analytics/modules/energy_management/widgets/energy_consumption_per_device_devices_list.dart';
import 'package:syncrow_web/pages/analytics/widgets/analytics_chart_empty_state_widget.dart';
import 'package:syncrow_web/pages/analytics/widgets/analytics_error_widget.dart';
import 'package:syncrow_web/pages/analytics/widgets/charts_loading_widget.dart';
import 'package:syncrow_web/utils/constants/assets.dart';
import 'package:syncrow_web/utils/style.dart';
class EnergyConsumptionPerDeviceChartBox extends StatelessWidget {
@ -54,8 +56,24 @@ class EnergyConsumptionPerDeviceChartBox extends StatelessWidget {
const SizedBox(height: 20),
const Divider(height: 0),
const SizedBox(height: 20),
Expanded(
child: EnergyConsumptionPerDeviceChart(chartData: state.chartData),
Visibility(
visible: state.chartData.isNotEmpty &&
state.chartData
.every((e) => e.energy.every((e) => e.value != 0)),
replacement: AnalyticsChartEmptyStateWidget(
isLoading:
state.status == EnergyConsumptionPerDeviceStatus.loading,
isError: state.status == EnergyConsumptionPerDeviceStatus.failure,
isInitial:
state.status == EnergyConsumptionPerDeviceStatus.initial,
errorMessage: state.errorMessage,
iconPath: Assets.emptyEnergyManagementPerDevice,
),
child: Expanded(
child: EnergyConsumptionPerDeviceChart(
chartData: state.chartData,
),
),
),
],
),

View File

@ -3,8 +3,10 @@ import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:syncrow_web/pages/analytics/modules/energy_management/blocs/total_energy_consumption/total_energy_consumption_bloc.dart';
import 'package:syncrow_web/pages/analytics/modules/energy_management/widgets/chart_title.dart';
import 'package:syncrow_web/pages/analytics/modules/energy_management/widgets/total_energy_consumption_chart.dart';
import 'package:syncrow_web/pages/analytics/widgets/analytics_chart_empty_state_widget.dart';
import 'package:syncrow_web/pages/analytics/widgets/analytics_error_widget.dart';
import 'package:syncrow_web/pages/analytics/widgets/charts_loading_widget.dart';
import 'package:syncrow_web/utils/constants/assets.dart';
import 'package:syncrow_web/utils/style.dart';
class TotalEnergyConsumptionChartBox extends StatelessWidget {
@ -41,7 +43,18 @@ class TotalEnergyConsumptionChartBox extends StatelessWidget {
const SizedBox(height: 20),
const Divider(),
const SizedBox(height: 20),
TotalEnergyConsumptionChart(chartData: state.chartData),
Visibility(
visible: state.chartData.isNotEmpty &&
state.chartData.every((e) => e.value != 0),
replacement: AnalyticsChartEmptyStateWidget(
isLoading: state.status == TotalEnergyConsumptionStatus.loading,
isError: state.status == TotalEnergyConsumptionStatus.failure,
isInitial: state.status == TotalEnergyConsumptionStatus.initial,
errorMessage: state.errorMessage,
iconPath: Assets.emptyEnergyManagementChart,
),
child: TotalEnergyConsumptionChart(chartData: state.chartData),
),
],
),
),

View File

@ -18,6 +18,7 @@ class OccupancyChart extends StatelessWidget {
return BarChart(
BarChartData(
maxY: 100.001,
alignment: BarChartAlignment.start,
gridData: EnergyManagementChartsHelper.gridData().copyWith(
checkToShowHorizontalLine: (value) => true,
horizontalInterval: 20,

View File

@ -6,8 +6,10 @@ import 'package:syncrow_web/pages/analytics/modules/energy_management/widgets/ch
import 'package:syncrow_web/pages/analytics/modules/occupancy/blocs/occupancy/occupancy_bloc.dart';
import 'package:syncrow_web/pages/analytics/modules/occupancy/helpers/fetch_occupancy_data_helper.dart';
import 'package:syncrow_web/pages/analytics/modules/occupancy/widgets/occupancy_chart.dart';
import 'package:syncrow_web/pages/analytics/widgets/analytics_chart_empty_state_widget.dart';
import 'package:syncrow_web/pages/analytics/widgets/analytics_error_widget.dart';
import 'package:syncrow_web/pages/space_tree/bloc/space_tree_bloc.dart';
import 'package:syncrow_web/utils/constants/assets.dart';
import 'package:syncrow_web/utils/style.dart';
class OccupancyChartBox extends StatelessWidget {
@ -67,7 +69,24 @@ class OccupancyChartBox extends StatelessWidget {
const SizedBox(height: 20),
const Divider(),
const SizedBox(height: 20),
Expanded(child: OccupancyChart(chartData: state.chartData)),
Visibility(
visible: state.chartData.isNotEmpty &&
state.chartData.every(
(e) => e.occupancy.isNotEmpty,
),
replacement: AnalyticsChartEmptyStateWidget(
isLoading: state.status == OccupancyStatus.loading,
isError: state.status == OccupancyStatus.failure,
isInitial: state.status == OccupancyStatus.initial,
errorMessage: state.errorMessage,
iconPath: Assets.emptyBarredChart,
),
child: Expanded(
child: OccupancyChart(
chartData: state.chartData,
),
),
),
],
),
);

View File

@ -6,8 +6,10 @@ import 'package:syncrow_web/pages/analytics/modules/energy_management/widgets/ch
import 'package:syncrow_web/pages/analytics/modules/occupancy/blocs/occupancy_heat_map/occupancy_heat_map_bloc.dart';
import 'package:syncrow_web/pages/analytics/modules/occupancy/helpers/fetch_occupancy_data_helper.dart';
import 'package:syncrow_web/pages/analytics/modules/occupancy/widgets/occupancy_heat_map.dart';
import 'package:syncrow_web/pages/analytics/widgets/analytics_chart_empty_state_widget.dart';
import 'package:syncrow_web/pages/analytics/widgets/analytics_error_widget.dart';
import 'package:syncrow_web/pages/space_tree/bloc/space_tree_bloc.dart';
import 'package:syncrow_web/utils/constants/assets.dart';
import 'package:syncrow_web/utils/style.dart';
class OccupancyHeatMapBox extends StatelessWidget {
@ -68,16 +70,29 @@ class OccupancyHeatMapBox extends StatelessWidget {
const SizedBox(height: 20),
const Divider(),
const SizedBox(height: 20),
Expanded(
child: OccupancyHeatMap(
selectedDate:
context.watch<AnalyticsDatePickerBloc>().state.yearlyDate,
heatMapData: state.heatMapData.asMap().map(
(_, value) => MapEntry(
value.eventDate,
value.countTotalPresenceDetected,
Visibility(
visible: state.heatMapData.isNotEmpty &&
state.heatMapData.every(
(e) => e.countTotalPresenceDetected != 0,
),
replacement: AnalyticsChartEmptyStateWidget(
isLoading: state.status == OccupancyHeatMapStatus.loading,
isError: state.status == OccupancyHeatMapStatus.failure,
isInitial: state.status == OccupancyHeatMapStatus.initial,
errorMessage: state.errorMessage,
iconPath: Assets.emptyHeatmap,
),
child: Expanded(
child: OccupancyHeatMap(
selectedDate:
context.watch<AnalyticsDatePickerBloc>().state.yearlyDate,
heatMapData: state.heatMapData.asMap().map(
(_, value) => MapEntry(
value.eventDate,
value.countTotalPresenceDetected,
),
),
),
),
),
),
],

View File

@ -17,8 +17,8 @@ class DeviceLocationDetailsServiceDecorator implements DeviceLocationService {
'reverse',
queryParameters: {
'format': 'json',
'lat': param.latitude,
'lon': param.longitude,
'lat': 25.1880567,
'lon': 55.266608,
},
);

View File

@ -0,0 +1,68 @@
import 'package:flutter/material.dart';
import 'package:flutter_svg/svg.dart';
import 'package:syncrow_web/common/widgets/app_loading_indicator.dart';
import 'package:syncrow_web/utils/color_manager.dart';
import 'package:syncrow_web/utils/extension/build_context_x.dart';
class AnalyticsChartEmptyStateWidget extends StatelessWidget {
const AnalyticsChartEmptyStateWidget({
required this.iconPath,
this.isLoading = false,
this.isError = false,
this.isInitial = false,
this.errorMessage,
this.noDataMessage = 'No data to display',
this.initialMessage = 'Please select a space to see data',
super.key,
});
final bool isLoading;
final bool isError;
final bool isInitial;
final String? errorMessage;
final String noDataMessage;
final String initialMessage;
final String iconPath;
@override
Widget build(BuildContext context) {
return Expanded(
child: _buildWidgetBasedOnState(context),
);
}
Widget _buildWidgetBasedOnState(BuildContext context) {
final widgetsMap = {
isLoading: const AppLoadingIndicator(),
isInitial: _buildState(context, initialMessage),
isError: _buildState(context, errorMessage ?? 'Something went wrong'),
};
return widgetsMap[true] ?? _buildState(context, noDataMessage);
}
Widget _buildState(BuildContext context, String message) {
return Center(
child: Column(
spacing: 16,
mainAxisAlignment: MainAxisAlignment.center,
children: [
const SizedBox(height: 16),
Expanded(child: SvgPicture.asset(iconPath, fit: BoxFit.contain)),
SelectableText(
message,
style: isError
? context.textTheme.bodyMedium?.copyWith(
color: ColorsManager.red,
fontSize: 16,
fontWeight: FontWeight.w700,
)
: null,
textAlign: TextAlign.center,
),
const SizedBox(height: 16),
],
),
);
}
}

View File

@ -50,6 +50,9 @@ class _DynamicTableState extends State<DynamicTable> {
bool _selectAll = false;
final ScrollController _verticalScrollController = ScrollController();
final ScrollController _horizontalScrollController = ScrollController();
static const double _fixedRowHeight = 60;
static const double _checkboxColumnWidth = 50;
static const double _settingsColumnWidth = 100;
@override
void initState() {
@ -67,7 +70,6 @@ class _DynamicTableState extends State<DynamicTable> {
bool _compareListOfLists(
List<List<dynamic>> oldList, List<List<dynamic>> newList) {
// Check if the old and new lists are the same
if (oldList.length != newList.length) return false;
for (int i = 0; i < oldList.length; i++) {
@ -104,73 +106,130 @@ class _DynamicTableState extends State<DynamicTable> {
context.read<DeviceManagementBloc>().add(UpdateSelection(_selectedRows));
}
double get _totalTableWidth {
final hasSettings = widget.headers.contains('Settings');
final base = (widget.withCheckBox ? _checkboxColumnWidth : 0) +
(hasSettings ? _settingsColumnWidth : 0);
final regularCount = widget.headers.length - (hasSettings ? 1 : 0);
final regularWidth = (widget.size.width - base) / regularCount;
return base + regularCount * regularWidth;
}
@override
Widget build(BuildContext context) {
return Container(
width: widget.size.width,
height: widget.size.height,
decoration: widget.cellDecoration,
child: Scrollbar(
controller: _verticalScrollController,
thumbVisibility: true,
trackVisibility: true,
child: ScrollConfiguration(
behavior: const ScrollBehavior().copyWith(scrollbars: false),
child: Scrollbar(
//fixed the horizontal scrollbar issue
controller: _horizontalScrollController,
thumbVisibility: true,
trackVisibility: true,
notificationPredicate: (notif) => notif.depth == 1,
notificationPredicate: (notif) =>
notif.metrics.axis == Axis.horizontal,
child: SingleChildScrollView(
controller: _verticalScrollController,
child: SingleChildScrollView(
controller: _horizontalScrollController,
scrollDirection: Axis.horizontal,
child: SizedBox(
width: widget.size.width,
child: Column(
children: [
Container(
decoration: widget.headerDecoration ??
const BoxDecoration(
color: ColorsManager.boxColor,
controller: _horizontalScrollController,
scrollDirection: Axis.horizontal,
child: SizedBox(
width: _totalTableWidth,
child: Column(
children: [
Container(
height: _fixedRowHeight,
decoration: widget.headerDecoration ??
const BoxDecoration(color: ColorsManager.boxColor),
child: Row(
children: [
if (widget.withCheckBox)
_buildSelectAllCheckbox(_checkboxColumnWidth),
for (var i = 0; i < widget.headers.length; i++)
_buildTableHeaderCell(
widget.headers[i],
widget.headers[i] == 'Settings'
? _settingsColumnWidth
: (_totalTableWidth -
(widget.withCheckBox
? _checkboxColumnWidth
: 0) -
(widget.headers.contains('Settings')
? _settingsColumnWidth
: 0)) /
(widget.headers.length -
(widget.headers.contains('Settings')
? 1
: 0)),
),
child: Row(
children: [
if (widget.withCheckBox) _buildSelectAllCheckbox(),
...List.generate(widget.headers.length, (index) {
return _buildTableHeaderCell(
widget.headers[index], index);
})
//...widget.headers.map((header) => _buildTableHeaderCell(header)),
],
),
],
),
SizedBox(
width: widget.size.width,
child: widget.isEmpty
? _buildEmptyState()
: Column(
children:
List.generate(widget.data.length, (rowIndex) {
),
Expanded(
child: widget.isEmpty
? _buildEmptyState()
: Scrollbar(
controller: _verticalScrollController,
thumbVisibility: true,
trackVisibility: true,
notificationPredicate: (notif) =>
notif.metrics.axis == Axis.vertical,
child: ListView.builder(
controller: _verticalScrollController,
itemCount: widget.data.length,
itemBuilder: (_, rowIndex) {
final row = widget.data[rowIndex];
return Row(
children: [
if (widget.withCheckBox)
_buildRowCheckbox(
rowIndex, widget.size.height * 0.08),
...row.asMap().entries.map((entry) {
return _buildTableCell(
entry.value.toString(),
widget.size.height * 0.08,
rowIndex: rowIndex,
columnIndex: entry.key,
);
}).toList(),
],
return SizedBox(
height: _fixedRowHeight,
child: Row(
children: [
if (widget.withCheckBox)
_buildRowCheckbox(
rowIndex,
_checkboxColumnWidth,
),
for (var colIndex = 0;
colIndex < row.length;
colIndex++)
widget.headers[colIndex] == 'Settings'
? buildSettingsIcon(
width: _settingsColumnWidth,
onTap: () => widget
.onSettingsPressed
?.call(rowIndex),
)
: _buildTableCell(
row[colIndex].toString(),
width: widget.headers[
colIndex] ==
'Settings'
? _settingsColumnWidth
: (_totalTableWidth -
(widget.withCheckBox
? _checkboxColumnWidth
: 0) -
(widget.headers
.contains(
'Settings')
? _settingsColumnWidth
: 0)) /
(widget.headers.length -
(widget.headers
.contains(
'Settings')
? 1
: 0)),
rowIndex: rowIndex,
columnIndex: colIndex,
),
],
),
);
}),
},
),
),
],
),
),
),
],
),
),
),
@ -210,9 +269,10 @@ class _DynamicTableState extends State<DynamicTable> {
],
),
);
Widget _buildSelectAllCheckbox() {
Widget _buildSelectAllCheckbox(double width) {
return Container(
width: 50,
width: width,
decoration: const BoxDecoration(
border: Border.symmetric(
vertical: BorderSide(color: ColorsManager.boxDivider),
@ -227,11 +287,11 @@ class _DynamicTableState extends State<DynamicTable> {
);
}
Widget _buildRowCheckbox(int index, double size) {
Widget _buildRowCheckbox(int index, double width) {
return Container(
width: 50,
width: width,
padding: const EdgeInsets.all(8.0),
height: size,
height: _fixedRowHeight,
decoration: const BoxDecoration(
border: Border(
bottom: BorderSide(
@ -253,50 +313,47 @@ class _DynamicTableState extends State<DynamicTable> {
);
}
Widget _buildTableHeaderCell(String title, int index) {
return Expanded(
child: Container(
decoration: const BoxDecoration(
border: Border.symmetric(
vertical: BorderSide(color: ColorsManager.boxDivider),
),
Widget _buildTableHeaderCell(String title, double width) {
return Container(
width: width,
decoration: const BoxDecoration(
border: Border.symmetric(
vertical: BorderSide(color: ColorsManager.boxDivider),
),
constraints: const BoxConstraints.expand(height: 40),
alignment: Alignment.centerLeft,
child: Padding(
padding: EdgeInsets.symmetric(
horizontal: index == widget.headers.length - 1 ? 12 : 8.0,
vertical: 4),
child: Text(
title,
style: context.textTheme.titleSmall!.copyWith(
color: ColorsManager.grayColor,
fontSize: 12,
fontWeight: FontWeight.w400,
),
maxLines: 2,
),
constraints: BoxConstraints(minHeight: 40, maxHeight: _fixedRowHeight),
alignment: Alignment.centerLeft,
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 8.0, vertical: 4),
child: Text(
title,
style: context.textTheme.titleSmall!.copyWith(
color: ColorsManager.grayColor,
fontSize: 12,
fontWeight: FontWeight.w400,
),
maxLines: 2,
overflow: TextOverflow.ellipsis,
),
),
);
}
Widget _buildTableCell(String content, double size,
{required int rowIndex, required int columnIndex}) {
Widget _buildTableCell(String content,
{required double width,
required int rowIndex,
required int columnIndex}) {
bool isBatteryLevel = content.endsWith('%');
double? batteryLevel;
if (isBatteryLevel) {
batteryLevel = double.tryParse(content.replaceAll('%', '').trim());
}
bool isSettingsColumn = widget.headers[columnIndex] == 'Settings';
if (isSettingsColumn) {
return buildSettingsIcon(
width: 120,
height: 60,
iconSize: 40,
onTap: () => widget.onSettingsPressed?.call(rowIndex),
);
width: width, onTap: () => widget.onSettingsPressed?.call(rowIndex));
}
Color? statusColor;
@ -320,92 +377,82 @@ class _DynamicTableState extends State<DynamicTable> {
statusColor = Colors.black;
}
return Expanded(
child: Container(
height: size,
padding: const EdgeInsets.all(5.0),
decoration: const BoxDecoration(
border: Border(
bottom: BorderSide(
color: ColorsManager.boxDivider,
width: 1.0,
),
return Container(
width: width,
height: _fixedRowHeight,
padding: const EdgeInsets.symmetric(horizontal: 8.0, vertical: 4),
decoration: const BoxDecoration(
border: Border(
bottom: BorderSide(
color: ColorsManager.boxDivider,
width: 1.0,
),
color: Colors.white,
),
alignment: Alignment.centerLeft,
child: Text(
content,
style: TextStyle(
color: (batteryLevel != null && batteryLevel < 20)
? ColorsManager.red
: (batteryLevel != null && batteryLevel > 20)
? ColorsManager.green
: statusColor,
fontSize: 13,
fontWeight: FontWeight.w400),
maxLines: 2,
color: Colors.white,
),
alignment: Alignment.centerLeft,
child: Text(
content,
style: TextStyle(
color: (batteryLevel != null && batteryLevel < 20)
? ColorsManager.red
: (batteryLevel != null && batteryLevel > 20)
? ColorsManager.green
: statusColor,
fontSize: 13,
fontWeight: FontWeight.w400,
),
maxLines: 2,
overflow: TextOverflow.ellipsis,
),
);
}
Widget buildSettingsIcon(
{double width = 120,
double height = 60,
double iconSize = 40,
VoidCallback? onTap}) {
return Column(
children: [
Container(
padding: const EdgeInsets.only(top: 10, bottom: 15, left: 10),
margin: const EdgeInsets.only(right: 15),
decoration: const BoxDecoration(
color: ColorsManager.whiteColors,
border: Border(
bottom: BorderSide(
color: ColorsManager.boxDivider,
width: 1.0,
),
),
Widget buildSettingsIcon({required double width, VoidCallback? onTap}) {
return Container(
width: width,
height: _fixedRowHeight,
padding: const EdgeInsets.only(left: 15, top: 10, bottom: 10),
decoration: const BoxDecoration(
color: ColorsManager.whiteColors,
border: Border(
bottom: BorderSide(
color: ColorsManager.boxDivider,
width: 1.0,
),
width: width,
child: Padding(
padding: const EdgeInsets.only(
right: 16.0,
left: 17.0,
),
child: Container(
width: 50,
decoration: BoxDecoration(
color: const Color(0xFFF7F8FA),
borderRadius: BorderRadius.circular(height / 2),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.17),
blurRadius: 14,
offset: const Offset(0, 4),
),
],
),
),
child: Align(
alignment: Alignment.centerLeft,
child: Container(
width: 50,
decoration: BoxDecoration(
color: const Color(0xFFF7F8FA),
borderRadius: BorderRadius.circular(20),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.17),
blurRadius: 14,
offset: const Offset(0, 4),
),
child: InkWell(
onTap: onTap,
child: Padding(
padding: const EdgeInsets.all(8.0),
child: Center(
child: SvgPicture.asset(
Assets.settings,
width: 40,
height: 22,
color: ColorsManager.primaryColor,
),
),
],
),
child: InkWell(
onTap: onTap,
child: Padding(
padding: EdgeInsets.all(8.0),
child: Center(
child: SvgPicture.asset(
Assets.settings,
width: 40,
height: 20,
color: ColorsManager.primaryColor,
),
),
),
),
),
],
),
);
}
}

View File

@ -15,7 +15,8 @@ import 'package:syncrow_web/utils/constants/assets.dart';
import 'package:syncrow_web/utils/extension/build_context_x.dart';
import 'package:syncrow_web/utils/helpers/responsice_layout_helper/responsive_layout_helper.dart';
class AcDeviceBatchControlView extends StatelessWidget with HelperResponsiveLayout {
class AcDeviceBatchControlView extends StatelessWidget
with HelperResponsiveLayout {
const AcDeviceBatchControlView({super.key, required this.devicesIds});
final List<String> devicesIds;
@ -51,7 +52,7 @@ class AcDeviceBatchControlView extends StatelessWidget with HelperResponsiveLayo
deviceId: devicesIds.first,
code: 'switch',
value: state.status.acSwitch,
label: 'ThermoState',
label: 'Thermostat',
icon: Assets.ac,
onChange: (value) {
context.read<AcBloc>().add(AcBatchControlEvent(
@ -100,8 +101,8 @@ class AcDeviceBatchControlView extends StatelessWidget with HelperResponsiveLayo
),
Text(
'h',
style:
context.textTheme.bodySmall!.copyWith(color: ColorsManager.blackColor),
style: context.textTheme.bodySmall!
.copyWith(color: ColorsManager.blackColor),
),
Text(
'30',
@ -148,7 +149,8 @@ class AcDeviceBatchControlView extends StatelessWidget with HelperResponsiveLayo
callFactoryReset: () {
context.read<AcBloc>().add(AcFactoryResetEvent(
deviceId: state.status.uuid,
factoryResetModel: FactoryResetModel(devicesUuid: devicesIds),
factoryResetModel:
FactoryResetModel(devicesUuid: devicesIds),
));
},
),

View File

@ -57,6 +57,9 @@ class Status {
};
}
factory Status.fromJson(String source) => Status.fromMap(json.decode(source));
String toJson() => json.encode(toMap());
Status copyWith({
String? code,
dynamic value,
@ -66,8 +69,4 @@ class Status {
value: value ?? this.value,
);
}
factory Status.fromJson(String source) => Status.fromMap(json.decode(source));
String toJson() => json.encode(toMap());
}

View File

@ -68,6 +68,7 @@ class DeviceManagementBody extends StatelessWidget with HelperResponsiveLayout {
children: [
Expanded(child: SpaceTreeView(
onSelect: () {
context.read<DeviceManagementBloc>().add(ResetFilters());
context.read<DeviceManagementBloc>().add(FetchDevices(context));
},
)),

View File

@ -62,9 +62,10 @@ class CurtainModuleItems extends StatelessWidget with HelperResponsiveLayout {
BlocProvider.of<CurtainModuleBloc>(context),
child: BuildScheduleView(
deviceUuid: deviceId,
category: 'CUR_2',
category: 'Timer',
code: 'control',
countdownCode: 'Timer',
deviceType: 'CUR_2',
),
));
},

View File

@ -4,6 +4,7 @@ import 'package:syncrow_web/pages/device_managment/curtain_module/bloc/curtain_m
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';
import 'package:syncrow_web/utils/color_manager.dart';
class AccurteCalibratingDialog extends StatelessWidget {
final String deviceId;
@ -17,6 +18,7 @@ class AccurteCalibratingDialog extends StatelessWidget {
@override
Widget build(_) {
return AlertDialog(
backgroundColor: ColorsManager.whiteColors,
contentPadding: EdgeInsets.zero,
content: AccurateDialogWidget(
title: 'Calibrating',

View File

@ -2,6 +2,7 @@ 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';
import 'package:syncrow_web/utils/color_manager.dart';
class AccurateCalibrationDialog extends StatelessWidget {
final String deviceId;
@ -15,6 +16,7 @@ class AccurateCalibrationDialog extends StatelessWidget {
@override
Widget build(_) {
return AlertDialog(
backgroundColor: ColorsManager.whiteColors,
contentPadding: EdgeInsets.zero,
content: AccurateDialogWidget(
title: 'Accurate Calibration',

View File

@ -22,6 +22,7 @@ class AccurateDialogWidget extends StatelessWidget {
child: Column(
children: [
Expanded(
flex: 3,
child: Column(
children: [
Padding(
@ -43,17 +44,22 @@ class AccurateDialogWidget extends StatelessWidget {
),
),
Expanded(
flex: 5,
child: body,
),
Expanded(
flex: 2,
child: Column(
mainAxisAlignment: MainAxisAlignment.end,
children: [
const Divider(),
const Expanded(child: Divider()),
Row(
children: [
Expanded(
child: InkWell(
borderRadius: const BorderRadius.only(
bottomLeft: Radius.circular(26),
),
onTap: leftOnTap,
child: Container(
height: 40,
@ -64,6 +70,9 @@ class AccurateDialogWidget extends StatelessWidget {
color: ColorsManager.grayBorder,
),
),
borderRadius: BorderRadius.only(
bottomLeft: Radius.circular(26),
),
),
child: const Text(
'Cancel',
@ -74,6 +83,9 @@ class AccurateDialogWidget extends StatelessWidget {
),
Expanded(
child: InkWell(
borderRadius: const BorderRadius.only(
bottomRight: Radius.circular(26),
),
onTap: rightOnTap,
child: Container(
height: 40,
@ -84,6 +96,9 @@ class AccurateDialogWidget extends StatelessWidget {
color: ColorsManager.grayBorder,
),
),
borderRadius: BorderRadius.only(
bottomRight: Radius.circular(26),
),
),
child: const Text(
'Next',

View File

@ -17,6 +17,7 @@ class CalibrateCompletedDialog extends StatelessWidget {
@override
Widget build(_) {
return AlertDialog(
backgroundColor: ColorsManager.whiteColors,
contentPadding: EdgeInsets.zero,
content: SizedBox(
height: 250,

View File

@ -28,7 +28,7 @@ class NormalTextBodyForDialog extends StatelessWidget {
title,
style: const TextStyle(
color: ColorsManager.grayColor,
fontSize: 17,
fontSize: 15,
),
),
),
@ -42,7 +42,7 @@ class NormalTextBodyForDialog extends StatelessWidget {
const Text('1. ',
style: TextStyle(
color: ColorsManager.grayColor,
fontSize: 17,
fontSize: 15,
)),
SizedBox(
width: 450,
@ -50,7 +50,7 @@ class NormalTextBodyForDialog extends StatelessWidget {
step1,
style: const TextStyle(
color: ColorsManager.grayColor,
fontSize: 17,
fontSize: 15,
),
),
),
@ -67,13 +67,13 @@ class NormalTextBodyForDialog extends StatelessWidget {
const Text('2. ',
style: TextStyle(
color: ColorsManager.grayColor,
fontSize: 17,
fontSize: 15,
)),
Text(
step2,
style: const TextStyle(
color: ColorsManager.grayColor,
fontSize: 17,
fontSize: 15,
),
),
],

View File

@ -63,6 +63,7 @@ class _QuickCalibratingDialogState extends State<QuickCalibratingDialog> {
@override
Widget build(_) {
return AlertDialog(
backgroundColor: ColorsManager.whiteColors,
contentPadding: EdgeInsets.zero,
content: AccurateDialogWidget(
title: 'Calibrating',
@ -71,7 +72,7 @@ class _QuickCalibratingDialogState extends State<QuickCalibratingDialog> {
children: [
const Expanded(
child: Align(
alignment: Alignment.center,
alignment: Alignment.topCenter,
child: Padding(
padding: EdgeInsets.only(right: 75),
child: Text(
@ -85,17 +86,21 @@ class _QuickCalibratingDialogState extends State<QuickCalibratingDialog> {
child: Align(
alignment: Alignment.center,
child: Container(
width: 110,
width: 130,
padding: const EdgeInsets.all(5),
decoration: BoxDecoration(
color: ColorsManager.whiteColors,
color: ColorsManager.neutralGray.withValues(
alpha: 0.5,
),
borderRadius: BorderRadius.circular(12),
),
child: Row(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Expanded(
child: NumberInputField(controller: _controller),
child: Padding(
padding: const EdgeInsetsGeometry.only(left: 5),
child: NumberInputField(controller: _controller)),
),
Expanded(
child: Text(
@ -127,7 +132,7 @@ class _QuickCalibratingDialogState extends State<QuickCalibratingDialog> {
),
const Expanded(
child: Align(
alignment: Alignment.center,
alignment: Alignment.bottomCenter,
child: Text(
'2.click Next to Complete the calibration',
style: TextStyle(color: ColorsManager.lightGrayColor),

View File

@ -2,6 +2,7 @@ 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';
import 'package:syncrow_web/utils/color_manager.dart';
class QuickCalibrationDialog extends StatelessWidget {
final int timControl;
@ -17,6 +18,7 @@ class QuickCalibrationDialog extends StatelessWidget {
@override
Widget build(_) {
return AlertDialog(
backgroundColor: ColorsManager.whiteColors,
contentPadding: EdgeInsets.zero,
content: AccurateDialogWidget(
title: 'Quick Calibration',

View File

@ -40,7 +40,7 @@ class OneGangGlassSwitchBloc
emit(OneGangGlassSwitchLoading());
try {
final status = await DevicesManagementApi().getDeviceStatus(event.deviceId);
_listenToChanges(event.deviceId, emit);
_listenToChanges(event.deviceId);
deviceStatus = OneGangGlassStatusModel.fromJson(event.deviceId, status.status);
emit(OneGangGlassSwitchStatusLoaded(deviceStatus));
} catch (e) {
@ -48,42 +48,28 @@ class OneGangGlassSwitchBloc
}
}
void _listenToChanges(
String deviceId,
Emitter<OneGangGlassSwitchState> emit,
) {
StreamSubscription<DatabaseEvent>? _deviceStatusSubscription;
void _listenToChanges(String deviceId) {
try {
final ref = FirebaseDatabase.instance.ref('device-status/$deviceId');
final stream = ref.onValue;
_deviceStatusSubscription = ref.onValue.listen((DatabaseEvent event) async {
if (event.snapshot.value == null) return;
stream.listen((DatabaseEvent event) {
final data = event.snapshot.value as Map<dynamic, dynamic>?;
if (data == null) return;
final usersMap = event.snapshot.value! as Map<dynamic, dynamic>;
final statusList = <Status>[];
if (data['status'] != null) {
for (var element in data['status']) {
statusList.add(
Status(
code: element['code'].toString(),
value: element['value'].toString(),
),
);
}
}
if (statusList.isNotEmpty) {
final newStatus = OneGangGlassStatusModel.fromJson(deviceId, statusList);
if (newStatus != deviceStatus) {
deviceStatus = newStatus;
if (!isClosed) {
add(StatusUpdated(deviceStatus));
}
}
}
usersMap['status'].forEach((element) {
statusList.add(Status(code: element['code'], value: element['value']));
});
deviceStatus =
OneGangGlassStatusModel.fromJson(usersMap['productUuid'], statusList);
add(StatusUpdated(deviceStatus));
});
} catch (e) {
emit(OneGangGlassSwitchError('Failed to listen to changes: $e'));
}
} catch (_) {}
}
void _onStatusUpdated(
@ -174,4 +160,10 @@ class OneGangGlassSwitchBloc
deviceStatus = deviceStatus.copyWith(switch1: value);
}
}
@override
Future<void> close() {
_deviceStatusSubscription?.cancel();
return super.close();
}
}

View File

@ -90,6 +90,8 @@ class OneGangGlassSwitchControlView extends StatelessWidget
child: BuildScheduleView(
category: 'switch_1',
deviceUuid: deviceId,
countdownCode: 'countdown_1',
deviceType: '1GT',
),
));
},

View File

@ -80,6 +80,8 @@ class WallLightDeviceControl extends StatelessWidget
child: BuildScheduleView(
category: 'switch_1',
deviceUuid: deviceId,
countdownCode: 'countdown_1',
deviceType: '1G',
),
));
},

View File

@ -277,6 +277,32 @@ class SmartPowerDeviceControl extends StatelessWidget
totalConsumption: 10000,
date: blocProvider.formattedDate,
),
EnergyConsumptionPage(
formattedDate:
'${blocProvider.dateTime!.day}/${blocProvider.dateTime!.month}/${blocProvider.dateTime!.year} ${blocProvider.endChartDate}',
onTap: () {
blocProvider.add(SelectDateEvent(context: context));
},
widget: blocProvider.dateSwitcher(),
chartData: blocProvider.energyDataList.isNotEmpty
? blocProvider.energyDataList
: [
EnergyData('12:00 AM', 4.0),
EnergyData('01:00 AM', 6.5),
EnergyData('02:00 AM', 3.8),
EnergyData('03:00 AM', 3.2),
EnergyData('04:00 AM', 6.0),
EnergyData('05:00 AM', 3.4),
EnergyData('06:00 AM', 5.2),
EnergyData('07:00 AM', 3.5),
EnergyData('08:00 AM', 6.8),
EnergyData('09:00 AM', 5.6),
EnergyData('10:00 AM', 3.9),
EnergyData('11:00 AM', 4.0),
],
totalConsumption: 10000,
date: blocProvider.formattedDate,
),
],
),
),

View File

@ -47,7 +47,7 @@ class ScheduleBloc extends Bloc<ScheduleEvent, ScheduleState> {
final success = await RemoteControlDeviceService().controlDevice(
deviceUuid: deviceId,
status: Status(
code: 'countdown_1',
code: event.countdownCode,
value: 0,
),
);
@ -80,15 +80,18 @@ class ScheduleBloc extends Bloc<ScheduleEvent, ScheduleState> {
) {
if (state is ScheduleLoaded) {
final currentState = state as ScheduleLoaded;
emit(currentState.copyWith(
countdownSeconds: currentState.countdownSeconds,
selectedTime: currentState.selectedTime,
deviceId: deviceId,
scheduleMode: event.scheduleMode,
countdownRemaining: Duration.zero,
countdownHours: 0,
countdownMinutes: 0,
inchingHours: 0,
inchingMinutes: 0,
isCountdownActive: false,
countdownHours: currentState.countdownHours,
countdownMinutes: currentState.countdownMinutes,
inchingHours: currentState.inchingHours,
inchingMinutes: currentState.inchingMinutes,
isInchingActive: false,
isCountdownActive: currentState.countdownRemaining > Duration.zero,
));
}
}
@ -221,7 +224,6 @@ class ScheduleBloc extends Bloc<ScheduleEvent, ScheduleState> {
deviceId,
event.category,
);
if (state is ScheduleLoaded) {
final currentState = state as ScheduleLoaded;
emit(currentState.copyWith(
@ -230,7 +232,6 @@ class ScheduleBloc extends Bloc<ScheduleEvent, ScheduleState> {
selectedDays: List.filled(7, false),
functionOn: false,
isEditing: false,
countdownRemaining: Duration.zero,
));
} else {
emit(ScheduleLoaded(
@ -285,9 +286,8 @@ class ScheduleBloc extends Bloc<ScheduleEvent, ScheduleState> {
) async {
try {
if (state is ScheduleLoaded) {
final dateTime = DateTime.parse(event.time);
Status status = Status(code: '', value: '');
if (event.category == 'CUR_2') {
if (event.deviceType == 'CUR_2') {
status = status.copyWith(
code: 'control',
value: event.functionOn == true ? 'open' : 'close');
@ -295,6 +295,8 @@ class ScheduleBloc extends Bloc<ScheduleEvent, ScheduleState> {
status =
status.copyWith(code: event.category, value: event.functionOn);
}
final dateTime = DateTime.parse(event.time);
final updatedSchedule = ScheduleEntry(
scheduleId: event.scheduleId,
category: event.category,
@ -405,7 +407,7 @@ class ScheduleBloc extends Bloc<ScheduleEvent, ScheduleState> {
final totalSeconds =
Duration(hours: event.hours, minutes: event.minutes).inSeconds;
final code = event.mode == ScheduleModes.countdown
? 'countdown_1'
? event.countDownCode
: 'switch_inching';
final currentState = state as ScheduleLoaded;
final duration = Duration(seconds: totalSeconds);
@ -432,7 +434,7 @@ class ScheduleBloc extends Bloc<ScheduleEvent, ScheduleState> {
);
if (success) {
if (code == 'countdown_1') {
if (code == event.countDownCode) {
final countdownDuration = Duration(seconds: totalSeconds);
emit(
@ -446,7 +448,7 @@ class ScheduleBloc extends Bloc<ScheduleEvent, ScheduleState> {
);
if (countdownDuration.inSeconds > 0) {
_startCountdownTimer(emit, countdownDuration);
_startCountdownTimer(emit, countdownDuration, event.countDownCode);
} else {
_countdownTimer?.cancel();
emit(
@ -476,9 +478,7 @@ class ScheduleBloc extends Bloc<ScheduleEvent, ScheduleState> {
}
void _startCountdownTimer(
Emitter<ScheduleState> emit,
Duration duration,
) {
Emitter<ScheduleState> emit, Duration duration, String countdownCode) {
_countdownTimer?.cancel();
_countdownTimer = Timer.periodic(const Duration(seconds: 1), (timer) {
if (_currentCountdown != null && _currentCountdown! > Duration.zero) {
@ -488,6 +488,7 @@ class ScheduleBloc extends Bloc<ScheduleEvent, ScheduleState> {
} else {
timer.cancel();
add(StopScheduleEvent(
countdownCode: countdownCode,
mode: _currentCountdown == null
? ScheduleModes.countdown
: ScheduleModes.inching,
@ -524,70 +525,75 @@ class ScheduleBloc extends Bloc<ScheduleEvent, ScheduleState> {
try {
final status =
await DevicesManagementApi().getDeviceStatus(event.deviceId);
print(status.status);
int totalSeconds = 0;
final countdownItem = status.status.firstWhere(
(item) => item.code == event.countdownCode,
orElse: () => Status(code: '', value: 0),
);
totalSeconds = (countdownItem.value as int?) ?? 0;
final countdownHours = totalSeconds ~/ 3600;
final countdownMinutes = (totalSeconds % 3600) ~/ 60;
final countdownSeconds = totalSeconds % 60;
final deviceStatus =
WaterHeaterStatusModel.fromJson(event.deviceId, status.status);
final isCountdownActive = totalSeconds > 0;
final isInchingActive = !isCountdownActive &&
(deviceStatus.inchingHours > 0 || deviceStatus.inchingMinutes > 0);
final scheduleMode =
deviceStatus.countdownHours > 0 || deviceStatus.countdownMinutes > 0
? ScheduleModes.countdown
: deviceStatus.inchingHours > 0 || deviceStatus.inchingMinutes > 0
? ScheduleModes.inching
: ScheduleModes.schedule;
final isCountdown = scheduleMode == ScheduleModes.countdown;
final isInching = scheduleMode == ScheduleModes.inching;
final newState = state is ScheduleLoaded
? (state as ScheduleLoaded).copyWith(
scheduleMode: ScheduleModes.schedule,
countdownHours: countdownHours,
countdownMinutes: countdownMinutes,
countdownSeconds: countdownSeconds,
inchingHours: deviceStatus.inchingHours,
inchingMinutes: deviceStatus.inchingMinutes,
isCountdownActive: isCountdownActive,
isInchingActive: isInchingActive,
countdownRemaining: isCountdownActive
? Duration(seconds: totalSeconds)
: Duration.zero,
)
: ScheduleLoaded(
scheduleMode: ScheduleModes.schedule,
schedules: const [],
selectedTime: null,
selectedDays: List.filled(7, false),
functionOn: false,
isEditing: false,
deviceId: event.deviceId,
countdownHours: countdownHours,
countdownMinutes: countdownMinutes,
countdownSeconds: countdownSeconds,
inchingHours: deviceStatus.inchingHours,
inchingMinutes: deviceStatus.inchingMinutes,
isCountdownActive: isCountdownActive,
isInchingActive: isInchingActive,
countdownRemaining: isCountdownActive
? Duration(seconds: totalSeconds)
: Duration.zero,
);
emit(newState);
Duration? countdownRemaining;
var isCountdownActive = false;
var isInchingActive = false;
if (isCountdownActive) {
_countdownTimer?.cancel();
_currentCountdown = Duration(seconds: totalSeconds);
countdownRemaining = _currentCountdown!;
if (isCountdown) {
countdownRemaining = Duration(
hours: deviceStatus.countdownHours,
minutes: deviceStatus.countdownMinutes,
);
isCountdownActive = countdownRemaining > Duration.zero;
} else if (isInching) {
isInchingActive = Duration(
hours: deviceStatus.inchingHours,
minutes: deviceStatus.inchingMinutes,
) >
Duration.zero;
}
if (state is ScheduleLoaded) {
final currentState = state as ScheduleLoaded;
emit(currentState.copyWith(
scheduleMode: scheduleMode,
countdownHours: deviceStatus.countdownHours,
countdownMinutes: deviceStatus.countdownMinutes,
inchingHours: deviceStatus.inchingHours,
inchingMinutes: deviceStatus.inchingMinutes,
isCountdownActive: isCountdownActive,
isInchingActive: isInchingActive,
countdownRemaining: countdownRemaining ?? Duration.zero,
));
if (totalSeconds > 0) {
_startCountdownTimer(
emit, Duration(seconds: totalSeconds), event.countdownCode);
} else {
add(StopScheduleEvent(
countdownCode: event.countdownCode,
mode: ScheduleModes.countdown,
deviceId: event.deviceId,
));
}
} else {
emit(ScheduleLoaded(
schedules: const [],
selectedTime: null,
selectedDays: List.filled(7, false),
functionOn: false,
isEditing: false,
deviceId: deviceId,
scheduleMode: scheduleMode,
countdownHours: deviceStatus.countdownHours,
countdownMinutes: deviceStatus.countdownMinutes,
inchingHours: deviceStatus.inchingHours,
inchingMinutes: deviceStatus.inchingMinutes,
isCountdownActive: isCountdownActive,
isInchingActive: isInchingActive,
countdownRemaining: countdownRemaining ?? Duration.zero,
));
_countdownTimer?.cancel();
}
// if (isCountdownActive && countdownRemaining != null) {
// _startCountdownTimer(emit, countdownRemaining);
// }
} catch (e) {
emit(ScheduleError('Failed to fetch device status: $e'));
}

View File

@ -91,6 +91,7 @@ class ScheduleEditEvent extends ScheduleEvent {
final String time;
final List<String> selectedDays;
final bool functionOn;
final String deviceType;
const ScheduleEditEvent({
required this.scheduleId,
@ -98,6 +99,7 @@ class ScheduleEditEvent extends ScheduleEvent {
required this.time,
required this.selectedDays,
required this.functionOn,
required this.deviceType,
});
@override
@ -107,6 +109,7 @@ class ScheduleEditEvent extends ScheduleEvent {
time,
selectedDays,
functionOn,
deviceType,
];
}
@ -138,11 +141,13 @@ class ScheduleUpdateEntryEvent extends ScheduleEvent {
class UpdateScheduleModeEvent extends ScheduleEvent {
final ScheduleModes scheduleMode;
final String countdownCode;
const UpdateScheduleModeEvent({required this.scheduleMode});
const UpdateScheduleModeEvent(
{required this.scheduleMode, required this.countdownCode});
@override
List<Object> get props => [scheduleMode];
List<Object> get props => [scheduleMode, countdownCode!];
}
class UpdateCountdownTimeEvent extends ScheduleEvent {
@ -177,28 +182,32 @@ class StartScheduleEvent extends ScheduleEvent {
final ScheduleModes mode;
final int hours;
final int minutes;
final String countDownCode;
const StartScheduleEvent({
required this.mode,
required this.hours,
required this.minutes,
required this.countDownCode,
});
@override
List<Object?> get props => [mode, hours, minutes];
List<Object?> get props => [mode, hours, minutes, countDownCode];
}
class StopScheduleEvent extends ScheduleEvent {
final ScheduleModes mode;
final String deviceId;
final String countdownCode;
const StopScheduleEvent({
required this.mode,
required this.deviceId,
required this.countdownCode,
});
@override
List<Object?> get props => [mode, deviceId];
List<Object?> get props => [mode, deviceId, countdownCode];
}
class ScheduleDecrementCountdownEvent extends ScheduleEvent {
@ -210,11 +219,13 @@ class ScheduleDecrementCountdownEvent extends ScheduleEvent {
class ScheduleFetchStatusEvent extends ScheduleEvent {
final String deviceId;
final String countdownCode;
const ScheduleFetchStatusEvent(this.deviceId);
const ScheduleFetchStatusEvent(
{required this.deviceId, required this.countdownCode});
@override
List<Object> get props => [deviceId];
List<Object> get props => [deviceId, countdownCode];
}
class DeleteScheduleEvent extends ScheduleEvent {

View File

@ -29,7 +29,7 @@ class ScheduleLoaded extends ScheduleState {
final int inchingSeconds;
final bool isInchingActive;
final ScheduleModes scheduleMode;
final Duration? countdownRemaining;
final Duration countdownRemaining;
final int? countdownSeconds;
const ScheduleLoaded({
@ -48,7 +48,7 @@ class ScheduleLoaded extends ScheduleState {
this.inchingMinutes = 0,
this.isInchingActive = false,
this.scheduleMode = ScheduleModes.countdown,
this.countdownRemaining,
this.countdownRemaining = Duration.zero,
});
ScheduleLoaded copyWith({

View File

@ -11,6 +11,7 @@ class CountdownModeButtons extends StatelessWidget {
final String deviceId;
final int hours;
final int minutes;
final String countDownCode;
const CountdownModeButtons({
super.key,
@ -18,6 +19,7 @@ class CountdownModeButtons extends StatelessWidget {
required this.deviceId,
required this.hours,
required this.minutes,
required this.countDownCode,
});
@override
@ -43,6 +45,7 @@ class CountdownModeButtons extends StatelessWidget {
StopScheduleEvent(
mode: ScheduleModes.countdown,
deviceId: deviceId,
countdownCode: countDownCode,
),
);
},
@ -54,10 +57,10 @@ class CountdownModeButtons extends StatelessWidget {
onPressed: () {
context.read<ScheduleBloc>().add(
StartScheduleEvent(
mode: ScheduleModes.countdown,
hours: hours,
minutes: minutes,
),
mode: ScheduleModes.countdown,
hours: hours,
minutes: minutes,
countDownCode: countDownCode),
);
},
backgroundColor: ColorsManager.primaryColor,

View File

@ -75,23 +75,33 @@ class _CountdownInchingViewState extends State<CountdownInchingView> {
final isCountDown = state.scheduleMode == ScheduleModes.countdown;
final isActive =
isCountDown ? state.isCountdownActive : state.isInchingActive;
final displayHours = isActive && state.countdownRemaining != null
? state.countdownRemaining!.inHours
: (isCountDown ? state.countdownHours : state.inchingHours);
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!);
final displayHours =
isActive && state.countdownRemaining != Duration.zero
? state.countdownRemaining.inHours
: (isCountDown ? state.countdownHours : state.inchingHours);
if (displayHours == 0 && displayMinutes == 0 && displaySeconds == 0) {
final displayMinutes =
isActive && state.countdownRemaining != Duration.zero
? state.countdownRemaining.inMinutes.remainder(60)
: (isCountDown ? state.countdownMinutes : state.inchingMinutes);
final displaySeconds =
isActive && state.countdownRemaining != Duration.zero
? state.countdownRemaining.inSeconds.remainder(60)
: (isCountDown ? (state.countdownSeconds ?? 0) : 0);
_updateControllers(displayHours, displayMinutes, displaySeconds);
if (isActive &&
displayHours == 0 &&
displayMinutes == 0 &&
displaySeconds == 0) {
context.read<ScheduleBloc>().add(
StopScheduleEvent(
mode: ScheduleModes.countdown,
deviceId: widget.deviceId,
countdownCode: '',
),
);
}

View File

@ -43,7 +43,9 @@ class InchingModeButtons extends StatelessWidget {
onPressed: () {
context.read<ScheduleBloc>().add(
StopScheduleEvent(
deviceId: deviceId, mode: ScheduleModes.inching),
deviceId: deviceId,
mode: ScheduleModes.inching,
countdownCode: ''),
);
},
backgroundColor: Colors.red,

View File

@ -18,11 +18,15 @@ class BuildScheduleView extends StatelessWidget {
super.key,
required this.deviceUuid,
required this.category,
required this.countdownCode,
this.code,
required this.deviceType,
});
final String deviceUuid;
final String category;
final String? code;
final String? countdownCode;
final String deviceType;
@override
Widget build(BuildContext context) {
@ -31,7 +35,8 @@ class BuildScheduleView extends StatelessWidget {
deviceId: deviceUuid,
)
..add(ScheduleGetEvent(category: category))
..add(ScheduleFetchStatusEvent(deviceUuid)),
..add(ScheduleFetchStatusEvent(
deviceId: deviceUuid, countdownCode: countdownCode ?? '')),
child: Dialog(
backgroundColor: Colors.white,
insetPadding: const EdgeInsets.all(20),
@ -52,31 +57,32 @@ class BuildScheduleView extends StatelessWidget {
children: [
const ScheduleHeader(),
const SizedBox(height: 20),
if (category == 'CUR_2')
if (deviceType == 'CUR_2')
const SizedBox()
else
ScheduleModeSelector(
countdownCode: countdownCode ?? '',
currentMode: state.scheduleMode,
),
const SizedBox(height: 20),
if (state.scheduleMode == ScheduleModes.schedule)
ScheduleManagementUI(
deviceType: deviceType,
category: category,
deviceUuid: deviceUuid,
onAddSchedule: () async {
final entry = await ScheduleDialogHelper
.showAddScheduleDialog(
context,
schedule: ScheduleEntry(
category: category,
time: '',
function: Status(
code: code.toString(), value: null),
days: [],
),
isEdit: false,
code: code,
);
.showAddScheduleDialog(context,
schedule: ScheduleEntry(
category: category,
time: '',
function: Status(
code: code.toString(), value: null),
days: [],
),
isEdit: false,
code: code,
deviceType: deviceType);
if (entry != null) {
context.read<ScheduleBloc>().add(
ScheduleAddEvent(
@ -90,14 +96,16 @@ class BuildScheduleView extends StatelessWidget {
}
},
),
if (state.scheduleMode == ScheduleModes.countdown ||
state.scheduleMode == ScheduleModes.inching)
CountdownInchingView(
deviceId: deviceUuid,
),
if (deviceType != 'CUR_2')
if (state.scheduleMode == ScheduleModes.countdown ||
state.scheduleMode == ScheduleModes.inching)
CountdownInchingView(
deviceId: deviceUuid,
),
const SizedBox(height: 20),
if (state.scheduleMode == ScheduleModes.countdown)
CountdownModeButtons(
countDownCode: countdownCode ?? '',
isActive: state.isCountdownActive,
deviceId: deviceUuid,
hours: state.countdownHours,

View File

@ -5,14 +5,16 @@ import 'package:syncrow_web/utils/color_manager.dart';
import 'package:syncrow_web/utils/extension/build_context_x.dart';
class ScheduleManagementUI extends StatelessWidget {
final String deviceUuid;
final String deviceUuid;
final VoidCallback onAddSchedule;
final String category;
final String deviceType;
const ScheduleManagementUI({
super.key,
required this.deviceUuid,
required this.onAddSchedule,
required this.deviceType,
this.category = 'switch_1',
});
@ -44,7 +46,11 @@ class ScheduleManagementUI extends StatelessWidget {
),
),
const SizedBox(height: 20),
ScheduleTableWidget(deviceUuid: deviceUuid, category: category),
ScheduleTableWidget(
deviceUuid: deviceUuid,
category: category,
deviceType: deviceType,
),
],
);
}

View File

@ -7,10 +7,12 @@ import 'package:syncrow_web/utils/extension/build_context_x.dart';
class ScheduleModeSelector extends StatelessWidget {
final ScheduleModes currentMode;
final String countdownCode;
const ScheduleModeSelector({
super.key,
required this.currentMode,
required this.countdownCode,
});
@override
@ -71,7 +73,8 @@ class ScheduleModeSelector extends StatelessWidget {
onChanged: (ScheduleModes? value) {
if (value != null) {
context.read<ScheduleBloc>().add(
UpdateScheduleModeEvent(scheduleMode: value),
UpdateScheduleModeEvent(
scheduleMode: value, countdownCode: countdownCode),
);
if (value == ScheduleModes.schedule) {
context.read<ScheduleBloc>().add(

View File

@ -12,11 +12,13 @@ import 'package:syncrow_web/utils/format_date_time.dart';
class ScheduleTableWidget extends StatelessWidget {
final String deviceUuid;
final String category;
final String deviceType;
const ScheduleTableWidget({
super.key,
required this.deviceUuid,
this.category = 'switch_1',
required this.deviceType,
});
@override
@ -25,13 +27,14 @@ class ScheduleTableWidget extends StatelessWidget {
create: (_) => ScheduleBloc(
deviceId: deviceUuid,
)..add(ScheduleGetEvent(category: category)),
child: _ScheduleTableView(),
child: _ScheduleTableView(deviceType),
);
}
}
class _ScheduleTableView extends StatelessWidget {
const _ScheduleTableView();
final String deviceType;
const _ScheduleTableView(this.deviceType);
@override
Widget build(BuildContext context) {
@ -81,7 +84,7 @@ class _ScheduleTableView extends StatelessWidget {
bottomLeft: Radius.circular(20),
bottomRight: Radius.circular(20)),
),
child: _buildTableBody(state.schedules, context));
child: _buildTableBody(state.schedules, context, deviceType));
}
if (state is ScheduleError) {
return Center(child: Text(state.error));
@ -123,7 +126,8 @@ class _ScheduleTableView extends StatelessWidget {
);
}
Widget _buildTableBody(List<ScheduleModel> schedules, BuildContext context) {
Widget _buildTableBody(
List<ScheduleModel> schedules, BuildContext context, String deviceType) {
return SizedBox(
height: 200,
child: SingleChildScrollView(
@ -132,7 +136,8 @@ class _ScheduleTableView extends StatelessWidget {
defaultVerticalAlignment: TableCellVerticalAlignment.middle,
children: [
for (int i = 0; i < schedules.length; i++)
_buildScheduleRow(schedules[i], i, context),
_buildScheduleRow(schedules[i], i, context,
deviceType: deviceType),
],
),
),
@ -155,25 +160,19 @@ class _ScheduleTableView extends StatelessWidget {
}
TableRow _buildScheduleRow(
ScheduleModel schedule, int index, BuildContext context) {
ScheduleModel schedule, int index, BuildContext context,
{required String deviceType}) {
return TableRow(
children: [
Center(
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: temp,
// schedule.function.value,
functionOn: schedule.function.value,
enable: !schedule.enable,
),
);
@ -195,8 +194,9 @@ class _ScheduleTableView extends StatelessWidget {
child: Text(_getSelectedDays(
ScheduleModel.parseSelectedDays(schedule.days)))),
Center(child: Text(formatIsoStringToTime(schedule.time, context))),
if (schedule.category == 'CUR_2')
Center(child: Text(schedule.function.value))
if (deviceType == 'CUR_2')
Center(
child: Text(schedule.function.value == true ? 'open' : 'close'))
else
Center(child: Text(schedule.function.value ? 'On' : 'Off')),
Center(
@ -206,14 +206,14 @@ class _ScheduleTableView extends StatelessWidget {
TextButton(
style: TextButton.styleFrom(padding: EdgeInsets.zero),
onPressed: () {
ScheduleDialogHelper.showAddScheduleDialog(
context,
schedule: ScheduleEntry.fromScheduleModel(schedule),
isEdit: true,
).then((updatedSchedule) {
ScheduleDialogHelper.showAddScheduleDialog(context,
schedule: ScheduleEntry.fromScheduleModel(schedule),
isEdit: true,
deviceType: deviceType)
.then((updatedSchedule) {
if (updatedSchedule != null) {
bool temp;
if (schedule.category == 'CUR_2') {
if (deviceType == 'CUR_2') {
updatedSchedule.function.value == 'open'
? temp = true
: temp = false;
@ -222,6 +222,7 @@ class _ScheduleTableView extends StatelessWidget {
}
context.read<ScheduleBloc>().add(
ScheduleEditEvent(
deviceType: deviceType,
scheduleId: schedule.scheduleId,
category: schedule.category,
time: updatedSchedule.time,

View File

@ -41,7 +41,7 @@ class ThreeGangGlassSwitchBloc
emit(ThreeGangGlassSwitchLoading());
try {
final status = await DevicesManagementApi().getDeviceStatus(event.deviceId);
_listenToChanges(event.deviceId, emit);
_listenToChanges(event.deviceId);
deviceStatus =
ThreeGangGlassStatusModel.fromJson(event.deviceId, status.status);
emit(ThreeGangGlassSwitchStatusLoaded(deviceStatus));
@ -50,42 +50,28 @@ class ThreeGangGlassSwitchBloc
}
}
void _listenToChanges(
String deviceId,
Emitter<ThreeGangGlassSwitchState> emit,
) {
StreamSubscription<DatabaseEvent>? _deviceStatusSubscription;
void _listenToChanges(String deviceId) {
try {
final ref = FirebaseDatabase.instance.ref('device-status/$deviceId');
final stream = ref.onValue;
_deviceStatusSubscription = ref.onValue.listen((DatabaseEvent event) async {
if (event.snapshot.value == null) return;
stream.listen((DatabaseEvent event) {
final data = event.snapshot.value as Map<dynamic, dynamic>?;
if (data == null) return;
final usersMap = event.snapshot.value! as Map<dynamic, dynamic>;
final statusList = <Status>[];
if (data['status'] != null) {
for (var element in data['status']) {
statusList.add(
Status(
code: element['code'].toString(),
value: element['value'].toString(),
),
);
}
}
if (statusList.isNotEmpty) {
final newStatus = ThreeGangGlassStatusModel.fromJson(deviceId, statusList);
if (newStatus != deviceStatus) {
deviceStatus = newStatus;
if (!isClosed) {
add(StatusUpdated(deviceStatus));
}
}
}
usersMap['status'].forEach((element) {
statusList.add(Status(code: element['code'], value: element['value']));
});
deviceStatus =
ThreeGangGlassStatusModel.fromJson(usersMap['productUuid'], statusList);
add(StatusUpdated(deviceStatus));
});
} catch (e) {
emit(ThreeGangGlassSwitchError('Failed to listen to changes: $e'));
}
} catch (_) {}
}
void _onStatusUpdated(
@ -184,4 +170,10 @@ class ThreeGangGlassSwitchBloc
break;
}
}
@override
Future<void> close() {
_deviceStatusSubscription?.cancel();
return super.close();
}
}

View File

@ -111,6 +111,8 @@ class ThreeGangGlassSwitchControlView extends StatelessWidget
child: BuildScheduleView(
category: 'switch_1',
deviceUuid: deviceId,
countdownCode: 'countdown_1',
deviceType: '3GT',
),
));
},
@ -127,6 +129,8 @@ class ThreeGangGlassSwitchControlView extends StatelessWidget
child: BuildScheduleView(
category: 'switch_2',
deviceUuid: deviceId,
countdownCode: 'countdown_2',
deviceType: '3GT',
),
));
},
@ -143,6 +147,8 @@ class ThreeGangGlassSwitchControlView extends StatelessWidget
child: BuildScheduleView(
category: 'switch_3',
deviceUuid: deviceId,
countdownCode: 'countdown_3',
deviceType: '3GT',
),
));
},

View File

@ -102,6 +102,8 @@ class LivingRoomDeviceControlsView extends StatelessWidget
child: BuildScheduleView(
deviceUuid: deviceId,
category: 'switch_1',
countdownCode: 'countdown_1',
deviceType: '3G',
),
));
},
@ -118,6 +120,8 @@ class LivingRoomDeviceControlsView extends StatelessWidget
child: BuildScheduleView(
deviceUuid: deviceId,
category: 'switch_2',
countdownCode: 'countdown_2',
deviceType: '3G',
),
));
},
@ -134,6 +138,8 @@ class LivingRoomDeviceControlsView extends StatelessWidget
child: BuildScheduleView(
deviceUuid: deviceId,
category: 'switch_3',
countdownCode: 'countdown_3',
deviceType: '3G',
),
));
},

View File

@ -1,173 +1,177 @@
import 'dart:async';
import 'dart:developer';
import 'dart:async';
import 'package:bloc/bloc.dart';
import 'package:equatable/equatable.dart';
import 'package:firebase_database/firebase_database.dart';
import 'package:flutter/foundation.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/two_g_glass_switch/models/two_gang_glass_status_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';
import 'package:bloc/bloc.dart';
import 'package:equatable/equatable.dart';
import 'package:firebase_database/firebase_database.dart';
import 'package:flutter/foundation.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/two_g_glass_switch/models/two_gang_glass_status_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 'two_gang_glass_switch_event.dart';
part 'two_gang_glass_switch_state.dart';
part 'two_gang_glass_switch_event.dart';
part 'two_gang_glass_switch_state.dart';
class TwoGangGlassSwitchBloc
extends Bloc<TwoGangGlassSwitchEvent, TwoGangGlassSwitchState> {
final String deviceId;
final ControlDeviceService controlDeviceService;
final BatchControlDevicesService batchControlDevicesService;
class TwoGangGlassSwitchBloc
extends Bloc<TwoGangGlassSwitchEvent, TwoGangGlassSwitchState> {
final String deviceId;
final ControlDeviceService controlDeviceService;
final BatchControlDevicesService batchControlDevicesService;
late TwoGangGlassStatusModel deviceStatus;
late TwoGangGlassStatusModel deviceStatus;
TwoGangGlassSwitchBloc({
required this.deviceId,
required this.controlDeviceService,
required this.batchControlDevicesService,
}) : super(TwoGangGlassSwitchInitial()) {
on<TwoGangGlassSwitchFetchDeviceEvent>(_onFetchDeviceStatus);
on<TwoGangGlassSwitchControl>(_onControl);
on<TwoGangGlassSwitchBatchControl>(_onBatchControl);
on<TwoGangGlassSwitchFetchBatchStatusEvent>(_onFetchBatchStatus);
on<TwoGangGlassFactoryReset>(_onFactoryReset);
on<StatusUpdated>(_onStatusUpdated);
}
Future<void> _onFetchDeviceStatus(
TwoGangGlassSwitchFetchDeviceEvent event,
Emitter<TwoGangGlassSwitchState> emit,
) async {
emit(TwoGangGlassSwitchLoading());
try {
final status = await DevicesManagementApi().getDeviceStatus(event.deviceId);
deviceStatus = TwoGangGlassStatusModel.fromJson(event.deviceId, status.status);
_listenToChanges(event.deviceId);
emit(TwoGangGlassSwitchStatusLoaded(deviceStatus));
} catch (e) {
emit(TwoGangGlassSwitchError(e.toString()));
TwoGangGlassSwitchBloc({
required this.deviceId,
required this.controlDeviceService,
required this.batchControlDevicesService,
}) : super(TwoGangGlassSwitchInitial()) {
on<TwoGangGlassSwitchFetchDeviceEvent>(_onFetchDeviceStatus);
on<TwoGangGlassSwitchControl>(_onControl);
on<TwoGangGlassSwitchBatchControl>(_onBatchControl);
on<TwoGangGlassSwitchFetchBatchStatusEvent>(_onFetchBatchStatus);
on<TwoGangGlassFactoryReset>(_onFactoryReset);
on<StatusUpdated>(_onStatusUpdated);
}
}
void _listenToChanges(String deviceId) {
try {
final ref = FirebaseDatabase.instance.ref(
'device-status/$deviceId',
);
ref.onValue.listen((event) {
final eventsMap = event.snapshot.value as Map<dynamic, dynamic>;
List<Status> statusList = [];
eventsMap['status'].forEach((element) {
statusList.add(Status(code: element['code'], value: element['value']));
});
deviceStatus = TwoGangGlassStatusModel.fromJson(deviceId, statusList);
add(StatusUpdated(deviceStatus));
});
} catch (_) {
log(
'Error listening to changes',
name: 'TwoGangGlassSwitchBloc._listenToChanges',
);
}
}
Future<void> _onControl(
TwoGangGlassSwitchControl event,
Emitter<TwoGangGlassSwitchState> emit,
) async {
emit(TwoGangGlassSwitchLoading());
_updateLocalValue(event.code, event.value);
emit(TwoGangGlassSwitchStatusLoaded(deviceStatus));
try {
await controlDeviceService.controlDevice(
deviceUuid: event.deviceId,
status: Status(code: event.code, value: event.value),
);
} catch (e) {
_updateLocalValue(event.code, !event.value);
emit(TwoGangGlassSwitchError(e.toString()));
}
}
Future<void> _onBatchControl(
TwoGangGlassSwitchBatchControl event,
Emitter<TwoGangGlassSwitchState> emit,
) async {
emit(TwoGangGlassSwitchLoading());
_updateLocalValue(event.code, event.value);
emit(TwoGangGlassSwitchBatchStatusLoaded(deviceStatus));
try {
await batchControlDevicesService.batchControlDevices(
uuids: event.deviceIds,
code: event.code,
value: event.value,
);
} catch (e) {
_updateLocalValue(event.code, !event.value);
emit(TwoGangGlassSwitchError(e.toString()));
}
}
Future<void> _onFetchBatchStatus(
TwoGangGlassSwitchFetchBatchStatusEvent event,
Emitter<TwoGangGlassSwitchState> emit,
) async {
emit(TwoGangGlassSwitchLoading());
try {
final status = await DevicesManagementApi().getBatchStatus(event.deviceIds);
deviceStatus = TwoGangGlassStatusModel.fromJson(
event.deviceIds.first,
status.status,
);
emit(TwoGangGlassSwitchBatchStatusLoaded(deviceStatus));
} catch (e) {
emit(TwoGangGlassSwitchError(e.toString()));
}
}
Future<void> _onFactoryReset(
TwoGangGlassFactoryReset event,
Emitter<TwoGangGlassSwitchState> emit,
) async {
emit(TwoGangGlassSwitchLoading());
try {
final response = await DevicesManagementApi().factoryReset(
event.factoryReset,
event.deviceId,
);
if (!response) {
emit(TwoGangGlassSwitchError('Failed to reset device'));
} else {
add(TwoGangGlassSwitchFetchDeviceEvent(event.deviceId));
Future<void> _onFetchDeviceStatus(
TwoGangGlassSwitchFetchDeviceEvent event,
Emitter<TwoGangGlassSwitchState> emit,
) async {
emit(TwoGangGlassSwitchLoading());
try {
final status = await DevicesManagementApi().getDeviceStatus(event.deviceId);
deviceStatus = TwoGangGlassStatusModel.fromJson(event.deviceId, status.status);
_listenToChanges(event.deviceId);
emit(TwoGangGlassSwitchStatusLoaded(deviceStatus));
} catch (e) {
emit(TwoGangGlassSwitchError(e.toString()));
}
} catch (e) {
emit(TwoGangGlassSwitchError(e.toString()));
}
}
void _onStatusUpdated(
StatusUpdated event,
Emitter<TwoGangGlassSwitchState> emit,
) {
deviceStatus = event.deviceStatus;
emit(TwoGangGlassSwitchStatusLoaded(deviceStatus));
}
StreamSubscription<DatabaseEvent>? _deviceStatusSubscription;
void _updateLocalValue(String code, bool value) {
switch (code) {
case 'switch_1':
deviceStatus = deviceStatus.copyWith(switch1: value);
break;
case 'switch_2':
deviceStatus = deviceStatus.copyWith(switch2: value);
break;
void _listenToChanges(String deviceId) {
try {
final ref = FirebaseDatabase.instance.ref('device-status/$deviceId');
_deviceStatusSubscription = ref.onValue.listen((DatabaseEvent event) async {
if (event.snapshot.value == null) return;
final usersMap = event.snapshot.value! as Map<dynamic, dynamic>;
final statusList = <Status>[];
usersMap['status'].forEach((element) {
statusList.add(Status(code: element['code'], value: element['value']));
});
deviceStatus =
TwoGangGlassStatusModel.fromJson(usersMap['productUuid'], statusList);
add(StatusUpdated(deviceStatus));
});
} catch (_) {}
}
Future<void> _onControl(
TwoGangGlassSwitchControl event,
Emitter<TwoGangGlassSwitchState> emit,
) async {
emit(TwoGangGlassSwitchLoading());
_updateLocalValue(event.code, event.value);
emit(TwoGangGlassSwitchStatusLoaded(deviceStatus));
try {
await controlDeviceService.controlDevice(
deviceUuid: event.deviceId,
status: Status(code: event.code, value: event.value),
);
} catch (e) {
_updateLocalValue(event.code, !event.value);
emit(TwoGangGlassSwitchError(e.toString()));
}
}
Future<void> _onBatchControl(
TwoGangGlassSwitchBatchControl event,
Emitter<TwoGangGlassSwitchState> emit,
) async {
emit(TwoGangGlassSwitchLoading());
_updateLocalValue(event.code, event.value);
emit(TwoGangGlassSwitchBatchStatusLoaded(deviceStatus));
try {
await batchControlDevicesService.batchControlDevices(
uuids: event.deviceIds,
code: event.code,
value: event.value,
);
} catch (e) {
_updateLocalValue(event.code, !event.value);
emit(TwoGangGlassSwitchError(e.toString()));
}
}
Future<void> _onFetchBatchStatus(
TwoGangGlassSwitchFetchBatchStatusEvent event,
Emitter<TwoGangGlassSwitchState> emit,
) async {
emit(TwoGangGlassSwitchLoading());
try {
final status = await DevicesManagementApi().getBatchStatus(event.deviceIds);
deviceStatus = TwoGangGlassStatusModel.fromJson(
event.deviceIds.first,
status.status,
);
emit(TwoGangGlassSwitchBatchStatusLoaded(deviceStatus));
} catch (e) {
emit(TwoGangGlassSwitchError(e.toString()));
}
}
Future<void> _onFactoryReset(
TwoGangGlassFactoryReset event,
Emitter<TwoGangGlassSwitchState> emit,
) async {
emit(TwoGangGlassSwitchLoading());
try {
final response = await DevicesManagementApi().factoryReset(
event.factoryReset,
event.deviceId,
);
if (!response) {
emit(TwoGangGlassSwitchError('Failed to reset device'));
} else {
add(TwoGangGlassSwitchFetchDeviceEvent(event.deviceId));
}
} catch (e) {
emit(TwoGangGlassSwitchError(e.toString()));
}
}
void _onStatusUpdated(
StatusUpdated event,
Emitter<TwoGangGlassSwitchState> emit,
) {
deviceStatus = event.deviceStatus;
emit(TwoGangGlassSwitchStatusLoaded(deviceStatus));
}
void _updateLocalValue(String code, bool value) {
switch (code) {
case 'switch_1':
deviceStatus = deviceStatus.copyWith(switch1: value);
break;
case 'switch_2':
deviceStatus = deviceStatus.copyWith(switch2: value);
break;
}
}
@override
Future<void> close() {
_deviceStatusSubscription?.cancel();
return super.close();
}
}

View File

@ -102,6 +102,8 @@ class TwoGangGlassSwitchControlView extends StatelessWidget
builder: (ctx) => BlocProvider.value(
value: BlocProvider.of<TwoGangGlassSwitchBloc>(context),
child: BuildScheduleView(
deviceType: '2GT',
countdownCode: 'countdown_1',
deviceUuid: deviceId,
category: 'switch_1',
),
@ -118,6 +120,8 @@ class TwoGangGlassSwitchControlView extends StatelessWidget
builder: (ctx) => BlocProvider.value(
value: BlocProvider.of<TwoGangGlassSwitchBloc>(context),
child: BuildScheduleView(
deviceType: '2GT',
countdownCode: 'countdown_2',
deviceUuid: deviceId,
category: 'switch_2',
),

View File

@ -97,6 +97,8 @@ class TwoGangBatchControlView extends StatelessWidget
child: BuildScheduleView(
category: 'switch_1',
deviceUuid: deviceIds.first,
countdownCode: 'countdown_1',
deviceType: '2G',
),
));
},
@ -114,6 +116,8 @@ class TwoGangBatchControlView extends StatelessWidget
child: BuildScheduleView(
category: 'switch_2',
deviceUuid: deviceIds.first,
countdownCode: 'countdown_2',
deviceType: '2G',
),
));
},
@ -121,10 +125,7 @@ class TwoGangBatchControlView extends StatelessWidget
subtitle: 'Scheduling',
iconPath: Assets.scheduling,
),
// FirmwareUpdateWidget(
// deviceId: deviceIds.first,
// version: 12,
// ),
FactoryResetWidget(callFactoryReset: () {
context.read<TwoGangSwitchBloc>().add(
TwoGangFactoryReset(

View File

@ -103,6 +103,8 @@ class TwoGangDeviceControlView extends StatelessWidget
child: BuildScheduleView(
deviceUuid: deviceId,
category: 'switch_1',
countdownCode: 'countdown_1',
deviceType: '2G',
),
));
},
@ -125,6 +127,8 @@ class TwoGangDeviceControlView extends StatelessWidget
child: BuildScheduleView(
deviceUuid: deviceId,
category: 'switch_2',
countdownCode: 'countdown_2',
deviceType: '2G',
),
));
},

View File

@ -18,9 +18,10 @@ class ScheduleDialogHelper {
ScheduleEntry? schedule,
bool isEdit = false,
String? code,
required String deviceType,
}) {
bool temp;
if (schedule?.category == 'CUR_2') {
if (deviceType == 'CUR_2') {
temp = schedule!.function.value == 'open' ? true : false;
} else {
temp = schedule!.function.value;
@ -103,8 +104,7 @@ class ScheduleDialogHelper {
setState(() => selectedDays[i] = v);
}),
const SizedBox(height: 16),
_buildFunctionSwitch(schedule!.category, ctx, functionOn!,
(v) {
_buildFunctionSwitch(deviceType, ctx, functionOn!, (v) {
setState(() => functionOn = v);
}),
],
@ -120,32 +120,29 @@ class ScheduleDialogHelper {
),
),
SizedBox(
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: code ?? 'switch_1',
value: temp,
// functionOn,
),
days: _convertSelectedDaysToStrings(selectedDays),
scheduleId: schedule?.scheduleId,
);
Navigator.pop(ctx, entry);
},
child: const Text('Save'),
),
),
width: 100,
child: ElevatedButton(
onPressed: () {
dynamic temp;
if (deviceType == 'CUR_2') {
temp = functionOn! ? 'open' : 'close';
} else {
temp = functionOn;
}
final entry = ScheduleEntry(
category: schedule?.category ?? 'switch_1',
time: _formatTimeOfDayToISO(selectedTime),
function: Status(
code: code ?? 'switch_1',
value: temp,
),
days: _convertSelectedDaysToStrings(selectedDays),
scheduleId: schedule.scheduleId,
);
Navigator.pop(ctx, entry);
},
child: const Text('Save'),
)),
],
);
},

View File

@ -84,6 +84,8 @@ class WaterHeaterDeviceControlView extends StatelessWidget
child: BuildScheduleView(
deviceUuid: device.uuid ?? '',
category: 'switch_1',
countdownCode: 'countdown_1',
deviceType: 'WH',
),
));
},

View File

@ -105,7 +105,7 @@ class HomeBloc extends Bloc<HomeEvent, HomeState> {
color: const Color(0xFF0026A2),
),
HomeItemModel(
title: 'Devices Management',
title: 'Device Management',
icon: Assets.devicesIcon,
active: true,
onPress: (context) {

View File

@ -455,7 +455,7 @@ class UsersBloc extends Bloc<UsersEvent, UsersState> {
Future<void> checkEmail(
CheckEmailEvent event, Emitter<UsersState> emit) async {
emit(UsersLoadingState());
String? res = await UserPermissionApi().checkEmail(
String? res = await UserPermissionApi().checkEmail(
emailController.text,
);
checkEmailValid = res!;

View File

@ -34,7 +34,8 @@ class _AddNewUserDialogState extends State<AddNewUserDialog> {
return Dialog(
child: Container(
decoration: const BoxDecoration(
color: Colors.white, borderRadius: BorderRadius.all(Radius.circular(20))),
color: Colors.white,
borderRadius: BorderRadius.all(Radius.circular(20))),
width: 900,
child: Column(
children: [
@ -63,7 +64,8 @@ class _AddNewUserDialogState extends State<AddNewUserDialog> {
children: [
_buildStep1Indicator(1, "Basics", _blocRole),
_buildStep2Indicator(2, "Spaces", _blocRole),
_buildStep3Indicator(3, "Role & Permissions", _blocRole),
_buildStep3Indicator(
3, "Role & Permissions", _blocRole),
],
),
),
@ -105,18 +107,32 @@ class _AddNewUserDialogState extends State<AddNewUserDialog> {
),
InkWell(
onTap: () {
final isBasicsStep = currentStep == 1;
if (isBasicsStep) {
// Validate the form first
final isValid = _blocRole.formKey.currentState
?.validate() ??
false;
if (!isValid)
return; // Stop if form is not valid
}
_blocRole.add(const CheckEmailEvent());
setState(() {
if (currentStep < 3) {
currentStep++;
if (currentStep == 2) {
_blocRole.add(const CheckStepStatus(isEditUser: false));
_blocRole.add(const CheckStepStatus(
isEditUser: false));
} else if (currentStep == 3) {
_blocRole.add(const CheckSpacesStepStatus());
_blocRole
.add(const CheckSpacesStepStatus());
}
} else {
_blocRole.add(SendInviteUsers(context: context));
_blocRole
.add(SendInviteUsers(context: context));
}
});
},
@ -124,8 +140,11 @@ class _AddNewUserDialogState extends State<AddNewUserDialog> {
currentStep < 3 ? "Next" : "Save",
style: TextStyle(
color: (_blocRole.isCompleteSpaces == false ||
_blocRole.isCompleteBasics == false ||
_blocRole.isCompleteRolePermissions == false) &&
_blocRole.isCompleteBasics ==
false ||
_blocRole
.isCompleteRolePermissions ==
false) &&
currentStep == 3
? ColorsManager.grayColor
: ColorsManager.secondaryColor),
@ -143,7 +162,7 @@ class _AddNewUserDialogState extends State<AddNewUserDialog> {
Widget _getFormContent() {
switch (currentStep) {
case 1:
return const BasicsView(
return BasicsView(
userId: '',
);
case 2:
@ -196,8 +215,12 @@ class _AddNewUserDialogState extends State<AddNewUserDialog> {
label,
style: TextStyle(
fontSize: 16,
color: currentStep == step ? ColorsManager.blackColor : ColorsManager.greyColor,
fontWeight: currentStep == step ? FontWeight.bold : FontWeight.normal,
color: currentStep == step
? ColorsManager.blackColor
: ColorsManager.greyColor,
fontWeight: currentStep == step
? FontWeight.bold
: FontWeight.normal,
),
),
],
@ -260,8 +283,12 @@ class _AddNewUserDialogState extends State<AddNewUserDialog> {
label,
style: TextStyle(
fontSize: 16,
color: currentStep == step ? ColorsManager.blackColor : ColorsManager.greyColor,
fontWeight: currentStep == step ? FontWeight.bold : FontWeight.normal,
color: currentStep == step
? ColorsManager.blackColor
: ColorsManager.greyColor,
fontWeight: currentStep == step
? FontWeight.bold
: FontWeight.normal,
),
),
],
@ -318,8 +345,12 @@ class _AddNewUserDialogState extends State<AddNewUserDialog> {
label,
style: TextStyle(
fontSize: 16,
color: currentStep == step ? ColorsManager.blackColor : ColorsManager.greyColor,
fontWeight: currentStep == step ? FontWeight.bold : FontWeight.normal,
color: currentStep == step
? ColorsManager.blackColor
: ColorsManager.greyColor,
fontWeight: currentStep == step
? FontWeight.bold
: FontWeight.normal,
),
),
],

View File

@ -1,9 +1,12 @@
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:intl_phone_field/countries.dart';
import 'package:intl_phone_field/country_picker_dialog.dart';
import 'package:intl_phone_field/intl_phone_field.dart';
import 'package:syncrow_web/pages/roles_and_permission/users_page/add_user_dialog/bloc/users_bloc.dart';
import 'package:syncrow_web/pages/roles_and_permission/users_page/add_user_dialog/bloc/users_event.dart';
import 'package:syncrow_web/pages/roles_and_permission/users_page/add_user_dialog/bloc/users_status.dart';
import 'package:syncrow_web/utils/color_manager.dart';
import 'package:syncrow_web/utils/extension/build_context_x.dart';
@ -11,7 +14,9 @@ import 'package:syncrow_web/utils/style.dart';
class BasicsView extends StatelessWidget {
final String? userId;
const BasicsView({super.key, this.userId = ''});
Timer? _debounce;
BasicsView({super.key, this.userId = ''});
@override
Widget build(BuildContext context) {
return BlocBuilder<UsersBloc, UsersState>(builder: (context, state) {
@ -21,6 +26,7 @@ class BasicsView extends StatelessWidget {
}
return Form(
key: _blocRole.formKey,
autovalidateMode: AutovalidateMode.onUserInteraction,
child: ListView(
shrinkWrap: true,
children: [
@ -208,6 +214,14 @@ class BasicsView extends StatelessWidget {
fontSize: 12,
color: ColorsManager.textGray),
),
onChanged: (value) {
if (_debounce?.isActive ?? false) _debounce!.cancel();
_debounce = Timer(const Duration(milliseconds: 800), () {
_blocRole.add(const CheckEmailEvent());
});
},
validator: (value) {
if (value == null || value.isEmpty) {
return 'Enter Email Address';

View File

@ -32,113 +32,114 @@ class SpaceDropdown extends StatelessWidget {
color: ColorsManager.blackColor,
),
),
SizedBox(
child: Container(
DropdownButton2<String>(
underline: const SizedBox(),
buttonStyleData: ButtonStyleData(
decoration:
BoxDecoration(borderRadius: BorderRadius.circular(12)),
),
value: selectedValue,
items: spaces.map((space) {
return DropdownMenuItem<String>(
value: space.uuid,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(
' ${space.name}',
style: Theme.of(context).textTheme.bodyMedium!.copyWith(
fontSize: 16,
fontWeight: FontWeight.bold,
color: selectedValue == space.uuid
? ColorsManager.dialogBlueTitle
: ColorsManager.blackColor,
),
),
Text(
' ${space.lastThreeParents}',
style: Theme.of(context).textTheme.bodyMedium!.copyWith(
fontSize: 12,
color: selectedValue == space.uuid
? ColorsManager.dialogBlueTitle
: ColorsManager.blackColor,
),
),
],
),
);
}).toList(),
onChanged: onChanged,
style: TextStyle(
color: Colors.black,
fontSize: 13,
),
hint: Padding(
padding: const EdgeInsets.only(left: 10),
child: Text(
hintMessage,
style: Theme.of(context).textTheme.bodySmall!.copyWith(
color: ColorsManager.textGray,
),
),
),
customButton: Container(
height: 40,
decoration: BoxDecoration(
border: Border.all(color: ColorsManager.textGray, width: 1.0),
borderRadius: BorderRadius.circular(10),
),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Expanded(
flex: 8,
child: Padding(
padding: const EdgeInsets.only(left: 10),
child: Text(
selectedValue != null
? spaces
.firstWhere((e) => e.uuid == selectedValue)
.name
: hintMessage,
style: Theme.of(context).textTheme.bodySmall!.copyWith(
fontSize: 13,
color: selectedValue != null
? Colors.black
: ColorsManager.textGray,
),
overflow: TextOverflow.ellipsis,
),
),
),
Expanded(
child: Container(
decoration: BoxDecoration(
color: Colors.grey[200],
borderRadius: const BorderRadius.only(
topRight: Radius.circular(10),
bottomRight: Radius.circular(10),
),
),
height: 45,
child: const Icon(
Icons.keyboard_arrow_down,
color: ColorsManager.textGray,
),
),
),
],
),
),
dropdownStyleData: DropdownStyleData(
maxHeight: MediaQuery.of(context).size.height * 0.4,
decoration: BoxDecoration(
color: ColorsManager.whiteColors,
borderRadius: BorderRadius.circular(10),
),
child: DropdownButton2<String>(
underline: const SizedBox(),
value: selectedValue,
items: spaces.map((space) {
return DropdownMenuItem<String>(
value: space.uuid,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(
' ${space.name}',
style:
Theme.of(context).textTheme.bodyMedium!.copyWith(
fontSize: 12,
color: ColorsManager.blackColor,
),
),
Text(
' ${space.lastThreeParents}',
style:
Theme.of(context).textTheme.bodyMedium!.copyWith(
fontSize: 12,
),
),
],
),
);
}).toList(),
onChanged: onChanged,
style: TextStyle(color: Colors.black),
hint: Padding(
padding: const EdgeInsets.only(left: 10),
child: Text(
hintMessage,
style: Theme.of(context).textTheme.bodySmall!.copyWith(
color: ColorsManager.textGray,
),
),
),
customButton: Container(
height: 45,
decoration: BoxDecoration(
border:
Border.all(color: ColorsManager.textGray, width: 1.0),
borderRadius: BorderRadius.circular(10),
),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Expanded(
flex: 8,
child: Padding(
padding: const EdgeInsets.only(left: 10),
child: Text(
selectedValue != null
? spaces
.firstWhere((e) => e.uuid == selectedValue)
.name
: hintMessage,
style:
Theme.of(context).textTheme.bodySmall!.copyWith(
color: selectedValue != null
? Colors.black
: ColorsManager.textGray,
),
overflow: TextOverflow.ellipsis,
),
),
),
Expanded(
child: Container(
decoration: BoxDecoration(
color: Colors.grey[200],
borderRadius: const BorderRadius.only(
topRight: Radius.circular(10),
bottomRight: Radius.circular(10),
),
),
height: 45,
child: const Icon(
Icons.keyboard_arrow_down,
color: ColorsManager.textGray,
),
),
),
],
),
),
dropdownStyleData: DropdownStyleData(
maxHeight: MediaQuery.of(context).size.height * 0.4,
decoration: BoxDecoration(
color: ColorsManager.whiteColors,
borderRadius: BorderRadius.circular(10),
),
),
menuItemStyleData: const MenuItemStyleData(
height: 60,
),
),
),
menuItemStyleData: const MenuItemStyleData(
height: 60,
),
),
],

View File

@ -121,7 +121,8 @@ class _RoutineViewCardState extends State<RoutineViewCard> {
child: SizedBox(
width: 16,
height: 16,
child: CircularProgressIndicator(strokeWidth: 2),
child:
CircularProgressIndicator(strokeWidth: 2),
),
),
)
@ -159,8 +160,9 @@ class _RoutineViewCardState extends State<RoutineViewCard> {
height: iconSize,
width: iconSize,
fit: BoxFit.contain,
errorBuilder: (context, error, stackTrace) =>
Image.asset(
errorBuilder:
(context, error, stackTrace) =>
Image.asset(
Assets.logo,
height: iconSize,
width: iconSize,
@ -203,7 +205,8 @@ class _RoutineViewCardState extends State<RoutineViewCard> {
maxLines: 1,
style: context.textTheme.bodySmall?.copyWith(
color: ColorsManager.blackColor,
fontSize: widget.isSmallScreenSize(context) ? 10 : 12,
fontSize:
widget.isSmallScreenSize(context) ? 10 : 12,
),
),
if (widget.spaceName != '')
@ -222,8 +225,9 @@ class _RoutineViewCardState extends State<RoutineViewCard> {
maxLines: 1,
style: context.textTheme.bodySmall?.copyWith(
color: ColorsManager.blackColor,
fontSize:
widget.isSmallScreenSize(context) ? 10 : 12,
fontSize: widget.isSmallScreenSize(context)
? 10
: 12,
),
),
],

View File

@ -7,6 +7,10 @@ import 'package:syncrow_web/pages/space_management_v2/modules/communities/data/s
import 'package:syncrow_web/pages/space_management_v2/modules/communities/domain/params/load_communities_param.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/products/data/services/remote_products_service.dart';
import 'package:syncrow_web/pages/space_management_v2/modules/products/presentation/bloc/products_bloc.dart';
import 'package:syncrow_web/pages/space_management_v2/modules/space_details/data/services/remote_space_details_service.dart';
import 'package:syncrow_web/pages/space_management_v2/modules/space_details/presentation/bloc/space_details_bloc.dart';
import 'package:syncrow_web/services/api/http_service.dart';
import 'package:syncrow_web/utils/theme/responsive_text_theme.dart';
import 'package:syncrow_web/web_layout/web_scaffold.dart';
@ -26,6 +30,16 @@ class SpaceManagementPage extends StatelessWidget {
)..add(const LoadCommunities(LoadCommunitiesParam())),
),
BlocProvider(create: (context) => CommunitiesTreeSelectionBloc()),
BlocProvider(
create: (context) => SpaceDetailsBloc(
RemoteSpaceDetailsService(httpService: HTTPService()),
),
),
BlocProvider(
create: (context) => ProductsBloc(
RemoteProductsService(HTTPService()),
),
),
],
child: WebScaffold(
appBarTitle: Text(

View File

@ -0,0 +1,116 @@
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_svg/svg.dart';
import 'package:syncrow_web/pages/space_management_v2/main_module/widgets/community_structure_header_action_buttons.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';
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';
import 'package:syncrow_web/utils/constants/assets.dart';
class CommunityStructureHeader extends StatelessWidget {
const CommunityStructureHeader({super.key});
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
final screenWidth = MediaQuery.of(context).size.width;
return Container(
padding: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 16.0),
decoration: BoxDecoration(
color: ColorsManager.whiteColors,
boxShadow: [
BoxShadow(
color: ColorsManager.shadowBlackColor,
blurRadius: 8,
offset: const Offset(0, 4),
),
],
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Expanded(
child: _buildCommunityInfo(context, theme, screenWidth),
),
const SizedBox(width: 16),
],
),
],
),
);
}
void _showCreateCommunityDialog(BuildContext context) {
showDialog<void>(
context: context,
builder: (context) => CreateCommunityDialog(
title: const Text('Edit Community'),
onCreateCommunity: (community) {
// TODO(FarisArmoush): Implement
},
),
);
}
Widget _buildCommunityInfo(
BuildContext context, ThemeData theme, double screenWidth) {
final selectedCommunity =
context.watch<CommunitiesTreeSelectionBloc>().state.selectedCommunity;
final selectedSpace =
context.watch<CommunitiesTreeSelectionBloc>().state.selectedSpace;
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Community Structure',
style: theme.textTheme.headlineLarge
?.copyWith(color: ColorsManager.blackColor),
),
if (selectedCommunity != null)
Row(
children: [
Expanded(
child: Row(
children: [
Flexible(
child: SelectableText(
selectedCommunity.name,
style: theme.textTheme.bodyLarge
?.copyWith(color: ColorsManager.blackColor),
maxLines: 1,
),
),
const SizedBox(width: 2),
GestureDetector(
onTap: () => _showCreateCommunityDialog(context),
child: SvgPicture.asset(
Assets.iconEdit,
width: 16,
height: 16,
),
),
],
),
),
const SizedBox(width: 8),
CommunityStructureHeaderActionButtons(
onDelete: (space) {},
onDuplicate: (space) {},
onEdit: (space) {
SpaceDetailsDialogHelper.showEdit(
context,
spaceModel: selectedSpace!,
);
},
selectedSpace: selectedSpace,
),
],
),
],
);
}
}

View File

@ -0,0 +1,46 @@
import 'package:flutter/material.dart';
import 'package:syncrow_web/pages/space_management_v2/main_module/widgets/community_structure_header_button.dart';
import 'package:syncrow_web/pages/space_management_v2/modules/communities/domain/models/space_model.dart';
import 'package:syncrow_web/utils/constants/assets.dart';
class CommunityStructureHeaderActionButtons extends StatelessWidget {
const CommunityStructureHeaderActionButtons({
super.key,
required this.onDelete,
required this.selectedSpace,
required this.onDuplicate,
required this.onEdit,
});
final void Function(SpaceModel space) onDelete;
final void Function(SpaceModel space) onDuplicate;
final void Function(SpaceModel space) onEdit;
final SpaceModel? selectedSpace;
@override
Widget build(BuildContext context) {
return Wrap(
alignment: WrapAlignment.end,
spacing: 10,
children: [
if (selectedSpace != null) ...[
CommunityStructureHeaderButton(
label: 'Edit',
svgAsset: Assets.editSpace,
onPressed: () => onEdit(selectedSpace!),
),
CommunityStructureHeaderButton(
label: 'Duplicate',
svgAsset: Assets.duplicate,
onPressed: () => onDuplicate(selectedSpace!),
),
CommunityStructureHeaderButton(
label: 'Delete',
svgAsset: Assets.spaceDelete,
onPressed: () => onDelete(selectedSpace!),
),
],
],
);
}
}

View File

@ -0,0 +1,61 @@
import 'package:flutter/material.dart';
import 'package:flutter_svg/svg.dart';
import 'package:syncrow_web/pages/common/buttons/default_button.dart';
import 'package:syncrow_web/utils/color_manager.dart';
import 'package:syncrow_web/utils/extension/build_context_x.dart';
class CommunityStructureHeaderButton extends StatelessWidget {
const CommunityStructureHeaderButton({
super.key,
required this.label,
required this.onPressed,
this.svgAsset,
});
final String label;
final VoidCallback onPressed;
final String? svgAsset;
@override
Widget build(BuildContext context) {
const double buttonHeight = 40;
return ConstrainedBox(
constraints: const BoxConstraints(
maxWidth: 130,
minHeight: buttonHeight,
),
child: DefaultButton(
onPressed: onPressed,
borderWidth: 2,
backgroundColor: ColorsManager.textFieldGreyColor,
foregroundColor: ColorsManager.blackColor,
borderRadius: 12.0,
padding: 2.0,
height: buttonHeight,
elevation: 0,
borderColor: ColorsManager.lightGrayColor,
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
if (svgAsset != null)
SvgPicture.asset(
svgAsset!,
width: 20,
height: 20,
),
const SizedBox(width: 10),
Flexible(
child: Text(
label,
style: context.textTheme.bodySmall
?.copyWith(color: ColorsManager.blackColor, fontSize: 14),
overflow: TextOverflow.ellipsis,
maxLines: 1,
),
),
],
),
),
);
}
}

View File

@ -1,6 +1,7 @@
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/community_structure_header.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';
@ -18,9 +19,17 @@ class SpaceManagementCommunityStructure extends StatelessWidget {
replacement: const Row(
children: [spacer, Expanded(child: CreateSpaceButton()), spacer],
),
child: CommunityStructureCanvas(
community: selectedCommunity,
selectedSpace: selectedSpace,
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
const CommunityStructureHeader(),
Expanded(
child: CommunityStructureCanvas(
community: selectedCommunity,
selectedSpace: selectedSpace,
),
),
],
),
);
}

View File

@ -19,6 +19,16 @@ class SpaceModel extends Equatable {
required this.parent,
});
factory SpaceModel.empty() => const SpaceModel(
uuid: '',
createdAt: null,
updatedAt: null,
spaceName: '',
icon: '',
children: [],
parent: null,
);
factory SpaceModel.fromJson(Map<String, dynamic> json) {
return SpaceModel(
uuid: json['uuid'] as String? ?? '',

View File

@ -1,9 +1,9 @@
import 'package:dio/dio.dart';
import 'package:syncrow_web/pages/space_management_v2/modules/products/domain/models/product.dart';
import 'package:syncrow_web/pages/space_management_v2/modules/products/domain/params/load_products_param.dart';
import 'package:syncrow_web/pages/space_management_v2/modules/products/domain/services/products_service.dart';
import 'package:syncrow_web/services/api/api_exception.dart';
import 'package:syncrow_web/services/api/http_service.dart';
import 'package:syncrow_web/utils/constants/api_const.dart';
class RemoteProductsService implements ProductsService {
const RemoteProductsService(this._httpService);
@ -13,17 +13,14 @@ class RemoteProductsService implements ProductsService {
static const _defaultErrorMessage = 'Failed to load devices';
@override
Future<List<Product>> getProducts(LoadProductsParam param) async {
Future<List<Product>> getProducts() async {
try {
final response = await _httpService.get(
path: 'devices',
queryParameters: {
'spaceUuid': param.spaceUuid,
if (param.type != null) 'type': param.type,
if (param.status != null) 'status': param.status,
},
path: ApiEndpoints.listProducts,
expectedResponseModel: (data) {
return (data as List)
final json = data as Map<String, dynamic>;
final products = json['data'] as List<dynamic>;
return products
.map((e) => Product.fromJson(e as Map<String, dynamic>))
.toList();
},

View File

@ -3,16 +3,18 @@ import 'package:equatable/equatable.dart';
class Product extends Equatable {
final String uuid;
final String name;
final String productType;
const Product({
required this.uuid,
required this.name,
required this.productType,
});
factory Product.fromJson(Map<String, dynamic> json) {
return Product(
uuid: json['uuid'] as String,
name: json['name'] as String,
uuid: json['uuid'] as String? ?? '',
name: json['name'] as String? ?? '',
productType: json['prodType'] as String? ?? '',
);
}
@ -20,9 +22,10 @@ class Product extends Equatable {
return {
'uuid': uuid,
'name': name,
'productType': productType,
};
}
@override
List<Object?> get props => [uuid, name];
List<Object?> get props => [uuid, name, productType];
}

View File

@ -1,11 +0,0 @@
class LoadProductsParam {
final String spaceUuid;
final String? type;
final String? status;
const LoadProductsParam({
required this.spaceUuid,
this.type,
this.status,
});
}

View File

@ -1,6 +1,5 @@
import 'package:syncrow_web/pages/space_management_v2/modules/products/domain/models/product.dart';
import 'package:syncrow_web/pages/space_management_v2/modules/products/domain/params/load_products_param.dart';
abstract class ProductsService {
Future<List<Product>> getProducts(LoadProductsParam param);
Future<List<Product>> getProducts();
}

View File

@ -1,7 +1,6 @@
import 'package:bloc/bloc.dart';
import 'package:equatable/equatable.dart';
import 'package:syncrow_web/pages/space_management_v2/modules/products/domain/models/product.dart';
import 'package:syncrow_web/pages/space_management_v2/modules/products/domain/params/load_products_param.dart';
import 'package:syncrow_web/pages/space_management_v2/modules/products/domain/services/products_service.dart';
import 'package:syncrow_web/services/api/api_exception.dart';
@ -9,20 +8,20 @@ part 'products_event.dart';
part 'products_state.dart';
class ProductsBloc extends Bloc<ProductsEvent, ProductsState> {
final ProductsService _deviceService;
ProductsBloc(this._deviceService) : super(ProductsInitial()) {
ProductsBloc(this._productsService) : super(ProductsInitial()) {
on<LoadProducts>(_onLoadProducts);
}
final ProductsService _productsService;
Future<void> _onLoadProducts(
LoadProducts event,
Emitter<ProductsState> emit,
) async {
emit(ProductsLoading());
try {
final devices = await _deviceService.getProducts(event.param);
emit(ProductsLoaded(devices));
final products = await _productsService.getProducts();
emit(ProductsLoaded(products));
} on APIException catch (e) {
emit(ProductsFailure(e.message));
} catch (e) {

View File

@ -8,10 +8,5 @@ sealed class ProductsEvent extends Equatable {
}
final class LoadProducts extends ProductsEvent {
const LoadProducts(this.param);
final LoadProductsParam param;
@override
List<Object> get props => [param];
const LoadProducts();
}

View File

@ -21,10 +21,10 @@ final class ProductsLoaded extends ProductsState {
}
final class ProductsFailure extends ProductsState {
final String message;
final String errorMessage;
const ProductsFailure(this.message);
const ProductsFailure(this.errorMessage);
@override
List<Object> get props => [message];
List<Object> get props => [errorMessage];
}

View File

@ -1,6 +1,7 @@
import 'package:dio/dio.dart';
import 'package:syncrow_web/pages/common/bloc/project_manager.dart';
import 'package:syncrow_web/pages/space_management_v2/modules/space_details/domain/models/space_details_model.dart';
import 'package:syncrow_web/pages/space_management_v2/modules/space_details/domain/params/load_spaces_param.dart';
import 'package:syncrow_web/pages/space_management_v2/modules/space_details/domain/params/load_space_details_param.dart';
import 'package:syncrow_web/pages/space_management_v2/modules/space_details/domain/services/space_details_service.dart';
import 'package:syncrow_web/services/api/api_exception.dart';
import 'package:syncrow_web/services/api/http_service.dart';
@ -15,12 +16,15 @@ class RemoteSpaceDetailsService implements SpaceDetailsService {
static const _defaultErrorMessage = 'Failed to load space details';
@override
Future<SpaceDetailsModel> getSpaceDetails(LoadSpacesParam param) async {
Future<SpaceDetailsModel> getSpaceDetails(LoadSpaceDetailsParam param) async {
try {
final response = await _httpService.get(
path: 'endpoint',
path: await _makeEndpoint(param),
expectedResponseModel: (data) {
return SpaceDetailsModel.fromJson(data as Map<String, dynamic>);
final response = data as Map<String, dynamic>;
return SpaceDetailsModel.fromJson(
response['data'] as Map<String, dynamic>,
);
},
);
return response;
@ -37,4 +41,13 @@ class RemoteSpaceDetailsService implements SpaceDetailsService {
throw APIException(formattedErrorMessage);
}
}
Future<String> _makeEndpoint(LoadSpaceDetailsParam param) async {
final projectUuid = await ProjectManager.getProjectUUID();
if (projectUuid == null || projectUuid.isEmpty) {
throw APIException('Project UUID is not set');
}
return '/projects/$projectUuid/communities/${param.communityUuid}/spaces/${param.spaceUuid}';
}
}

View File

@ -1,6 +1,7 @@
import 'package:equatable/equatable.dart';
import 'package:syncrow_web/pages/space_management_v2/modules/products/domain/models/product.dart';
import 'package:syncrow_web/pages/space_management_v2/modules/tags/domain/models/tag.dart';
import 'package:syncrow_web/utils/constants/assets.dart';
class SpaceDetailsModel extends Equatable {
final String uuid;
@ -17,14 +18,21 @@ class SpaceDetailsModel extends Equatable {
required this.subspaces,
});
factory SpaceDetailsModel.empty() => const SpaceDetailsModel(
uuid: '',
spaceName: '',
icon: Assets.location,
productAllocations: [],
subspaces: [],
);
factory SpaceDetailsModel.fromJson(Map<String, dynamic> json) {
return SpaceDetailsModel(
uuid: json['uuid'] as String,
spaceName: json['spaceName'] as String,
icon: json['icon'] as String,
productAllocations: (json['productAllocations'] as List)
.map((e) => ProductAllocation.fromJson(e as Map<String, dynamic>))
.toList(),
.map((e) => ProductAllocation.fromJson(e as Map<String, dynamic>))
.toList(),
subspaces: (json['subspaces'] as List)
.map((e) => Subspace.fromJson(e as Map<String, dynamic>))
.toList(),
@ -41,6 +49,22 @@ class SpaceDetailsModel extends Equatable {
};
}
SpaceDetailsModel copyWith({
String? uuid,
String? spaceName,
String? icon,
List<ProductAllocation>? productAllocations,
List<Subspace>? subspaces,
}) {
return SpaceDetailsModel(
uuid: uuid ?? this.uuid,
spaceName: spaceName ?? this.spaceName,
icon: icon ?? this.icon,
productAllocations: productAllocations ?? this.productAllocations,
subspaces: subspaces ?? this.subspaces,
);
}
@override
List<Object?> get props => [uuid, spaceName, icon, productAllocations, subspaces];
}
@ -48,12 +72,10 @@ class SpaceDetailsModel extends Equatable {
class ProductAllocation extends Equatable {
final Product product;
final Tag tag;
final String? location;
const ProductAllocation({
required this.product,
required this.tag,
this.location,
});
factory ProductAllocation.fromJson(Map<String, dynamic> json) {
@ -70,6 +92,16 @@ class ProductAllocation extends Equatable {
};
}
ProductAllocation copyWith({
Product? product,
Tag? tag,
}) {
return ProductAllocation(
product: product ?? this.product,
tag: tag ?? this.tag,
);
}
@override
List<Object?> get props => [product, tag];
}
@ -88,7 +120,7 @@ class Subspace extends Equatable {
factory Subspace.fromJson(Map<String, dynamic> json) {
return Subspace(
uuid: json['uuid'] as String,
name: json['name'] as String,
name: json['subspaceName'] as String,
productAllocations: (json['productAllocations'] as List)
.map((e) => ProductAllocation.fromJson(e as Map<String, dynamic>))
.toList(),
@ -103,6 +135,18 @@ class Subspace extends Equatable {
};
}
Subspace copyWith({
String? uuid,
String? name,
List<ProductAllocation>? productAllocations,
}) {
return Subspace(
uuid: uuid ?? this.uuid,
name: name ?? this.name,
productAllocations: productAllocations ?? this.productAllocations,
);
}
@override
List<Object?> get props => [uuid, name, productAllocations];
}

View File

@ -0,0 +1,9 @@
class LoadSpaceDetailsParam {
const LoadSpaceDetailsParam({
required this.spaceUuid,
required this.communityUuid,
});
final String spaceUuid;
final String communityUuid;
}

View File

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

View File

@ -1,6 +1,6 @@
import 'package:syncrow_web/pages/space_management_v2/modules/space_details/domain/models/space_details_model.dart';
import 'package:syncrow_web/pages/space_management_v2/modules/space_details/domain/params/load_spaces_param.dart';
import 'package:syncrow_web/pages/space_management_v2/modules/space_details/domain/params/load_space_details_param.dart';
abstract class SpaceDetailsService {
Future<SpaceDetailsModel> getSpaceDetails(LoadSpacesParam param);
Future<SpaceDetailsModel> getSpaceDetails(LoadSpaceDetailsParam param);
}

View File

@ -1,7 +1,7 @@
import 'package:bloc/bloc.dart';
import 'package:equatable/equatable.dart';
import 'package:syncrow_web/pages/space_management_v2/modules/space_details/domain/models/space_details_model.dart';
import 'package:syncrow_web/pages/space_management_v2/modules/space_details/domain/params/load_spaces_param.dart';
import 'package:syncrow_web/pages/space_management_v2/modules/space_details/domain/params/load_space_details_param.dart';
import 'package:syncrow_web/pages/space_management_v2/modules/space_details/domain/services/space_details_service.dart';
import 'package:syncrow_web/services/api/api_exception.dart';
@ -9,12 +9,13 @@ part 'space_details_event.dart';
part 'space_details_state.dart';
class SpaceDetailsBloc extends Bloc<SpaceDetailsEvent, SpaceDetailsState> {
final SpaceDetailsService _spaceDetailsService;
SpaceDetailsBloc(this._spaceDetailsService) : super(SpaceDetailsInitial()) {
on<LoadSpaceDetails>(_onLoadSpaceDetails);
on<ClearSpaceDetails>(_onClearSpaceDetails);
}
final SpaceDetailsService _spaceDetailsService;
Future<void> _onLoadSpaceDetails(
LoadSpaceDetails event,
Emitter<SpaceDetailsState> emit,
@ -31,4 +32,11 @@ class SpaceDetailsBloc extends Bloc<SpaceDetailsEvent, SpaceDetailsState> {
emit(SpaceDetailsFailure(e.toString()));
}
}
void _onClearSpaceDetails(
ClearSpaceDetails event,
Emitter<SpaceDetailsState> emit,
) {
emit(SpaceDetailsInitial());
}
}

View File

@ -7,11 +7,18 @@ sealed class SpaceDetailsEvent extends Equatable {
List<Object> get props => [];
}
class LoadSpaceDetails extends SpaceDetailsEvent {
final class LoadSpaceDetails extends SpaceDetailsEvent {
const LoadSpaceDetails(this.param);
final LoadSpacesParam param;
final LoadSpaceDetailsParam param;
@override
List<Object> get props => [param];
}
final class ClearSpaceDetails extends SpaceDetailsEvent {
const ClearSpaceDetails();
@override
List<Object> get props => [];
}

View File

@ -21,10 +21,10 @@ final class SpaceDetailsLoaded extends SpaceDetailsState {
}
final class SpaceDetailsFailure extends SpaceDetailsState {
final String message;
final String errorMessage;
const SpaceDetailsFailure(this.message);
const SpaceDetailsFailure(this.errorMessage);
@override
List<Object> get props => [message];
List<Object> get props => [errorMessage];
}

View File

@ -1,11 +1,46 @@
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.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/space_details/data/services/remote_space_details_service.dart';
import 'package:syncrow_web/pages/space_management_v2/modules/space_details/presentation/bloc/space_details_bloc.dart';
import 'package:syncrow_web/pages/space_management_v2/modules/space_details/presentation/widgets/space_details_dialog.dart';
import 'package:syncrow_web/services/api/http_service.dart';
abstract final class SpaceDetailsDialogHelper {
static void showCreate(BuildContext context) {
showDialog<void>(
context: context,
builder: (context) => const SpaceDetailsDialog(),
builder: (_) => BlocProvider(
create: (context) => SpaceDetailsBloc(
RemoteSpaceDetailsService(httpService: HTTPService()),
),
child: SpaceDetailsDialog(
context: context,
title: const Text('Create Space'),
spaceModel: SpaceModel.empty(),
onSave: print,
),
),
);
}
static void showEdit(
BuildContext context, {
required SpaceModel spaceModel,
}) {
showDialog<void>(
context: context,
builder: (_) => BlocProvider(
create: (context) => SpaceDetailsBloc(
RemoteSpaceDetailsService(httpService: HTTPService()),
),
child: SpaceDetailsDialog(
context: context,
title: const Text('Edit Space'),
spaceModel: spaceModel,
onSave: (space) {},
),
),
);
}
}

View File

@ -0,0 +1,60 @@
import 'package:flutter/material.dart';
import 'package:flutter_svg/svg.dart';
import 'package:syncrow_web/utils/color_manager.dart';
class ButtonContentWidget extends StatelessWidget {
final String label;
final String? svgAssets;
final bool disabled;
const ButtonContentWidget({
required this.label,
this.svgAssets,
this.disabled = false,
super.key,
});
@override
Widget build(BuildContext context) {
final screenWidth = MediaQuery.of(context).size.width;
return Opacity(
opacity: disabled ? 0.5 : 1.0,
child: Container(
width: screenWidth * 0.25,
decoration: BoxDecoration(
color: ColorsManager.textFieldGreyColor,
border: Border.all(
color: ColorsManager.neutralGray,
width: 3.0,
),
borderRadius: BorderRadius.circular(20),
),
padding: const EdgeInsets.symmetric(vertical: 10.0, horizontal: 16.0),
child: Row(
children: [
if (svgAssets != null)
Padding(
padding: const EdgeInsets.only(left: 6.0),
child: SvgPicture.asset(
svgAssets!,
width: screenWidth * 0.015,
height: screenWidth * 0.015,
),
),
const SizedBox(width: 10),
Expanded(
child: Text(
label,
style: const TextStyle(
color: ColorsManager.blackColor,
fontSize: 16,
),
),
),
],
),
),
);
}
}

View File

@ -0,0 +1,46 @@
import 'package:flutter/material.dart';
import 'package:syncrow_web/pages/common/buttons/cancel_button.dart';
import 'package:syncrow_web/pages/common/buttons/default_button.dart';
import 'package:syncrow_web/utils/color_manager.dart';
class SpaceDetailsActionButtons extends StatelessWidget {
const SpaceDetailsActionButtons({
super.key,
required this.onSave,
required this.onCancel,
this.saveButtonLabel = 'OK',
this.cancelButtonLabel = 'Cancel',
});
final VoidCallback onCancel;
final VoidCallback? onSave;
final String saveButtonLabel;
final String cancelButtonLabel;
@override
Widget build(BuildContext context) {
return Row(
mainAxisSize: MainAxisSize.min,
mainAxisAlignment: MainAxisAlignment.spaceBetween,
spacing: 10,
children: [
Expanded(child: _buildCancelButton(context)),
Expanded(child: _buildSaveButton()),
],
);
}
Widget _buildCancelButton(BuildContext context) {
return CancelButton(onPressed: onCancel, label: cancelButtonLabel);
}
Widget _buildSaveButton() {
return DefaultButton(
onPressed: onSave,
borderRadius: 10,
backgroundColor: ColorsManager.secondaryColor,
foregroundColor: ColorsManager.whiteColors,
child: Text(saveButtonLabel),
);
}
}

View File

@ -0,0 +1,123 @@
import 'package:flutter/material.dart';
import 'package:flutter_svg/svg.dart';
import 'package:syncrow_web/common/edit_chip.dart';
import 'package:syncrow_web/pages/space_management_v2/modules/space_details/domain/models/space_details_model.dart';
import 'package:syncrow_web/pages/space_management_v2/modules/space_details/presentation/widgets/button_content_widget.dart';
import 'package:syncrow_web/pages/space_management_v2/modules/tags/presentation/widgets/assign_tags_dialog.dart';
import 'package:syncrow_web/utils/color_manager.dart';
import 'package:syncrow_web/utils/constants/assets.dart';
import 'package:syncrow_web/utils/enum/device_types.dart';
import 'package:syncrow_web/utils/extension/build_context_x.dart';
class SpaceDetailsDevicesBox extends StatelessWidget {
const SpaceDetailsDevicesBox({
required this.space,
super.key,
});
final SpaceDetailsModel space;
@override
Widget build(BuildContext context) {
final productAllocations = space.productAllocations;
final subspaces = space.subspaces;
final isAnySubspaceHasProductAllocations =
subspaces.any((subspace) => subspace.productAllocations.isNotEmpty);
if (productAllocations.isNotEmpty || isAnySubspaceHasProductAllocations) {
return Container(
width: double.infinity,
padding: const EdgeInsets.all(8),
decoration: BoxDecoration(
color: ColorsManager.textFieldGreyColor,
borderRadius: BorderRadius.circular(15),
border: Border.all(
color: ColorsManager.textFieldGreyColor,
width: 3.0,
),
),
child: Wrap(
spacing: 8.0,
runSpacing: 8.0,
children: [
...productAllocations.map(
(entry) => Chip(
avatar: SizedBox(
width: 24,
height: 24,
child: SvgPicture.asset(
_getDeviceIcon(entry.product.productType),
fit: BoxFit.contain,
),
),
label: Text(
entry.product.productType,
style: context.textTheme.bodySmall
?.copyWith(color: ColorsManager.spaceColor),
),
backgroundColor: ColorsManager.whiteColors,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(16),
side: const BorderSide(
color: ColorsManager.spaceColor,
),
),
),
),
EditChip(
onTap: () => _showAssignTagsDialog(context),
),
],
),
);
} else {
return TextButton(
onPressed: () => _showAssignTagsDialog(context),
style: TextButton.styleFrom(
padding: EdgeInsets.zero,
),
child: const SizedBox(
width: double.infinity,
child: ButtonContentWidget(
svgAssets: Assets.addIcon,
label: 'Add Devices',
),
),
);
}
}
void _showAssignTagsDialog(BuildContext context) {
showDialog<void>(
context: context,
builder: (context) => AssignTagsDialog(space: space),
);
}
String _getDeviceIcon(String productType) =>
switch (devicesTypesMap[productType]) {
DeviceType.LightBulb => Assets.lightBulb,
DeviceType.CeilingSensor => Assets.sensors,
DeviceType.AC => Assets.ac,
DeviceType.DoorLock => Assets.doorLock,
DeviceType.Curtain => Assets.curtain,
DeviceType.ThreeGang => Assets.gangSwitch,
DeviceType.Gateway => Assets.gateway,
DeviceType.OneGang => Assets.oneGang,
DeviceType.TwoGang => Assets.twoGang,
DeviceType.WH => Assets.waterHeater,
DeviceType.DoorSensor => Assets.openCloseDoor,
DeviceType.GarageDoor => Assets.openedDoor,
DeviceType.WaterLeak => Assets.waterLeakNormal,
DeviceType.Curtain2 => Assets.curtainIcon,
DeviceType.Blind => Assets.curtainIcon,
DeviceType.WallSensor => Assets.sensors,
DeviceType.DS => Assets.openCloseDoor,
DeviceType.OneTouch => Assets.gangSwitch,
DeviceType.TowTouch => Assets.gangSwitch,
DeviceType.ThreeTouch => Assets.gangSwitch,
DeviceType.NCPS => Assets.sensors,
DeviceType.PC => Assets.powerClamp,
DeviceType.Other => Assets.blackLogo,
null => Assets.blackLogo,
};
}

View File

@ -1,12 +1,101 @@
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.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/domain/models/space_details_model.dart';
import 'package:syncrow_web/pages/space_management_v2/modules/space_details/domain/params/load_space_details_param.dart';
import 'package:syncrow_web/pages/space_management_v2/modules/space_details/presentation/bloc/space_details_bloc.dart';
import 'package:syncrow_web/pages/space_management_v2/modules/space_details/presentation/widgets/space_details_form.dart';
import 'package:syncrow_web/utils/color_manager.dart';
import 'package:syncrow_web/utils/extension/build_context_x.dart';
class SpaceDetailsDialog extends StatelessWidget {
const SpaceDetailsDialog({super.key});
class SpaceDetailsDialog extends StatefulWidget {
const SpaceDetailsDialog({
required this.title,
required this.spaceModel,
required this.onSave,
required this.context,
super.key,
});
final Widget title;
final SpaceModel spaceModel;
final void Function(SpaceDetailsModel space) onSave;
final BuildContext context;
@override
State<SpaceDetailsDialog> createState() => _SpaceDetailsDialogState();
}
class _SpaceDetailsDialogState extends State<SpaceDetailsDialog> {
@override
void initState() {
final isCreateMode = widget.spaceModel.uuid.isEmpty;
if (!isCreateMode) {
final param = LoadSpaceDetailsParam(
spaceUuid: widget.spaceModel.uuid,
communityUuid: widget.context
.read<CommunitiesTreeSelectionBloc>()
.state
.selectedCommunity!
.uuid,
);
widget.context.read<SpaceDetailsBloc>().add(LoadSpaceDetails(param));
}
super.initState();
}
@override
Widget build(BuildContext context) {
return const Dialog(
child: Text('Create Space'),
final isCreateMode = widget.spaceModel.uuid.isEmpty;
if (isCreateMode) {
return SpaceDetailsForm(
title: widget.title,
space: SpaceDetailsModel.empty(),
onSave: widget.onSave,
);
}
return BlocBuilder<SpaceDetailsBloc, SpaceDetailsState>(
bloc: widget.context.read<SpaceDetailsBloc>(),
builder: (context, state) => switch (state) {
SpaceDetailsInitial() => _buildLoadingDialog(),
SpaceDetailsLoading() => _buildLoadingDialog(),
SpaceDetailsLoaded(:final spaceDetails) => SpaceDetailsForm(
title: widget.title,
space: spaceDetails,
onSave: widget.onSave,
),
SpaceDetailsFailure(:final errorMessage) => _buildErrorDialog(
errorMessage,
),
},
);
}
Widget _buildLoadingDialog() {
return AlertDialog(
title: widget.title,
backgroundColor: ColorsManager.whiteColors,
content: const Center(child: CircularProgressIndicator()),
);
}
Widget _buildErrorDialog(String errorMessage) {
return AlertDialog(
title: widget.title,
backgroundColor: ColorsManager.whiteColors,
content: Center(
child: SelectableText(
errorMessage,
style: context.textTheme.bodyLarge?.copyWith(
color: ColorsManager.red,
fontWeight: FontWeight.w500,
fontSize: 18,
),
),
),
);
}
}

View File

@ -0,0 +1,77 @@
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:syncrow_web/pages/space_management_v2/modules/space_details/domain/models/space_details_model.dart';
import 'package:syncrow_web/pages/space_management_v2/modules/space_details/presentation/widgets/space_details_action_buttons.dart';
import 'package:syncrow_web/pages/space_management_v2/modules/space_details/presentation/widgets/space_details_devices_box.dart';
import 'package:syncrow_web/pages/space_management_v2/modules/space_details/presentation/widgets/space_icon_picker.dart';
import 'package:syncrow_web/pages/space_management_v2/modules/space_details/presentation/widgets/space_name_text_field.dart';
import 'package:syncrow_web/pages/space_management_v2/modules/space_details/presentation/widgets/space_sub_spaces_box.dart';
import 'package:syncrow_web/pages/space_management_v2/modules/update_space/presentation/bloc/space_details_model_bloc/space_details_model_bloc.dart';
import 'package:syncrow_web/utils/color_manager.dart';
import 'package:syncrow_web/utils/extension/build_context_x.dart';
class SpaceDetailsForm extends StatelessWidget {
const SpaceDetailsForm({
required this.title,
required this.space,
required this.onSave,
super.key,
});
final Widget title;
final SpaceDetailsModel space;
final void Function(SpaceDetailsModel space) onSave;
@override
Widget build(BuildContext context) {
return BlocProvider(
create: (context) => SpaceDetailsModelBloc(initialState: space),
child: BlocBuilder<SpaceDetailsModelBloc, SpaceDetailsModel>(
buildWhen: (previous, current) => previous != current,
builder: (context, state) {
return AlertDialog(
title: title,
backgroundColor: ColorsManager.whiteColors,
content: SizedBox(
height: context.screenHeight * 0.3,
width: context.screenWidth * 0.5,
child: Row(
spacing: 20,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Expanded(child: SpaceIconPicker(iconPath: state.icon)),
Expanded(
flex: 2,
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
SpaceNameTextField(
initialValue: state.spaceName,
isNameFieldExist: (value) => state.subspaces.any(
(subspace) => subspace.name == value,
),
),
const Spacer(),
SpaceSubSpacesBox(
subspaces: state.subspaces,
),
const SizedBox(height: 16),
SpaceDetailsDevicesBox(space: state),
],
),
),
],
),
),
actions: [
SpaceDetailsActionButtons(
onSave: () => onSave(state),
onCancel: Navigator.of(context).pop,
),
],
);
}),
);
}
}

View File

@ -0,0 +1,76 @@
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_svg/svg.dart';
import 'package:syncrow_web/pages/space_management_v2/modules/space_details/presentation/widgets/space_icon_selection_dialog.dart';
import 'package:syncrow_web/pages/space_management_v2/modules/update_space/presentation/bloc/space_details_model_bloc/space_details_model_bloc.dart';
import 'package:syncrow_web/utils/color_manager.dart';
import 'package:syncrow_web/utils/constants/assets.dart';
import 'package:syncrow_web/utils/extension/build_context_x.dart';
class SpaceIconPicker extends StatelessWidget {
const SpaceIconPicker({
required this.iconPath,
super.key,
});
final String iconPath;
@override
Widget build(BuildContext context) {
return Center(
child: Stack(
alignment: Alignment.center,
children: [
Container(
width: context.screenWidth * 0.175,
height: context.screenHeight * 0.175,
decoration: const BoxDecoration(
color: ColorsManager.boxColor,
shape: BoxShape.circle,
),
padding: const EdgeInsets.all(24),
child: SvgPicture.asset(
iconPath,
width: context.screenWidth * 0.08,
height: context.screenHeight * 0.08,
),
),
Positioned.directional(
top: 12,
start: context.screenHeight * 0.06,
textDirection: Directionality.of(context),
child: InkWell(
onTap: () {
showDialog<String?>(
context: context,
builder: (context) => SpaceIconSelectionDialog(
selectedIcon: iconPath,
),
).then((value) {
if (value != null) {
if (context.mounted) {
context.read<SpaceDetailsModelBloc>().add(
UpdateSpaceDetailsIcon(value),
);
}
}
});
},
child: Container(
decoration: const BoxDecoration(
shape: BoxShape.circle,
),
child: SvgPicture.asset(
Assets.iconEdit,
width: 16,
height: 16,
),
),
),
),
],
),
);
}
}

View File

@ -0,0 +1,75 @@
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/utils/extension/build_context_x.dart';
class SpaceIconSelectionDialog extends StatelessWidget {
const SpaceIconSelectionDialog({super.key, required this.selectedIcon});
final String selectedIcon;
static const List<String> _icons = [
Assets.location,
Assets.villa,
Assets.gym,
Assets.sauna,
Assets.bbq,
Assets.building,
Assets.desk,
Assets.door,
Assets.parking,
Assets.pool,
Assets.stair,
Assets.steamRoom,
Assets.street,
Assets.unit,
];
@override
Widget build(BuildContext context) {
return AlertDialog(
title: SelectableText(
'Space Icon',
style: context.textTheme.headlineMedium,
),
backgroundColor: ColorsManager.whiteColors,
content: Container(
width: context.screenWidth * 0.45,
height: context.screenHeight * 0.275,
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: ColorsManager.boxColor,
borderRadius: BorderRadius.circular(12),
),
child: GridView.builder(
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 7,
crossAxisSpacing: 8,
mainAxisSpacing: 16,
),
itemCount: _icons.length,
itemBuilder: (context, index) {
final isSelected = selectedIcon == _icons[index];
return Container(
padding: const EdgeInsetsDirectional.all(2),
decoration: BoxDecoration(
shape: BoxShape.circle,
border: isSelected
? Border.all(color: ColorsManager.vividBlue, width: 2)
: null,
),
child: IconButton(
onPressed: () => Navigator.of(context).pop(_icons[index]),
icon: SvgPicture.asset(
_icons[index],
width: context.screenWidth * 0.03,
height: context.screenHeight * 0.08,
),
),
);
},
),
),
);
}
}

View File

@ -0,0 +1,85 @@
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:syncrow_web/pages/space_management_v2/modules/update_space/presentation/bloc/space_details_model_bloc/space_details_model_bloc.dart';
import 'package:syncrow_web/utils/color_manager.dart';
import 'package:syncrow_web/utils/extension/build_context_x.dart';
class SpaceNameTextField extends StatefulWidget {
const SpaceNameTextField({
required this.initialValue,
required this.isNameFieldExist,
super.key,
});
final String? initialValue;
final bool Function(String value) isNameFieldExist;
@override
State<SpaceNameTextField> createState() => _SpaceNameTextFieldState();
}
class _SpaceNameTextFieldState extends State<SpaceNameTextField> {
late final TextEditingController _controller;
@override
void initState() {
_controller = TextEditingController(text: widget.initialValue);
super.initState();
}
@override
void dispose() {
_controller.dispose();
super.dispose();
}
final _formKey = GlobalKey<FormState>();
String? _validateName(String? value) {
if (value == null || value.isEmpty) {
return '*Space name should not be empty.';
}
if (widget.isNameFieldExist(value)) {
return '*Name already exists';
}
return null;
}
@override
Widget build(BuildContext context) {
return Form(
key: _formKey,
autovalidateMode: AutovalidateMode.onUserInteraction,
child: TextFormField(
controller: _controller,
onChanged: (value) => context.read<SpaceDetailsModelBloc>().add(
UpdateSpaceDetailsName(value),
),
validator: _validateName,
style: context.textTheme.bodyMedium,
decoration: InputDecoration(
hintText: 'Please enter the name',
hintStyle: context.textTheme.bodyMedium!.copyWith(
color: ColorsManager.lightGrayColor,
),
filled: true,
fillColor: ColorsManager.boxColor,
enabledBorder: _buildBorder(context, ColorsManager.vividBlue),
focusedBorder: _buildBorder(context, ColorsManager.primaryColor),
errorBorder: _buildBorder(context, context.theme.colorScheme.error),
focusedErrorBorder: _buildBorder(context, context.theme.colorScheme.error),
errorStyle: context.textTheme.bodySmall?.copyWith(
color: context.theme.colorScheme.error,
),
),
),
);
}
OutlineInputBorder _buildBorder(BuildContext context, [Color? color]) {
return OutlineInputBorder(
borderRadius: BorderRadius.circular(10),
borderSide: BorderSide(width: 1, color: color ?? ColorsManager.boxColor),
);
}
}

View File

@ -0,0 +1,74 @@
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:syncrow_web/common/edit_chip.dart';
import 'package:syncrow_web/pages/space_management_v2/modules/space_details/domain/models/space_details_model.dart';
import 'package:syncrow_web/pages/space_management_v2/modules/space_details/presentation/widgets/button_content_widget.dart';
import 'package:syncrow_web/pages/space_management_v2/modules/space_details/presentation/widgets/space_sub_spaces_dialog.dart';
import 'package:syncrow_web/pages/space_management_v2/modules/space_details/presentation/widgets/subspace_name_display_widget.dart';
import 'package:syncrow_web/pages/space_management_v2/modules/update_space/presentation/bloc/space_details_model_bloc/space_details_model_bloc.dart';
import 'package:syncrow_web/utils/color_manager.dart';
import 'package:syncrow_web/utils/constants/assets.dart';
class SpaceSubSpacesBox extends StatelessWidget {
const SpaceSubSpacesBox({super.key, required this.subspaces});
final List<Subspace> subspaces;
@override
Widget build(BuildContext context) {
if (subspaces.isEmpty) {
return TextButton(
style: TextButton.styleFrom(
padding: EdgeInsets.zero,
overlayColor: ColorsManager.transparentColor,
),
onPressed: () => _showSubSpacesDialog(context),
child: const SizedBox(
width: double.infinity,
child: ButtonContentWidget(
svgAssets: Assets.addIcon,
label: 'Create Sub Spaces',
),
),
);
} else {
return Container(
padding: const EdgeInsets.all(8.0),
width: double.infinity,
decoration: BoxDecoration(
color: ColorsManager.textFieldGreyColor,
borderRadius: BorderRadius.circular(15),
border: Border.all(
color: ColorsManager.textFieldGreyColor,
width: 3.0,
),
),
child: Wrap(
spacing: 8.0,
runSpacing: 8.0,
children: [
...subspaces.map((e) => SubspaceNameDisplayWidget(subSpace: e)),
EditChip(
onTap: () => _showSubSpacesDialog(context),
),
],
),
);
}
}
void _showSubSpacesDialog(BuildContext context) {
showDialog<void>(
context: context,
barrierDismissible: false,
builder: (_) => SpaceSubSpacesDialog(
subspaces: subspaces,
onSave: (subspaces) {
context.read<SpaceDetailsModelBloc>().add(
UpdateSpaceDetailsSubspaces(subspaces),
);
},
),
);
}
}

View File

@ -0,0 +1,89 @@
import 'package:flutter/material.dart';
import 'package:syncrow_web/pages/space_management_v2/modules/space_details/domain/models/space_details_model.dart';
import 'package:syncrow_web/pages/space_management_v2/modules/space_details/presentation/widgets/space_details_action_buttons.dart';
import 'package:syncrow_web/pages/space_management_v2/modules/space_details/presentation/widgets/sub_spaces_input.dart';
import 'package:uuid/uuid.dart';
class SpaceSubSpacesDialog extends StatefulWidget {
const SpaceSubSpacesDialog({
required this.subspaces,
required this.onSave,
super.key,
});
final List<Subspace> subspaces;
final void Function(List<Subspace> subspaces) onSave;
@override
State<SpaceSubSpacesDialog> createState() => _SpaceSubSpacesDialogState();
}
class _SpaceSubSpacesDialogState extends State<SpaceSubSpacesDialog> {
late List<Subspace> _subspaces;
bool get _hasDuplicateNames =>
_subspaces.map((subspace) => subspace.name.toLowerCase()).toSet().length !=
_subspaces.length;
@override
void initState() {
super.initState();
_subspaces = List.from(widget.subspaces);
}
void _handleSubspaceAdded(String name) {
setState(() {
_subspaces = [
..._subspaces,
Subspace(
name: name,
uuid: const Uuid().v4(),
productAllocations: const [],
),
];
});
}
void _handleSubspaceDeleted(String uuid) => setState(
() => _subspaces = _subspaces.where((s) => s.uuid != uuid).toList(),
);
void _handleSave() {
widget.onSave(_subspaces);
Navigator.of(context).pop();
}
@override
Widget build(BuildContext context) {
return AlertDialog(
title: const Text('Create Sub Spaces'),
content: Column(
mainAxisSize: MainAxisSize.min,
children: [
SubSpacesInput(
subSpaces: _subspaces,
onSubspaceAdded: _handleSubspaceAdded,
onSubspaceDeleted: _handleSubspaceDeleted,
),
AnimatedSwitcher(
duration: const Duration(milliseconds: 100),
child: Visibility(
key: ValueKey(_hasDuplicateNames),
visible: _hasDuplicateNames,
child: const Text(
'Error: Duplicate subspace names are not allowed.',
style: TextStyle(color: Colors.red),
),
),
),
],
),
actions: [
SpaceDetailsActionButtons(
onSave: _hasDuplicateNames ? null : _handleSave,
onCancel: Navigator.of(context).pop,
)
],
);
}
}

View File

@ -0,0 +1,107 @@
import 'package:flutter/material.dart';
import 'package:syncrow_web/pages/space_management_v2/modules/space_details/domain/models/space_details_model.dart';
import 'package:syncrow_web/pages/space_management_v2/modules/space_details/presentation/widgets/subspace_chip.dart';
import 'package:syncrow_web/utils/color_manager.dart';
import 'package:syncrow_web/utils/extension/build_context_x.dart';
class SubSpacesInput extends StatefulWidget {
const SubSpacesInput({
super.key,
required this.subSpaces,
required this.onSubspaceAdded,
required this.onSubspaceDeleted,
});
final List<Subspace> subSpaces;
final void Function(String name) onSubspaceAdded;
final void Function(String uuid) onSubspaceDeleted;
@override
State<SubSpacesInput> createState() => _SubSpacesInputState();
}
class _SubSpacesInputState extends State<SubSpacesInput> {
late final TextEditingController _subspaceNameController;
late final FocusNode _focusNode;
@override
void initState() {
super.initState();
_subspaceNameController = TextEditingController();
_focusNode = FocusNode();
}
@override
void dispose() {
_subspaceNameController.dispose();
_focusNode.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Container(
width: context.screenWidth * 0.35,
padding: const EdgeInsets.symmetric(
vertical: 10,
horizontal: 16,
),
decoration: BoxDecoration(
color: ColorsManager.boxColor,
borderRadius: BorderRadius.circular(10),
),
child: Wrap(
spacing: 8,
runSpacing: 8,
alignment: WrapAlignment.start,
crossAxisAlignment: WrapCrossAlignment.center,
children: [
...widget.subSpaces.asMap().entries.map(
(entry) {
final index = entry.key;
final subSpace = entry.value;
final lowerName = subSpace.name.toLowerCase();
final duplicateIndices = widget.subSpaces
.asMap()
.entries
.where((e) => e.value.name.toLowerCase() == lowerName)
.map((e) => e.key)
.toList();
final isDuplicate = duplicateIndices.length > 1 &&
duplicateIndices.indexOf(index) != 0;
return SubspaceChip(
subSpace: subSpace,
isDuplicate: isDuplicate,
onDeleted: () => widget.onSubspaceDeleted(subSpace.uuid),
);
},
),
SizedBox(
width: 200,
child: TextField(
focusNode: _focusNode,
controller: _subspaceNameController,
decoration: InputDecoration(
border: InputBorder.none,
hintText: widget.subSpaces.isEmpty ? 'Please enter the name' : null,
hintStyle: context.textTheme.bodySmall?.copyWith(
color: ColorsManager.lightGrayColor,
),
),
onSubmitted: (value) {
final trimmedValue = value.trim();
if (trimmedValue.isNotEmpty) {
widget.onSubspaceAdded(trimmedValue);
_subspaceNameController.clear();
_focusNode.requestFocus();
}
},
style: context.textTheme.bodyMedium,
),
),
],
),
);
}
}

View File

@ -0,0 +1,55 @@
import 'package:flutter/material.dart';
import 'package:syncrow_web/pages/space_management_v2/modules/space_details/domain/models/space_details_model.dart';
import 'package:syncrow_web/utils/color_manager.dart';
import 'package:syncrow_web/utils/extension/build_context_x.dart';
class SubspaceChip extends StatelessWidget {
const SubspaceChip({
required this.subSpace,
required this.isDuplicate,
required this.onDeleted,
super.key,
});
final Subspace subSpace;
final bool isDuplicate;
final void Function() onDeleted;
@override
Widget build(BuildContext context) {
return Chip(
label: Text(
subSpace.name,
style: context.textTheme.bodySmall?.copyWith(
color: isDuplicate ? ColorsManager.red : ColorsManager.spaceColor,
),
),
backgroundColor: ColorsManager.whiteColors,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(10),
side: BorderSide(
color: isDuplicate ? ColorsManager.red : ColorsManager.transparentColor,
width: 0,
),
),
deleteIcon: Container(
padding: const EdgeInsetsDirectional.all(1),
decoration: BoxDecoration(
shape: BoxShape.circle,
border: Border.all(
color: ColorsManager.lightGrayColor,
width: 1.5,
),
),
child: const FittedBox(
fit: BoxFit.scaleDown,
child: Icon(
Icons.close,
color: ColorsManager.lightGrayColor,
),
),
),
onDeleted: onDeleted,
);
}
}

View File

@ -0,0 +1,171 @@
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:syncrow_web/pages/space_management_v2/modules/space_details/domain/models/space_details_model.dart';
import 'package:syncrow_web/pages/space_management_v2/modules/update_space/presentation/bloc/space_details_model_bloc/space_details_model_bloc.dart';
import 'package:syncrow_web/utils/color_manager.dart';
import 'package:syncrow_web/utils/extension/build_context_x.dart';
class SubspaceNameDisplayWidget extends StatefulWidget {
const SubspaceNameDisplayWidget({super.key, required this.subSpace});
final Subspace subSpace;
@override
State<SubspaceNameDisplayWidget> createState() =>
_SubspaceNameDisplayWidgetState();
}
class _SubspaceNameDisplayWidgetState extends State<SubspaceNameDisplayWidget> {
late final TextEditingController _controller;
late final FocusNode _focusNode;
bool _isEditing = false;
bool _hasDuplicateName = false;
@override
void initState() {
_controller = TextEditingController(text: widget.subSpace.name);
_focusNode = FocusNode();
super.initState();
}
@override
void dispose() {
_controller.dispose();
_focusNode.dispose();
super.dispose();
}
bool _checkForDuplicateName(String name) {
final bloc = context.read<SpaceDetailsModelBloc>();
return bloc.state.subspaces
.where((s) => s.uuid != widget.subSpace.uuid)
.any((s) => s.name.toLowerCase() == name.toLowerCase());
}
void _handleNameChange(String value) {
setState(() {
_hasDuplicateName = _checkForDuplicateName(value);
});
}
void _tryToFinishEditing() {
if (!_hasDuplicateName) {
_onFinishEditing();
}
}
void _tryToSubmit(String value) {
if (_hasDuplicateName) return;
final bloc = context.read<SpaceDetailsModelBloc>();
bloc.add(
UpdateSpaceDetailsSubspaces(
bloc.state.subspaces
.map(
(e) => e.uuid == widget.subSpace.uuid ? e.copyWith(name: value) : e,
)
.toList(),
),
);
_onFinishEditing();
}
@override
Widget build(BuildContext context) {
final textStyle = context.textTheme.bodySmall?.copyWith(
color: ColorsManager.spaceColor,
);
return InkWell(
onTap: () {
setState(() => _isEditing = true);
_focusNode.requestFocus();
},
child: Chip(
backgroundColor: ColorsManager.whiteColors,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(20),
side: const BorderSide(color: ColorsManager.transparentColor),
),
onDeleted: () {
final bloc = context.read<SpaceDetailsModelBloc>();
bloc.add(
UpdateSpaceDetailsSubspaces(
bloc.state.subspaces
.where((s) => s.uuid != widget.subSpace.uuid)
.toList(),
),
);
},
deleteIcon: Container(
padding: const EdgeInsetsDirectional.all(1),
decoration: BoxDecoration(
shape: BoxShape.circle,
border: Border.all(
color: ColorsManager.lightGrayColor,
width: 1.5,
),
),
child: const FittedBox(
child: Icon(
Icons.close,
color: ColorsManager.lightGrayColor,
),
),
),
label: Visibility(
visible: _isEditing,
replacement: Text(
widget.subSpace.name,
style: textStyle,
),
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
SizedBox(
width: context.screenWidth * 0.065,
height: context.screenHeight * 0.025,
child: TextField(
focusNode: _focusNode,
controller: _controller,
style: textStyle?.copyWith(
color: _hasDuplicateName ? Colors.red : null,
),
decoration: const InputDecoration.collapsed(
hintText: '',
),
onChanged: _handleNameChange,
onTapOutside: (_) => _tryToFinishEditing(),
onSubmitted: _tryToSubmit,
),
),
if (_hasDuplicateName)
AnimatedSwitcher(
duration: const Duration(milliseconds: 250),
child: Visibility(
key: ValueKey(_hasDuplicateName),
visible: _hasDuplicateName,
child: Text(
'Name already exists',
style: textStyle?.copyWith(
color: Colors.red,
fontSize: 8,
),
),
),
),
],
),
),
),
);
}
void _onFinishEditing() {
setState(() {
_isEditing = false;
_hasDuplicateName = false;
});
_focusNode.unfocus();
}
}

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