Compare commits

...

83 Commits

Author SHA1 Message Date
2b8d987c69 Add SpaceReorderDataModel and integrate drag-and-drop functionality in CommunityStructureCanvas for improved space management. 2025-07-08 16:00:57 +03:00
707cb4791f Added CreateSpaceButton for improved user interaction and updated layout calculations to utilize context extensions for better responsiveness. 2025-07-08 13:08:43 +03:00
03c45ed8d0 Refactor SpaceCardWidget: Simplified widget structure by removing unnecessary SizedBox. 2025-07-08 13:07:55 +03:00
9e0ea4ad6f Adjust spacer flex in SpaceManagementCommunityStructure widget for improved layout consistency. 2025-07-08 13:07:39 +03:00
9e6b14737f Refactor CreateSpaceButton: Changed from StatelessWidget to StatefulWidget to manage hover state and added tooltip for improved user experience. Enhanced button styling and interaction feedback for better visual cues during space creation. 2025-07-08 13:07:26 +03:00
7c2aed2d58 Refactor RemoteUpdateSpaceService: Improved error handling in updateSpace method by checking API response success before returning the updated space. This enhances robustness and ensures proper error propagation for failed updates. 2025-07-08 12:20:10 +03:00
bcf62027bc Validate UUIDs in RemoteUpdateSpaceService: Added checks for empty space and community UUIDs before constructing the update URL, improving error handling and robustness in the update space process. 2025-07-08 11:12:12 +03:00
b001713ce4 Enhance Community Structure Widgets: Updated SpaceDetailsDialogHelper to accept community UUID for space creation and editing. Refactored CreateSpaceButton and CommunityStructureHeader to pass community UUID, improving data handling and consistency across the community structure features. 2025-07-08 11:10:22 +03:00
bab3226c73 Merge branch 'dev' of https://github.com/SyncrowIOT/web into SP-1717-FE-Draw-Create-Edit-Space-Dialog 2025-07-08 10:02:12 +03:00
fa1eaa570c Refactor Space Update Logic: Introduced UpdateSpaceParam for better parameter handling in update operations. Enhanced SpaceDetailsDialogHelper to manage loading and error states during space updates. Updated RemoteUpdateSpaceService to construct dynamic URLs for space updates based on community UUID. Improved CommunitiesTreeFailureWidget UI with SelectableText and added spacing for better layout. 2025-07-08 10:01:43 +03:00
4cfb984d2c Sp 1720 fe draw assign tags to space dialog (#341)
<!--
  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-1720](https://syncrow.atlassian.net/browse/SP-1720)

## Description

Implemented products and assign tags functionality.

## 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-1720]:
https://syncrow.atlassian.net/browse/SP-1720?atlOrigin=eyJpIjoiNWRkNTljNzYxNjVmNDY3MDlhMDU5Y2ZhYzA5YTRkZjUiLCJwIjoiZ2l0aHViLWNvbS1KU1cifQ
2025-07-08 10:01:10 +03:00
4c06479469 Replaced Column with ListView in SpaceDetailsForm to enhance scrolling behavior and accommodate dynamic content. 2025-07-07 14:54:25 +03:00
3101960201 Enhance Space Management Features with Tag Assignment Improvements:
- Introduced UUID for ProductAllocation to ensure unique identification.
- Refactored AssignTagsDialog to manage tag assignments and validation more effectively, including error handling for empty tags and duplicate tag usage.
- Updated AssignTagsTable to support dynamic product allocation management and improved UI interactions.
- Enhanced AddDeviceTypeWidget to maintain selected products and handle increment/decrement actions, improving user experience during device type selection.
- Added AssignTagsErrorMessages widget for better error visibility in tag assignment process.
2025-07-07 14:26:59 +03:00
ddfd4ee153 removed print statement. 2025-07-07 14:26:39 +03:00
7f0484eec6 Add UpdateSpaceDetails Event. 2025-07-07 14:26:18 +03:00
dc7064d142 Add Factory Method for Empty Tag Instance in Tag Model. 2025-07-07 14:25:37 +03:00
e523a83912 Refactor Tags Service and Bloc for Improved Data Handling:
- Updated RemoteTagsService to remove LoadTagsParam and fetch project UUID internally, enhancing encapsulation and reducing parameter dependency.
- Modified TagsService interface to reflect the new loading method signature.
- Adjusted TagsBloc to align with the updated service method, simplifying the loading process.
- Enhanced AssignTagsTable and AddDeviceTypeWidget to utilize the new data flow, improving maintainability and user experience.
2025-07-07 10:50:03 +03:00
e917225c3d Refactor Widgets for Improved UI Consistency and Usability:
- Replaced Text with SelectableText in AddDeviceTypeWidget and AssignTagsTable for better text selection and accessibility.
- Simplified onCancel action in AssignTagsDialog for improved readability.
- Enhanced ProductsGrid layout by removing unnecessary Column widget, streamlining the widget structure for better performance and maintainability.
2025-07-07 10:36:42 +03:00
66ed30b50c Merge branch 'dev' of https://github.com/SyncrowIOT/web into SP-1720-FE-Draw-AssignTagsToSpace-Dialog 2025-07-07 10:21:14 +03:00
47bd6ff89e Refactor AddDeviceTypeWidget for Improved Error Handling and Loading States:
- Extracted loading and error handling logic into separate methods for better readability and maintainability.
- Updated UI to utilize centralized loading and failure widgets, enhancing user experience during data fetching.
2025-07-07 10:20:51 +03:00
138390496c Sp 1716 fe implement edit community service and b lo c and ensure data consistency (#340)
<!--
  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-1716](https://syncrow.atlassian.net/browse/SP-1716)

## Description

Implemented Edit Community Feature, and ensured data integrity by
reflecting changes anywhere needed immediately when changing a community
name.
Made subspaces unique and removed duplication when calling data from
API.

## 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-1716]:
https://syncrow.atlassian.net/browse/SP-1716?atlOrigin=eyJpIjoiNWRkNTljNzYxNjVmNDY3MDlhMDU5Y2ZhYzA5YTRkZjUiLCJwIjoiZ2l0aHViLWNvbS1KU1cifQ
2025-07-07 10:20:45 +03:00
df87e41d61 Refactor AddDeviceTypeWidget and ProductTypeCard Components:
- Replaced ProductTypeCard with ProductsGrid in AddDeviceTypeWidget for improved layout and maintainability.
- Converted ProductTypeCardCounter from StatefulWidget to StatelessWidget, simplifying its implementation.
- Updated ProductTypeCard to accept count and increment/decrement callbacks, enhancing its reusability and interaction.
- Introduced ProductsGrid to manage product display in a grid format, improving UI organization and responsiveness.
2025-07-07 10:15:33 +03:00
f0bfe085a4 Enhance Product Model with Icon Mapping:
- Added icon mapping functionality to the Product model, allowing dynamic icon retrieval based on product type.
- Updated ProductTypeCard to utilize the new icon property, improving UI representation and maintainability.
2025-07-07 09:34:11 +03:00
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
46a7add90d made subspaces unique and removed duplication from BE side. 2025-07-06 13:23:50 +03:00
73de1e6ff9 Enhance EditCommunityDialog: Refactor to accept parent context and streamline community update handling. Introduced a new method _onUpdateCommunitySuccess for improved readability and maintainability. Updated SpaceManagementCommunityDialogHelper to pass the parent context for better state management in the community update flow. 2025-07-06 12:52:35 +03:00
826dea8054 Implement community update endpoint: Refactor RemoteUpdateCommunityService to dynamically construct the update URL using the project UUID. This change enhances the service's flexibility and error handling by ensuring the correct endpoint is used for community updates. 2025-07-06 12:39:54 +03:00
fdea4b1cd0 Refactor CommunityDialog: Extract error message display into a separate method _buildErrorMessage for improved readability and maintainability. This change enhances the structure of the dialog while ensuring consistent error handling. 2025-07-06 12:32:18 +03:00
823d86fd80 Add loading and success snackbar helpers: Introduced showLoadingDialog and showSuccessSnackBar methods in SpaceManagementCommunityDialogHelper for consistent loading indicators and success messages across community dialogs. Updated CreateCommunityDialog and EditCommunityDialog to utilize these new helpers, enhancing user experience and maintainability. 2025-07-06 11:17:28 +03:00
dd735032ea Update SpaceSubSpacesDialog: Replace Text with SelectableText for title and error message, and add spacing in the content column to enhance user experience and maintainability. 2025-07-06 11:01:29 +03:00
6dcc851d97 Made CommunityDialog reusable for edit and create features. 2025-07-06 10:46:20 +03:00
15b36fd052 Enhance SpaceDetailsDialog: Adjust loading indicator layout for improved responsiveness. The CircularProgressIndicator is now wrapped in a SizedBox to ensure proper sizing based on screen dimensions, enhancing user experience and maintainability. 2025-07-06 09:33:27 +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
037895844a Add EnergyConsumptionPage to SmartPowerDeviceControl for enhanced energy data visualization 2025-07-01 09:44:59 +03:00
e6fe9f35b0 problem fixed should reset filters when select space or community 2025-07-01 08:41:38 +03:00
92 changed files with 4440 additions and 974 deletions

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

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

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

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

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

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

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

@ -0,0 +1,14 @@
import 'package:syncrow_web/pages/space_management_v2/modules/communities/domain/models/community_model.dart';
import 'package:syncrow_web/pages/space_management_v2/modules/communities/domain/models/space_model.dart';
class SpaceReorderDataModel {
const SpaceReorderDataModel({
required this.space,
this.parent,
this.community,
});
final SpaceModel space;
final SpaceModel? parent;
final CommunityModel? community;
}

View File

@ -1,24 +1,39 @@
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:syncrow_web/pages/space_management_v2/modules/communities/presentation/bloc/communities_bloc.dart';
import 'package:syncrow_web/pages/space_management_v2/modules/communities/presentation/communities_tree_selection_bloc/communities_tree_selection_bloc.dart';
import 'package:syncrow_web/pages/space_management_v2/modules/communities/domain/models/community_model.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/update_community/presentation/edit_community_dialog.dart';
abstract final class SpaceManagementCommunityDialogHelper {
static void showCreateDialog(BuildContext context) {
static void showCreateDialog(BuildContext context) => showDialog<void>(
context: context,
builder: (_) => const CreateCommunityDialog(),
);
static void showEditDialog(
BuildContext context,
CommunityModel community,
) {
showDialog<void>(
context: context,
builder: (_) => CreateCommunityDialog(
title: const SelectableText('Community Name'),
onCreateCommunity: (community) {
context.read<CommunitiesBloc>().add(
InsertCommunity(community),
);
context.read<CommunitiesTreeSelectionBloc>().add(
SelectCommunityEvent(community: community),
);
},
builder: (_) => EditCommunityDialog(
community: community,
parentContext: context,
),
);
}
static void showLoadingDialog(BuildContext context) => showDialog<void>(
context: context,
barrierDismissible: false,
builder: (context) => const Center(
child: CircularProgressIndicator(),
),
);
static void showSuccessSnackBar(BuildContext context, String message) =>
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(message),
),
);
}

View File

@ -1,28 +1,29 @@
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.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/pages/space_management_v2/modules/create_community/domain/param/create_community_param.dart';
import 'package:syncrow_web/pages/space_management_v2/modules/create_community/presentation/bloc/create_community_bloc.dart';
import 'package:syncrow_web/pages/space_management_v2/modules/create_community/presentation/create_community_name_text_field.dart';
import 'package:syncrow_web/utils/color_manager.dart';
import 'package:syncrow_web/utils/extension/build_context_x.dart';
class CreateCommunityDialogWidget extends StatefulWidget {
class CommunityDialog extends StatefulWidget {
final String? initialName;
final Widget title;
final void Function(String name) onSubmit;
final String? errorMessage;
const CreateCommunityDialogWidget({
super.key,
const CommunityDialog({
required this.title,
required this.onSubmit,
this.initialName,
this.errorMessage,
super.key,
});
@override
State<CreateCommunityDialogWidget> createState() =>
_CreateCommunityDialogWidgetState();
State<CommunityDialog> createState() => _CommunityDialogState();
}
class _CreateCommunityDialogWidgetState extends State<CreateCommunityDialogWidget> {
class _CommunityDialogState extends State<CommunityDialog> {
late final TextEditingController _nameController;
@override
@ -63,35 +64,20 @@ class _CreateCommunityDialogWidgetState extends State<CreateCommunityDialogWidge
child: Form(
key: _formKey,
child: SingleChildScrollView(
child: BlocBuilder<CreateCommunityBloc, CreateCommunityState>(
builder: (context, state) {
return Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
DefaultTextStyle(
style: Theme.of(context).textTheme.headlineMedium!,
child: widget.title,
),
const SizedBox(height: 18),
CreateCommunityNameTextField(
nameController: _nameController,
),
if (state case CreateCommunityFailure(:final message))
Padding(
padding: const EdgeInsets.only(top: 18),
child: SelectableText(
'* $message',
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
color: Theme.of(context).colorScheme.error,
),
),
),
const SizedBox(height: 24),
_buildActionButtons(context),
],
);
},
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
DefaultTextStyle(
style: Theme.of(context).textTheme.headlineMedium!,
child: widget.title,
),
const SizedBox(height: 18),
CreateCommunityNameTextField(nameController: _nameController),
_buildErrorMessage(),
const SizedBox(height: 24),
_buildActionButtons(context),
],
),
),
),
@ -132,13 +118,22 @@ class _CreateCommunityDialogWidgetState extends State<CreateCommunityDialogWidge
void _onSubmit(BuildContext context) {
if (_formKey.currentState?.validate() ?? false) {
context.read<CreateCommunityBloc>().add(
CreateCommunity(
CreateCommunityParam(
name: _nameController.text.trim(),
),
),
);
widget.onSubmit.call(_nameController.text.trim());
}
}
Widget _buildErrorMessage() {
return Visibility(
visible: widget.errorMessage != null,
child: Padding(
padding: const EdgeInsetsDirectional.symmetric(vertical: 18),
child: SelectableText(
'* ${widget.errorMessage}',
style: context.textTheme.bodyMedium?.copyWith(
color: context.theme.colorScheme.error,
),
),
),
);
}
}

View File

@ -7,6 +7,11 @@ 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/data/services/unique_subspaces_decorator.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 +31,18 @@ class SpaceManagementPage extends StatelessWidget {
)..add(const LoadCommunities(LoadCommunitiesParam())),
),
BlocProvider(create: (context) => CommunitiesTreeSelectionBloc()),
BlocProvider(
create: (context) => SpaceDetailsBloc(
UniqueSubspacesDecorator(
RemoteSpaceDetailsService(httpService: HTTPService()),
),
),
),
BlocProvider(
create: (context) => ProductsBloc(
RemoteProductsService(HTTPService()),
),
),
],
child: WebScaffold(
appBarTitle: Text(

View File

@ -1,13 +1,17 @@
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:syncrow_web/pages/space_management_v2/main_module/models/space_connection_model.dart';
import 'package:syncrow_web/pages/space_management_v2/main_module/models/space_reorder_data_model.dart';
import 'package:syncrow_web/pages/space_management_v2/main_module/painters/spaces_connections_arrow_painter.dart';
import 'package:syncrow_web/pages/space_management_v2/main_module/widgets/create_space_button.dart';
import 'package:syncrow_web/pages/space_management_v2/main_module/widgets/space_card_widget.dart';
import 'package:syncrow_web/pages/space_management_v2/main_module/widgets/space_cell.dart';
import 'package:syncrow_web/pages/space_management_v2/modules/communities/domain/models/community_model.dart';
import 'package:syncrow_web/pages/space_management_v2/modules/communities/domain/models/space_model.dart';
import 'package:syncrow_web/pages/space_management_v2/modules/communities/presentation/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/space_details/presentation/helpers/space_details_dialog_helper.dart';
import 'package:syncrow_web/utils/extension/build_context_x.dart';
class CommunityStructureCanvas extends StatefulWidget {
const CommunityStructureCanvas({
@ -31,8 +35,9 @@ class _CommunityStructureCanvasState extends State<CommunityStructureCanvas>
final double _horizontalSpacing = 150.0;
final double _verticalSpacing = 120.0;
late TransformationController _transformationController;
late AnimationController _animationController;
late final TransformationController _transformationController;
late final AnimationController _animationController;
SpaceReorderDataModel? _draggedData;
@override
void initState() {
@ -97,7 +102,7 @@ class _CommunityStructureCanvasState extends State<CommunityStructureCanvas>
final position = _positions[space.uuid];
if (position == null) return;
const scale = 1.5;
const scale = 1;
final viewSize = context.size;
if (viewSize == null) return;
@ -112,16 +117,33 @@ class _CommunityStructureCanvasState extends State<CommunityStructureCanvas>
_runAnimation(matrix);
}
void _onReorder(SpaceReorderDataModel data, int newIndex) {
final newCommunity = widget.community.copyWith();
final children = data.parent?.children ?? newCommunity.spaces;
final oldIndex = children.indexWhere((s) => s.uuid == data.space.uuid);
if (oldIndex != -1) {
final item = children.removeAt(oldIndex);
if (newIndex > oldIndex) {
children.insert(newIndex - 1, item);
} else {
children.insert(newIndex, item);
}
}
context.read<CommunitiesBloc>().add(
CommunitiesUpdateCommunity(newCommunity),
);
}
void _onSpaceTapped(SpaceModel? space) {
context.read<CommunitiesTreeSelectionBloc>().add(
SelectSpaceEvent(community: widget.community, space: space),
);
}
void _resetSelectionAndZoom() {
void _resetSelectionAndZoom([CommunityModel? community]) {
context.read<CommunitiesTreeSelectionBloc>().add(
SelectSpaceEvent(
community: widget.community,
community: community ?? widget.community,
space: null,
),
);
@ -182,7 +204,8 @@ class _CommunityStructureCanvasState extends State<CommunityStructureCanvas>
_positions.clear();
final community = widget.community;
_calculateLayout(community.spaces, 0, {});
final levelXOffset = <int, double>{};
_calculateLayout(community.spaces, 0, levelXOffset);
final selectedSpace = widget.selectedSpace;
final highlightedUuids = <String>{};
@ -193,7 +216,24 @@ class _CommunityStructureCanvasState extends State<CommunityStructureCanvas>
final widgets = <Widget>[];
final connections = <SpaceConnectionModel>[];
_generateWidgets(community.spaces, widgets, connections, highlightedUuids);
_generateWidgets(
widget.community.spaces,
widgets,
connections,
highlightedUuids,
community: widget.community,
);
final createButtonX = levelXOffset[0] ?? 0.0;
const createButtonY = 0.0;
widgets.add(
Positioned(
left: createButtonX,
top: createButtonY,
child: CreateSpaceButton(communityUuid: widget.community.uuid),
),
);
return [
CustomPaint(
@ -211,58 +251,178 @@ class _CommunityStructureCanvasState extends State<CommunityStructureCanvas>
List<SpaceModel> spaces,
List<Widget> widgets,
List<SpaceConnectionModel> connections,
Set<String> highlightedUuids,
) {
for (final space in spaces) {
Set<String> highlightedUuids, {
CommunityModel? community,
SpaceModel? parent,
}) {
if (spaces.isNotEmpty) {
final firstChildPos = _positions[spaces.first.uuid]!;
final targetPos = Offset(
firstChildPos.dx - (_horizontalSpacing / 4),
firstChildPos.dy,
);
widgets.add(_buildDropTarget(parent, community, 0, targetPos));
}
for (var i = 0; i < spaces.length; i++) {
final space = spaces[i];
final position = _positions[space.uuid];
if (position == null) continue;
if (position == null) {
continue;
}
final isHighlighted = highlightedUuids.contains(space.uuid);
final hasNoSelectedSpace = widget.selectedSpace == null;
final spaceCard = SpaceCardWidget(
buildSpaceContainer: () {
return Opacity(
opacity: hasNoSelectedSpace || isHighlighted ? 1.0 : 0.5,
child: Tooltip(
message: space.spaceName,
preferBelow: false,
child: SpaceCell(
onTap: () => _onSpaceTapped(space),
icon: space.icon,
name: space.spaceName,
),
),
);
},
onTap: () => SpaceDetailsDialogHelper.showCreate(
context,
communityUuid: widget.community.uuid,
),
);
final reorderData = SpaceReorderDataModel(
space: space,
parent: parent,
community: community,
);
widgets.add(
Positioned(
left: position.dx,
top: position.dy,
width: _cardWidth,
height: _cardHeight,
child: SpaceCardWidget(
buildSpaceContainer: () {
return Opacity(
opacity: hasNoSelectedSpace || isHighlighted ? 1.0 : 0.5,
child: Tooltip(
message: space.spaceName,
preferBelow: false,
child: SpaceCell(
onTap: () => _onSpaceTapped(space),
icon: space.icon,
name: space.spaceName,
),
child: Draggable<SpaceReorderDataModel>(
data: reorderData,
feedback: Material(
color: Colors.transparent,
child: Opacity(
opacity: 0.2,
child: SizedBox(
width: _cardWidth,
height: _cardHeight,
child: spaceCard,
),
);
},
onTap: () => SpaceDetailsDialogHelper.showCreate(context),
),
),
onDragStarted: () => setState(() => _draggedData = reorderData),
onDragEnd: (_) => setState(() => _draggedData = null),
onDraggableCanceled: (_, __) => setState(() => _draggedData = null),
childWhenDragging: Opacity(opacity: 0.4, child: spaceCard),
child: spaceCard,
),
),
);
final targetPos = Offset(
position.dx + _cardWidth + (_horizontalSpacing / 4) - 20,
position.dy,
);
widgets.add(_buildDropTarget(parent, community, i + 1, targetPos));
for (final child in space.children) {
connections.add(
SpaceConnectionModel(from: space.uuid, to: child.uuid),
connections.add(SpaceConnectionModel(from: space.uuid, to: child.uuid));
}
if (space.children.isNotEmpty) {
_generateWidgets(
space.children,
widgets,
connections,
highlightedUuids,
parent: space,
);
}
_generateWidgets(space.children, widgets, connections, highlightedUuids);
}
}
Widget _buildDropTarget(
SpaceModel? parent,
CommunityModel? community,
int index,
Offset position,
) {
return Positioned(
left: position.dx,
top: position.dy,
width: 40,
height: _cardHeight,
child: DragTarget<SpaceReorderDataModel>(
builder: (context, candidateData, rejectedData) {
if (_draggedData == null) {
return const SizedBox();
}
final isTargetForDragged = (_draggedData?.parent?.uuid == parent?.uuid &&
_draggedData?.community == null) ||
(_draggedData?.community?.uuid == community?.uuid &&
_draggedData?.parent == null);
if (!isTargetForDragged) {
return const SizedBox();
}
return Container(
width: 40,
height: _cardHeight,
decoration: BoxDecoration(
color: context.theme.colorScheme.primary.withValues(
alpha: candidateData.isNotEmpty ? 0.7 : 0.3,
),
borderRadius: BorderRadius.circular(8),
),
child: Icon(
Icons.add,
color: context.theme.colorScheme.onPrimary,
),
);
},
onWillAcceptWithDetails: (data) {
final children = parent?.children ?? community?.spaces ?? [];
final isSameParent = (data.data.parent?.uuid == parent?.uuid &&
data.data.community == null) ||
(data.data.community?.uuid == community?.uuid &&
data.data.parent == null);
if (!isSameParent) {
return false;
}
final oldIndex =
children.indexWhere((s) => s.uuid == data.data.space.uuid);
if (oldIndex == index || oldIndex == index - 1) {
return false;
}
return true;
},
onAcceptWithDetails: (data) => _onReorder(data.data, index),
),
);
}
@override
Widget build(BuildContext context) {
final treeWidgets = _buildTreeWidgets();
return InteractiveViewer(
transformationController: _transformationController,
boundaryMargin: EdgeInsets.symmetric(
horizontal: MediaQuery.sizeOf(context).width * 0.3,
vertical: MediaQuery.sizeOf(context).height * 0.3,
horizontal: context.screenWidth * 0.3,
vertical: context.screenHeight * 0.3,
),
minScale: 0.5,
maxScale: 3.0,
@ -270,8 +430,8 @@ class _CommunityStructureCanvasState extends State<CommunityStructureCanvas>
child: GestureDetector(
onTap: _resetSelectionAndZoom,
child: SizedBox(
width: MediaQuery.sizeOf(context).width * 5,
height: MediaQuery.sizeOf(context).height * 5,
width: context.screenWidth * 5,
height: context.screenHeight * 5,
child: Stack(children: treeWidgets),
),
),

View File

@ -0,0 +1,110 @@
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/shared/helpers/space_management_community_dialog_helper.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/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),
],
),
],
),
);
}
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: () {
SpaceManagementCommunityDialogHelper.showEditDialog(
context,
selectedCommunity,
);
},
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!,
communityUuid: selectedCommunity.uuid,
),
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

@ -2,38 +2,66 @@ import 'package:flutter/material.dart';
import 'package:syncrow_web/pages/space_management_v2/modules/space_details/presentation/helpers/space_details_dialog_helper.dart';
import 'package:syncrow_web/utils/color_manager.dart';
class CreateSpaceButton extends StatelessWidget {
const CreateSpaceButton({super.key});
class CreateSpaceButton extends StatefulWidget {
const CreateSpaceButton({
required this.communityUuid,
super.key,
});
final String communityUuid;
@override
State<CreateSpaceButton> createState() => _CreateSpaceButtonState();
}
class _CreateSpaceButtonState extends State<CreateSpaceButton> {
bool _isHovered = false;
@override
Widget build(BuildContext context) {
return GestureDetector(
onTap: () => SpaceDetailsDialogHelper.showCreate(context),
child: Container(
height: 60,
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(20),
boxShadow: [
BoxShadow(
color: Colors.grey.withValues(alpha: 0.5),
spreadRadius: 5,
blurRadius: 7,
offset: const Offset(0, 3),
),
],
return Tooltip(
margin: const EdgeInsets.symmetric(vertical: 24),
message: 'Create a new space',
child: GestureDetector(
onTap: () => SpaceDetailsDialogHelper.showCreate(
context,
communityUuid: widget.communityUuid,
),
child: Center(
child: Container(
width: 40,
height: 40,
decoration: const BoxDecoration(
color: ColorsManager.boxColor,
shape: BoxShape.circle,
),
child: const Icon(
Icons.add,
color: Colors.blue,
child: MouseRegion(
onEnter: (_) => setState(() => _isHovered = true),
onExit: (_) => setState(() => _isHovered = false),
child: AnimatedOpacity(
duration: const Duration(milliseconds: 100),
opacity: _isHovered ? 1.0 : 0.45,
child: Container(
width: 150,
height: 90,
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(20),
boxShadow: [
BoxShadow(
color: Colors.grey.withValues(alpha: 0.2),
spreadRadius: 3,
blurRadius: 8,
offset: const Offset(0, 4),
),
],
),
child: Container(
margin: const EdgeInsets.symmetric(vertical: 20),
decoration: BoxDecoration(
border: Border.all(color: ColorsManager.borderColor, width: 2),
color: ColorsManager.boxColor,
shape: BoxShape.circle,
),
child: const Center(
child: Icon(
Icons.add,
color: Colors.blue,
),
),
),
),
),
),

View File

@ -22,22 +22,20 @@ class _SpaceCardWidgetState extends State<SpaceCardWidget> {
return MouseRegion(
onEnter: (_) => setState(() => isHovered = true),
onExit: (_) => setState(() => isHovered = false),
child: SizedBox(
child: Stack(
clipBehavior: Clip.none,
alignment: Alignment.center,
children: [
widget.buildSpaceContainer(),
if (isHovered)
Positioned(
bottom: 0,
child: PlusButtonWidget(
offset: Offset.zero,
onButtonTap: widget.onTap,
),
child: Stack(
clipBehavior: Clip.none,
alignment: Alignment.center,
children: [
widget.buildSpaceContainer(),
if (isHovered)
Positioned(
bottom: 0,
child: PlusButtonWidget(
offset: Offset.zero,
onButtonTap: widget.onTap,
),
],
),
),
],
),
);
}

View File

@ -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';
@ -12,15 +13,29 @@ class SpaceManagementCommunityStructure extends StatelessWidget {
final selectionBloc = context.watch<CommunitiesTreeSelectionBloc>().state;
final selectedCommunity = selectionBloc.selectedCommunity;
final selectedSpace = selectionBloc.selectedSpace;
const spacer = Spacer(flex: 10);
const spacer = Spacer(flex: 6);
return Visibility(
visible: selectedCommunity!.spaces.isNotEmpty,
replacement: const Row(
children: [spacer, Expanded(child: CreateSpaceButton()), spacer],
replacement: Row(
children: [
spacer,
Expanded(
child: CreateSpaceButton(communityUuid: selectedCommunity.uuid),
),
spacer
],
),
child: CommunityStructureCanvas(
community: selectedCommunity,
selectedSpace: selectedSpace,
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
const CommunityStructureHeader(),
Expanded(
child: CommunityStructureCanvas(
community: selectedCommunity,
selectedSpace: selectedSpace,
),
),
],
),
);
}

View File

@ -39,6 +39,26 @@ class CommunityModel extends Equatable {
.toList();
}
CommunityModel copyWith({
String? uuid,
String? name,
DateTime? createdAt,
DateTime? updatedAt,
String? description,
String? externalId,
List<SpaceModel>? spaces,
}) {
return CommunityModel(
uuid: uuid ?? this.uuid,
name: name ?? this.name,
createdAt: createdAt ?? this.createdAt,
updatedAt: updatedAt ?? this.updatedAt,
description: description ?? this.description,
externalId: externalId ?? this.externalId,
spaces: spaces ?? this.spaces,
);
}
@override
List<Object?> get props => [uuid, name, spaces];
}

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

@ -16,6 +16,7 @@ class CommunitiesBloc extends Bloc<CommunitiesEvent, CommunitiesState> {
on<LoadCommunities>(_onLoadCommunities);
on<LoadMoreCommunities>(_onLoadMoreCommunities);
on<InsertCommunity>(_onInsertCommunity);
on<CommunitiesUpdateCommunity>(_onCommunitiesUpdateCommunity);
}
final CommunitiesService _communitiesService;
@ -114,4 +115,18 @@ class CommunitiesBloc extends Bloc<CommunitiesEvent, CommunitiesState> {
) {
emit(state.copyWith(communities: [event.community, ...state.communities]));
}
void _onCommunitiesUpdateCommunity(
CommunitiesUpdateCommunity event,
Emitter<CommunitiesState> emit,
) {
final updatedCommunities = state.communities
.map((e) => e.uuid == event.community.uuid ? event.community : e)
.toList();
emit(
state.copyWith(
communities: updatedCommunities,
),
);
}
}

View File

@ -31,3 +31,12 @@ final class InsertCommunity extends CommunitiesEvent {
@override
List<Object?> get props => [community];
}
final class CommunitiesUpdateCommunity extends CommunitiesEvent {
const CommunitiesUpdateCommunity(this.community);
final CommunityModel community;
@override
List<Object?> get props => [community];
}

View File

@ -13,14 +13,14 @@ class CommunitiesTreeFailureWidget extends StatelessWidget {
return Expanded(
child: Center(
child: Column(
spacing: 16,
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(
SelectableText(
errorMessage ?? 'Something went wrong',
textAlign: TextAlign.center,
),
const SizedBox(height: 16),
ElevatedButton(
FilledButton(
onPressed: () => context.read<CommunitiesBloc>().add(
LoadCommunities(
LoadCommunitiesParam(

View File

@ -1,57 +1,58 @@
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:syncrow_web/pages/space_management_v2/modules/communities/domain/models/community_model.dart';
import 'package:syncrow_web/pages/space_management_v2/main_module/shared/helpers/space_management_community_dialog_helper.dart';
import 'package:syncrow_web/pages/space_management_v2/main_module/shared/widgets/community_dialog.dart';
import 'package:syncrow_web/pages/space_management_v2/modules/communities/presentation/bloc/communities_bloc.dart';
import 'package:syncrow_web/pages/space_management_v2/modules/communities/presentation/communities_tree_selection_bloc/communities_tree_selection_bloc.dart';
import 'package:syncrow_web/pages/space_management_v2/modules/create_community/data/services/remote_create_community_service.dart';
import 'package:syncrow_web/pages/space_management_v2/modules/create_community/domain/param/create_community_param.dart';
import 'package:syncrow_web/pages/space_management_v2/modules/create_community/presentation/bloc/create_community_bloc.dart';
import 'package:syncrow_web/pages/space_management_v2/modules/create_community/presentation/create_community_dialog_widget.dart';
import 'package:syncrow_web/services/api/http_service.dart';
class CreateCommunityDialog extends StatelessWidget {
final void Function(CommunityModel community) onCreateCommunity;
final String? initialName;
final Widget title;
const CreateCommunityDialog({
super.key,
required this.onCreateCommunity,
required this.title,
this.initialName,
});
const CreateCommunityDialog({super.key});
@override
Widget build(BuildContext context) {
return BlocProvider(
create: (_) => CreateCommunityBloc(RemoteCreateCommunityService(HTTPService())),
child: BlocListener<CreateCommunityBloc, CreateCommunityState>(
create: (_) => CreateCommunityBloc(
RemoteCreateCommunityService(HTTPService()),
),
child: BlocConsumer<CreateCommunityBloc, CreateCommunityState>(
listener: (context, state) {
switch (state) {
case CreateCommunityLoading():
showDialog<void>(
context: context,
builder: (context) => const Center(
child: CircularProgressIndicator(),
),
);
case CreateCommunityLoading() || CreateCommunityInitial():
SpaceManagementCommunityDialogHelper.showLoadingDialog(context);
break;
case CreateCommunitySuccess(:final community):
Navigator.of(context).pop();
Navigator.of(context).pop();
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('Community created successfully')),
SpaceManagementCommunityDialogHelper.showSuccessSnackBar(
context,
'${community.name} community created successfully',
);
onCreateCommunity.call(community);
context.read<CommunitiesBloc>().add(
InsertCommunity(community),
);
context.read<CommunitiesTreeSelectionBloc>().add(
SelectCommunityEvent(community: community),
);
break;
case CreateCommunityFailure():
Navigator.of(context).pop();
break;
default:
break;
}
},
child: CreateCommunityDialogWidget(
title: title,
initialName: initialName,
),
builder: (BuildContext context, CreateCommunityState state) {
return CommunityDialog(
title: const Text('Create Community'),
initialName: null,
onSubmit: (name) => context.read<CreateCommunityBloc>().add(
CreateCommunity(CreateCommunityParam(name: name)),
),
errorMessage: state is CreateCommunityFailure ? state.message : null,
);
},
),
);
}

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

@ -1,18 +1,24 @@
import 'package:equatable/equatable.dart';
import 'package:syncrow_web/utils/constants/assets.dart';
class Product extends Equatable {
final String uuid;
final String name;
const Product({
required this.uuid,
required this.name,
required this.productType,
});
final String uuid;
final String name;
final String productType;
String get icon => _mapIconToProduct(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 +26,37 @@ class Product extends Equatable {
return {
'uuid': uuid,
'name': name,
'productType': productType,
};
}
static String _mapIconToProduct(String prodType) {
const iconMapping = {
'1G': Assets.Gang1SwitchIcon,
'1GT': Assets.oneTouchSwitch,
'2G': Assets.Gang2SwitchIcon,
'2GT': Assets.twoTouchSwitch,
'3G': Assets.Gang3SwitchIcon,
'3GT': Assets.threeTouchSwitch,
'CUR': Assets.curtain,
'CUR_2': Assets.curtain,
'GD': Assets.garageDoor,
'GW': Assets.SmartGatewayIcon,
'DL': Assets.DoorLockIcon,
'WL': Assets.waterLeakSensor,
'WH': Assets.waterHeater,
'WM': Assets.waterLeakSensor,
'SOS': Assets.sos,
'AC': Assets.ac,
'CPS': Assets.presenceSensor,
'PC': Assets.powerClamp,
'WPS': Assets.presenceSensor,
'DS': Assets.doorSensor
};
return iconMapping[prodType] ?? Assets.presenceSensor;
}
@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

@ -0,0 +1,27 @@
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/domain/services/space_details_service.dart';
class UniqueSubspacesDecorator implements SpaceDetailsService {
final SpaceDetailsService _decoratee;
const UniqueSubspacesDecorator(this._decoratee);
@override
Future<SpaceDetailsModel> getSpaceDetails(LoadSpaceDetailsParam param) async {
final response = await _decoratee.getSpaceDetails(param);
final uniqueSubspaces = <String, Subspace>{};
for (final subspace in response.subspaces) {
final normalizedName = subspace.name.trim().toLowerCase();
if (!uniqueSubspaces.containsKey(normalizedName)) {
uniqueSubspaces[normalizedName] = subspace;
}
}
return response.copyWith(
subspaces: uniqueSubspaces.values.toList(),
);
}
}

View File

@ -1,6 +1,8 @@
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';
import 'package:uuid/uuid.dart';
class SpaceDetailsModel extends Equatable {
final String uuid;
@ -17,6 +19,13 @@ 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,
@ -41,23 +50,40 @@ 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];
}
class ProductAllocation extends Equatable {
final String uuid;
final Product product;
final Tag tag;
final String? location;
const ProductAllocation({
required this.uuid,
required this.product,
required this.tag,
this.location,
});
factory ProductAllocation.fromJson(Map<String, dynamic> json) {
return ProductAllocation(
uuid: json['uuid'] as String? ?? const Uuid().v4(),
product: Product.fromJson(json['product'] as Map<String, dynamic>),
tag: Tag.fromJson(json['tag'] as Map<String, dynamic>),
);
@ -65,13 +91,26 @@ class ProductAllocation extends Equatable {
Map<String, dynamic> toJson() {
return {
'uuid': uuid,
'product': product.toJson(),
'tag': tag.toJson(),
};
}
ProductAllocation copyWith({
String? uuid,
Product? product,
Tag? tag,
}) {
return ProductAllocation(
uuid: uuid ?? this.uuid,
product: product ?? this.product,
tag: tag ?? this.tag,
);
}
@override
List<Object?> get props => [product, tag];
List<Object?> get props => [uuid, product, tag];
}
class Subspace extends Equatable {
@ -88,7 +127,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 +142,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,127 @@
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/domain/models/space_details_model.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/pages/space_management_v2/modules/update_space/data/services/remote_update_space_service.dart';
import 'package:syncrow_web/pages/space_management_v2/modules/update_space/domain/params/update_space_param.dart';
import 'package:syncrow_web/pages/space_management_v2/modules/update_space/presentation/bloc/update_space_bloc.dart';
import 'package:syncrow_web/services/api/http_service.dart';
abstract final class SpaceDetailsDialogHelper {
static void showCreate(BuildContext context) {
static void showCreate(
BuildContext context, {
required String communityUuid,
}) {
showDialog<void>(
context: context,
builder: (context) => const SpaceDetailsDialog(),
builder: (_) => MultiBlocProvider(
providers: [
BlocProvider(
create: (context) => SpaceDetailsBloc(
RemoteSpaceDetailsService(httpService: HTTPService()),
),
),
BlocProvider(
create: (context) => UpdateSpaceBloc(
RemoteUpdateSpaceService(HTTPService()),
),
),
],
child: Builder(
builder: (context) => SpaceDetailsDialog(
context: context,
title: const SelectableText('Create Space'),
spaceModel: SpaceModel.empty(),
onSave: (space) {},
communityUuid: communityUuid,
),
),
),
);
}
static void showEdit(
BuildContext context, {
required SpaceModel spaceModel,
required String communityUuid,
}) {
showDialog<void>(
context: context,
builder: (_) => MultiBlocProvider(
providers: [
BlocProvider(
create: (context) => SpaceDetailsBloc(
RemoteSpaceDetailsService(httpService: HTTPService()),
),
),
BlocProvider(
create: (context) => UpdateSpaceBloc(
RemoteUpdateSpaceService(HTTPService()),
),
),
],
child: Builder(
builder: (context) => BlocListener<UpdateSpaceBloc, UpdateSpaceState>(
listener: _updateListener,
child: SpaceDetailsDialog(
context: context,
title: const SelectableText('Edit Space'),
spaceModel: spaceModel,
onSave: (space) => context.read<UpdateSpaceBloc>().add(
UpdateSpace(
UpdateSpaceParam(
communityUuid: communityUuid,
space: space,
),
),
),
communityUuid: communityUuid,
),
),
),
),
);
}
static void _updateListener(BuildContext context, UpdateSpaceState state) {
return switch (state) {
UpdateSpaceInitial() => null,
UpdateSpaceLoading() => _onLoading(context),
UpdateSpaceSuccess(:final space) => _onUpdateSuccess(context, space),
UpdateSpaceFailure(:final errorMessage) => _onError(context, errorMessage),
};
}
static void _onUpdateSuccess(BuildContext context, SpaceDetailsModel space) {
Navigator.of(context).pop();
}
static void _onLoading(BuildContext context) {
showDialog<void>(
context: context,
barrierDismissible: false,
builder: (_) => const Center(child: CircularProgressIndicator()),
);
}
static void _onError(BuildContext context, String errorMessage) {
Navigator.of(context).pop();
showDialog<void>(
context: context,
barrierDismissible: false,
builder: (_) => AlertDialog(
title: const Text('Error'),
content: Text(errorMessage),
actions: [
TextButton(
onPressed: Navigator.of(context).pop,
child: const Text('OK'),
),
],
),
);
}
}

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,139 @@
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.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/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/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 allAllocations = [
...space.productAllocations,
...space.subspaces.expand((s) => s.productAllocations),
];
if (allAllocations.isNotEmpty) {
final productCounts = <String, int>{};
for (final allocation in allAllocations) {
final productType = allocation.product.productType;
productCounts[productType] = (productCounts[productType] ?? 0) + 1;
}
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: [
...productCounts.entries.map((entry) {
final productType = entry.key;
final count = entry.value;
return Chip(
avatar: SizedBox(
width: 24,
height: 24,
child: SvgPicture.asset(
_getDeviceIcon(productType),
fit: BoxFit.contain,
),
),
label: Text(
'x$count',
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<SpaceDetailsModel>(
context: context,
builder: (context) => AssignTagsDialog(space: space),
).then((resultSpace) {
if (resultSpace != null) {
if (context.mounted) {
context.read<SpaceDetailsModelBloc>().add(UpdateSpaceDetails(resultSpace));
}
}
});
}
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,102 @@
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/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,
required this.communityUuid,
super.key,
});
final Widget title;
final SpaceModel spaceModel;
final void Function(SpaceDetailsModel space) onSave;
final BuildContext context;
final String communityUuid;
@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.communityUuid,
);
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: SizedBox(
height: context.screenHeight * 0.3,
width: context.screenWidth * 0.5,
child: 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,76 @@
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, space) {
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: space.icon)),
Expanded(
flex: 2,
child: ListView(
shrinkWrap: true,
children: [
SpaceNameTextField(
initialValue: space.spaceName,
isNameFieldExist: (value) => space.subspaces.any(
(subspace) => subspace.name == value,
),
),
const SizedBox(height: 32),
SpaceSubSpacesBox(
subspaces: space.subspaces,
),
const SizedBox(height: 16),
SpaceDetailsDevicesBox(space: space),
],
),
),
],
),
),
actions: [
SpaceDetailsActionButtons(
onSave: () => onSave(space),
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,90 @@
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 SelectableText('Create Sub Spaces'),
content: Column(
spacing: 12,
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 SelectableText(
'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();
}
}

View File

@ -1,10 +1,9 @@
import 'package:dio/dio.dart';
import 'package:syncrow_web/pages/common/bloc/project_manager.dart';
import 'package:syncrow_web/pages/space_management_v2/modules/tags/domain/models/tag.dart';
import 'package:syncrow_web/pages/space_management_v2/modules/tags/domain/params/load_tags_param.dart';
import 'package:syncrow_web/pages/space_management_v2/modules/tags/domain/services/tags_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';
final class RemoteTagsService implements TagsService {
const RemoteTagsService(this._httpService);
@ -14,17 +13,10 @@ final class RemoteTagsService implements TagsService {
static const _defaultErrorMessage = 'Failed to load tags';
@override
Future<List<Tag>> loadTags(LoadTagsParam param) async {
if (param.projectUuid == null) {
throw Exception('Project UUID is required');
}
Future<List<Tag>> loadTags() async {
try {
final response = await _httpService.get(
path: ApiEndpoints.listTags.replaceAll(
'{projectUuid}',
param.projectUuid!,
),
path: await _makeUrl(),
expectedResponseModel: (json) {
final result = json as Map<String, dynamic>;
final data = result['data'] as List<dynamic>;
@ -46,4 +38,12 @@ final class RemoteTagsService implements TagsService {
throw APIException(formattedErrorMessage);
}
}
Future<String> _makeUrl() async {
final projectUuid = await ProjectManager.getProjectUUID();
if (projectUuid == null || projectUuid.isEmpty) {
throw APIException('Project UUID is required');
}
return '/projects/$projectUuid/tags';
}
}

View File

@ -13,6 +13,13 @@ class Tag extends Equatable {
required this.updatedAt,
});
factory Tag.empty() => const Tag(
uuid: '',
name: '',
createdAt: '',
updatedAt: '',
);
factory Tag.fromJson(Map<String, dynamic> json) {
return Tag(
uuid: json['uuid'] as String,

View File

@ -1,6 +1,5 @@
import 'package:syncrow_web/pages/space_management_v2/modules/tags/domain/models/tag.dart';
import 'package:syncrow_web/pages/space_management_v2/modules/tags/domain/params/load_tags_param.dart';
abstract interface class TagsService {
Future<List<Tag>> loadTags(LoadTagsParam param);
Future<List<Tag>> loadTags();
}

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/tags/domain/models/tag.dart';
import 'package:syncrow_web/pages/space_management_v2/modules/tags/domain/params/load_tags_param.dart';
import 'package:syncrow_web/pages/space_management_v2/modules/tags/domain/services/tags_service.dart';
import 'package:syncrow_web/services/api/api_exception.dart';
@ -21,7 +20,7 @@ class TagsBloc extends Bloc<TagsEvent, TagsState> {
) async {
emit(TagsLoading());
try {
final tags = await _tagsService.loadTags(event.param);
final tags = await _tagsService.loadTags();
emit(TagsLoaded(tags));
} on APIException catch (e) {
emit(TagsFailure(e.message));

View File

@ -8,10 +8,5 @@ abstract class TagsEvent extends Equatable {
}
class LoadTags extends TagsEvent {
final LoadTagsParam param;
const LoadTags(this.param);
@override
List<Object?> get props => [param];
const LoadTags();
}

View File

@ -0,0 +1,100 @@
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_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/domain/models/product.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/presentation/widgets/space_details_action_buttons.dart';
import 'package:syncrow_web/pages/space_management_v2/modules/tags/presentation/widgets/products_grid.dart';
import 'package:syncrow_web/services/api/http_service.dart';
import 'package:syncrow_web/utils/color_manager.dart';
import 'package:syncrow_web/utils/extension/build_context_x.dart';
class AddDeviceTypeWidget extends StatefulWidget {
const AddDeviceTypeWidget({super.key});
@override
State<AddDeviceTypeWidget> createState() => _AddDeviceTypeWidgetState();
}
class _AddDeviceTypeWidgetState extends State<AddDeviceTypeWidget> {
final Map<Product, int> _selectedProducts = {};
void _onIncrement(Product product) {
setState(() {
_selectedProducts[product] = (_selectedProducts[product] ?? 0) + 1;
});
}
void _onDecrement(Product product) {
setState(() {
if ((_selectedProducts[product] ?? 0) > 0) {
_selectedProducts[product] = _selectedProducts[product]! - 1;
if (_selectedProducts[product] == 0) {
_selectedProducts.remove(product);
}
}
});
}
@override
Widget build(BuildContext context) {
return BlocProvider(
create: (_) => ProductsBloc(RemoteProductsService(HTTPService()))
..add(const LoadProducts()),
child: Builder(
builder: (context) => AlertDialog(
title: const SelectableText('Add Devices'),
backgroundColor: ColorsManager.whiteColors,
content: BlocBuilder<ProductsBloc, ProductsState>(
builder: (context, state) => switch (state) {
ProductsInitial() || ProductsLoading() => _buildLoading(context),
ProductsLoaded(:final products) => ProductsGrid(
products: products,
selectedProducts: _selectedProducts,
onIncrement: _onIncrement,
onDecrement: _onDecrement,
),
ProductsFailure(:final errorMessage) => _buildFailure(
context,
errorMessage,
),
},
),
actions: [
SpaceDetailsActionButtons(
onSave: () {
final result = _selectedProducts.entries
.expand((entry) => List.generate(entry.value, (_) => entry.key))
.toList();
Navigator.of(context).pop(result);
},
onCancel: Navigator.of(context).pop,
saveButtonLabel: 'Next',
),
],
),
),
);
}
Widget _buildLoading(BuildContext context) => SizedBox(
width: context.screenWidth * 0.9,
height: context.screenHeight * 0.65,
child: const Center(child: CircularProgressIndicator()),
);
Widget _buildFailure(BuildContext context, String errorMessage) {
return SizedBox(
width: context.screenWidth * 0.9,
height: context.screenHeight * 0.65,
child: Center(
child: SelectableText(
errorMessage,
style: context.textTheme.bodyMedium?.copyWith(
color: context.theme.colorScheme.error,
),
),
),
);
}
}

View File

@ -0,0 +1,231 @@
import 'package:flutter/material.dart';
import 'package:syncrow_web/pages/space_management_v2/modules/products/domain/models/product.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/tags/domain/models/tag.dart';
import 'package:syncrow_web/pages/space_management_v2/modules/tags/presentation/widgets/add_device_type_widget.dart';
import 'package:syncrow_web/pages/space_management_v2/modules/tags/presentation/widgets/assign_tags_error_messages.dart';
import 'package:syncrow_web/pages/space_management_v2/modules/tags/presentation/widgets/assign_tags_table.dart';
import 'package:syncrow_web/utils/extension/build_context_x.dart';
import 'package:uuid/uuid.dart';
class AssignTagsDialog extends StatefulWidget {
const AssignTagsDialog({required this.space, super.key});
final SpaceDetailsModel space;
@override
State<AssignTagsDialog> createState() => _AssignTagsDialogState();
}
class _AssignTagsDialogState extends State<AssignTagsDialog> {
late SpaceDetailsModel _space;
final Map<String, String> _validationErrors = {};
@override
void initState() {
super.initState();
_space = widget.space.copyWith(
productAllocations:
widget.space.productAllocations.map((e) => e.copyWith()).toList(),
subspaces: widget.space.subspaces
.map(
(s) => s.copyWith(
productAllocations:
s.productAllocations.map((e) => e.copyWith()).toList(),
),
)
.toList(),
);
_validateAllTags();
}
void _validateAllTags() {
final newErrors = <String, String>{};
final allAllocations = [
..._space.productAllocations,
..._space.subspaces.expand((s) => s.productAllocations),
];
final allocationsByProductType = <String, List<ProductAllocation>>{};
for (final allocation in allAllocations) {
(allocationsByProductType[allocation.product.productType] ??= [])
.add(allocation);
}
for (final productType in allocationsByProductType.keys) {
final allocations = allocationsByProductType[productType]!;
final tagCounts = <String, int>{};
for (final allocation in allocations) {
final tagName = allocation.tag.name.trim().toLowerCase();
if (tagName.isEmpty) {
newErrors[allocation.uuid] =
'Tag for ${allocation.product.name} cannot be empty.';
} else {
tagCounts[tagName] = (tagCounts[tagName] ?? 0) + 1;
}
}
for (final allocation in allocations) {
final tagName = allocation.tag.name.trim().toLowerCase();
if (tagName.isNotEmpty && (tagCounts[tagName] ?? 0) > 1) {
newErrors[allocation.uuid] =
'Tag "${allocation.tag.name}" is used by multiple $productType devices.';
}
}
}
setState(() {
_validationErrors
..clear()
..addAll(newErrors);
});
}
void _handleTagChange(String allocationUuid, Tag newTag) {
setState(() {
var index =
_space.productAllocations.indexWhere((pa) => pa.uuid == allocationUuid);
if (index != -1) {
final allocation = _space.productAllocations[index];
_space.productAllocations[index] = allocation.copyWith(tag: newTag);
} else {
for (final subspace in _space.subspaces) {
index = subspace.productAllocations
.indexWhere((pa) => pa.uuid == allocationUuid);
if (index != -1) {
final allocation = subspace.productAllocations[index];
subspace.productAllocations[index] = allocation.copyWith(tag: newTag);
break;
}
}
}
});
_validateAllTags();
}
void _handleLocationChange(String allocationUuid, String? newSubspaceUuid) {
setState(() {
ProductAllocation? allocationToMove;
var index =
_space.productAllocations.indexWhere((pa) => pa.uuid == allocationUuid);
if (index != -1) {
allocationToMove = _space.productAllocations.removeAt(index);
} else {
for (final subspace in _space.subspaces) {
index = subspace.productAllocations
.indexWhere((pa) => pa.uuid == allocationUuid);
if (index != -1) {
allocationToMove = subspace.productAllocations.removeAt(index);
break;
}
}
}
if (allocationToMove == null) return;
if (newSubspaceUuid == null) {
_space.productAllocations.add(allocationToMove);
} else {
_space.subspaces
.firstWhere((s) => s.uuid == newSubspaceUuid)
.productAllocations
.add(allocationToMove);
}
});
}
void _handleProductDelete(String allocationUuid) {
setState(() {
_space.productAllocations.removeWhere((pa) => pa.uuid == allocationUuid);
for (final subspace in _space.subspaces) {
subspace.productAllocations.removeWhere(
(pa) => pa.uuid == allocationUuid,
);
}
});
_validateAllTags();
}
@override
Widget build(BuildContext context) {
final allProductAllocations = [
..._space.productAllocations,
..._space.subspaces.expand((s) => s.productAllocations),
];
final productLocations = <String, String?>{};
for (final pa in _space.productAllocations) {
productLocations[pa.uuid] = null;
}
for (final subspace in _space.subspaces) {
for (final pa in subspace.productAllocations) {
productLocations[pa.uuid] = subspace.uuid;
}
}
final hasErrors = _validationErrors.isNotEmpty;
return AlertDialog(
title: const SelectableText('Assign Tags'),
content: ConstrainedBox(
constraints: BoxConstraints(
maxWidth: context.screenWidth * 0.6,
minWidth: context.screenWidth * 0.6,
maxHeight: context.screenHeight * 0.8,
),
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
SizedBox(
width: double.infinity,
child: AssignTagsTable(
productAllocations: allProductAllocations,
subspaces: _space.subspaces,
productLocations: productLocations,
onTagSelected: _handleTagChange,
onLocationSelected: _handleLocationChange,
onProductDeleted: _handleProductDelete,
),
),
if (hasErrors)
AssignTagsErrorMessages(
errorMessages: _validationErrors.values.toSet().toList(),
),
],
),
),
actions: [
SpaceDetailsActionButtons(
onSave: hasErrors ? null : () => Navigator.of(context).pop(_space),
onCancel: () async {
final newProducts = await showDialog<List<Product>>(
context: context,
builder: (context) => const AddDeviceTypeWidget(),
);
if (newProducts == null || newProducts.isEmpty) return;
setState(() {
for (final product in newProducts) {
_space.productAllocations.add(
ProductAllocation(
uuid: const Uuid().v4(),
product: product,
tag: Tag.empty(),
),
);
}
});
_validateAllTags();
},
cancelButtonLabel: 'Add New Device',
)
],
);
}
}

View File

@ -0,0 +1,29 @@
import 'package:flutter/material.dart';
import 'package:syncrow_web/utils/extension/build_context_x.dart';
class AssignTagsErrorMessages extends StatelessWidget {
const AssignTagsErrorMessages({super.key, required this.errorMessages});
final List<String> errorMessages;
@override
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.only(top: 16),
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: errorMessages
.map(
(error) => Text(
'- $error',
style: context.textTheme.bodyMedium?.copyWith(
color: context.theme.colorScheme.error,
),
),
)
.toList(),
),
);
}
}

View File

@ -0,0 +1,204 @@
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:syncrow_web/common/dialog_dropdown.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/tags/data/services/remote_tags_service.dart';
import 'package:syncrow_web/pages/space_management_v2/modules/tags/domain/models/tag.dart';
import 'package:syncrow_web/pages/space_management_v2/modules/tags/presentation/bloc/tags_bloc.dart';
import 'package:syncrow_web/pages/space_management_v2/modules/tags/presentation/widgets/product_tag_field.dart';
import 'package:syncrow_web/services/api/http_service.dart';
import 'package:syncrow_web/utils/color_manager.dart';
import 'package:syncrow_web/utils/extension/build_context_x.dart';
class AssignTagsTable extends StatelessWidget {
const AssignTagsTable({
required this.productAllocations,
required this.subspaces,
required this.productLocations,
required this.onTagSelected,
required this.onLocationSelected,
required this.onProductDeleted,
super.key,
});
final List<ProductAllocation> productAllocations;
final List<Subspace> subspaces;
final Map<String, String?> productLocations;
final void Function(String, Tag) onTagSelected;
final void Function(String, String?) onLocationSelected;
final void Function(String) onProductDeleted;
DataColumn _buildDataColumn(BuildContext context, String label) {
return DataColumn(
label: SelectableText(label, style: context.textTheme.bodyMedium),
);
}
@override
Widget build(BuildContext context) {
return BlocProvider<TagsBloc>(
create: (BuildContext context) => TagsBloc(
RemoteTagsService(HTTPService()),
)..add(const LoadTags()),
child: BlocBuilder<TagsBloc, TagsState>(
builder: (context, state) {
return switch (state) {
TagsLoading() || TagsInitial() => const Center(
child: CircularProgressIndicator(),
),
TagsFailure(:final message) => Center(
child: Text(message),
),
TagsLoaded(:final tags) => ClipRRect(
borderRadius: BorderRadius.circular(20),
child: DataTable(
headingRowColor: WidgetStateProperty.all(
ColorsManager.dataHeaderGrey,
),
key: ValueKey(productAllocations.length),
border: TableBorder.all(
color: ColorsManager.dataHeaderGrey,
width: 1,
borderRadius: BorderRadius.circular(20),
),
columns: [
_buildDataColumn(context, '#'),
_buildDataColumn(context, 'Device'),
_buildDataColumn(context, 'Tag'),
_buildDataColumn(context, 'Location'),
],
rows: productAllocations.isEmpty
? [
DataRow(
cells: [
DataCell(
Center(
child: SelectableText(
'No Devices Available',
style: context.textTheme.bodyMedium?.copyWith(
color: ColorsManager.lightGrayColor,
),
),
),
),
DataCell.empty,
DataCell.empty,
DataCell.empty,
],
),
]
: List.generate(productAllocations.length, (index) {
final productAllocation = productAllocations[index];
final allocationUuid = productAllocation.uuid;
final availableTags = tags
.where(
(tag) =>
!productAllocations
.where((p) =>
p.product.productType ==
productAllocation.product.productType)
.map((p) => p.tag.name.toLowerCase())
.contains(tag.name.toLowerCase()) ||
tag.uuid == productAllocation.tag.uuid,
)
.toList();
final currentLocationUuid =
productLocations[allocationUuid];
final currentLocationName = currentLocationUuid == null
? 'Main Space'
: subspaces
.firstWhere((s) => s.uuid == currentLocationUuid)
.name;
return DataRow(
key: ValueKey(allocationUuid),
cells: [
DataCell(Text((index + 1).toString())),
DataCell(
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Expanded(
child: Text(
productAllocation.product.name,
overflow: TextOverflow.ellipsis,
)),
const SizedBox(width: 10),
Container(
width: 20,
height: 20,
decoration: BoxDecoration(
shape: BoxShape.circle,
border: Border.all(
color: ColorsManager.lightGrayColor,
width: 1,
),
),
child: IconButton(
icon: const Icon(
Icons.close,
color: ColorsManager.lightGreyColor,
size: 16,
),
onPressed: () {
onProductDeleted(allocationUuid);
},
tooltip: 'Delete Tag',
padding: EdgeInsets.zero,
constraints: const BoxConstraints(),
),
),
],
),
),
DataCell(
Container(
alignment: Alignment.centerLeft,
width: double.infinity,
child: ProductTagField(
key: ValueKey('dropdown_$allocationUuid'),
productName: productAllocation.product.uuid,
initialValue: productAllocation.tag,
onSelected: (newTag) {
onTagSelected(allocationUuid, newTag);
},
items: availableTags,
),
),
),
DataCell(
SizedBox(
width: double.infinity,
child: DialogDropdown(
items: [
'Main Space',
...subspaces.map((s) => s.name)
],
selectedValue: currentLocationName,
onSelected: (newLocationName) {
final newSubspaceUuid = newLocationName ==
'Main Space'
? null
: subspaces
.firstWhere(
(s) => s.name == newLocationName)
.uuid;
onLocationSelected(
allocationUuid, newSubspaceUuid);
},
)),
),
],
);
}),
),
),
_ => const SizedBox.shrink(),
};
},
),
);
}
}

View File

@ -0,0 +1,186 @@
import 'package:flutter/material.dart';
import 'package:syncrow_web/pages/space_management_v2/modules/tags/domain/models/tag.dart';
import 'package:syncrow_web/utils/color_manager.dart';
import 'package:syncrow_web/utils/extension/build_context_x.dart';
class ProductTagField extends StatefulWidget {
final List<Tag> items;
final ValueChanged<Tag> onSelected;
final Tag? initialValue;
final String productName;
const ProductTagField({
super.key,
required this.items,
required this.onSelected,
this.initialValue,
required this.productName,
});
@override
State<ProductTagField> createState() => _ProductTagFieldState();
}
class _ProductTagFieldState extends State<ProductTagField> {
bool _isOpen = false;
OverlayEntry? _overlayEntry;
final TextEditingController _controller = TextEditingController();
final FocusNode _focusNode = FocusNode();
@override
void initState() {
super.initState();
_controller.text = widget.initialValue?.name ?? '';
_focusNode.addListener(_handleFocusChange);
}
@override
void dispose() {
_focusNode.removeListener(_handleFocusChange);
_controller.dispose();
_focusNode.dispose();
_overlayEntry?.remove();
_overlayEntry = null;
super.dispose();
}
void _handleFocusChange() {
if (!_focusNode.hasFocus) {
_submit(_controller.text);
}
}
void _submit(String value) {
final lowerCaseValue = value.toLowerCase();
final selectedTag = widget.items.firstWhere(
(tag) => tag.name.toLowerCase() == lowerCaseValue,
orElse: () => Tag(
name: value,
uuid: '',
createdAt: '',
updatedAt: '',
),
);
widget.onSelected(selectedTag);
_closeDropdown();
}
void _toggleDropdown() {
if (_isOpen) {
_closeDropdown();
} else {
_openDropdown();
}
}
void _openDropdown() {
_overlayEntry = _createOverlayEntry();
Overlay.of(context).insert(_overlayEntry!);
setState(() => _isOpen = true);
}
void _closeDropdown() {
if (_isOpen) {
_overlayEntry?.remove();
_overlayEntry = null;
setState(() => _isOpen = false);
}
}
@override
Widget build(BuildContext context) {
return GestureDetector(
onTap: () => FocusScope.of(context).unfocus(),
behavior: HitTestBehavior.opaque,
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 12.0, vertical: 8.0),
decoration: BoxDecoration(
border: Border.all(color: ColorsManager.transparentColor),
borderRadius: BorderRadius.circular(8.0),
),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Expanded(
child: TextFormField(
controller: _controller,
focusNode: _focusNode,
onFieldSubmitted: _submit,
style: context.textTheme.bodyMedium,
decoration: const InputDecoration(
hintText: 'Enter or Select a tag',
border: InputBorder.none,
),
),
),
GestureDetector(
onTap: _toggleDropdown,
child: const Icon(Icons.arrow_drop_down),
),
],
),
),
);
}
OverlayEntry _createOverlayEntry() {
final renderBox = context.findRenderObject()! as RenderBox;
final size = renderBox.size;
final offset = renderBox.localToGlobal(Offset.zero);
return OverlayEntry(
builder: (context) {
return GestureDetector(
onTap: _closeDropdown,
behavior: HitTestBehavior.translucent,
child: Stack(
children: [
Positioned(
left: offset.dx,
top: offset.dy + size.height,
width: size.width,
child: Material(
elevation: 4.0,
child: Container(
color: ColorsManager.whiteColors,
constraints: const BoxConstraints(maxHeight: 200.0),
child: ListView.builder(
shrinkWrap: true,
itemCount: widget.items.length,
itemBuilder: (context, index) {
final tag = widget.items[index];
return Container(
decoration: const BoxDecoration(
border: Border(
bottom: BorderSide(
color: ColorsManager.lightGrayBorderColor,
width: 1.0,
),
),
),
child: ListTile(
title: Text(
tag.name,
style: context.textTheme.bodyMedium?.copyWith(
color: ColorsManager.textPrimaryColor,
),
),
onTap: () {
_controller.text = tag.name;
_submit(tag.name);
_closeDropdown();
},
),
);
},
),
),
),
),
],
),
);
},
);
}
}

View File

@ -0,0 +1,67 @@
import 'package:flutter/material.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/presentation/widgets/product_type_card_counter.dart';
import 'package:syncrow_web/pages/spaces_management/tag_model/widgets/device_icon_widget.dart';
import 'package:syncrow_web/utils/color_manager.dart';
import 'package:syncrow_web/utils/extension/build_context_x.dart';
class ProductTypeCard extends StatelessWidget {
const ProductTypeCard({
required this.product,
required this.count,
required this.onIncrement,
required this.onDecrement,
super.key,
});
final Product product;
final int count;
final void Function() onIncrement;
final void Function() onDecrement;
@override
Widget build(BuildContext context) {
return Card(
elevation: 2,
color: ColorsManager.whiteColors,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8),
),
child: Padding(
padding: const EdgeInsets.all(4),
child: Column(
spacing: 8,
mainAxisAlignment: MainAxisAlignment.start,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Expanded(child: DeviceIconWidget(icon: product.icon)),
_buildName(context, product.name),
ProductTypeCardCounter(
onIncrement: onIncrement,
onDecrement: onDecrement,
count: count,
),
const SizedBox(height: 4),
],
),
),
);
}
Widget _buildName(BuildContext context, String name) {
return Expanded(
child: SizedBox(
height: 35,
child: Text(
name,
style: context.textTheme.bodySmall?.copyWith(
color: ColorsManager.blackColor,
),
textAlign: TextAlign.center,
maxLines: 2,
overflow: TextOverflow.ellipsis,
),
),
);
}
}

View File

@ -0,0 +1,58 @@
import 'package:flutter/material.dart';
import 'package:syncrow_web/utils/color_manager.dart';
import 'package:syncrow_web/utils/extension/build_context_x.dart';
class ProductTypeCardCounter extends StatelessWidget {
const ProductTypeCardCounter({
super.key,
required this.onIncrement,
required this.onDecrement,
required this.count,
});
final int count;
final void Function() onIncrement;
final void Function() onDecrement;
@override
Widget build(BuildContext context) {
return Container(
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 5),
decoration: BoxDecoration(
color: ColorsManager.counterBackgroundColor,
borderRadius: BorderRadius.circular(20),
),
child: Row(
mainAxisSize: MainAxisSize.min,
spacing: 8,
children: [
_buildCounterButton(
Icons.remove,
onDecrement,
),
Text(
count.toString(),
style: context.textTheme.bodyLarge?.copyWith(
color: ColorsManager.spaceColor,
),
),
_buildCounterButton(Icons.add, onIncrement),
],
),
);
}
Widget _buildCounterButton(
IconData icon,
VoidCallback onPressed,
) {
return GestureDetector(
onTap: onPressed,
child: Icon(
icon,
color: ColorsManager.spaceColor.withValues(alpha: 0.3),
size: 18,
),
);
}
}

View File

@ -0,0 +1,62 @@
import 'package:flutter/material.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/presentation/widgets/product_type_card.dart';
import 'package:syncrow_web/utils/color_manager.dart';
import 'package:syncrow_web/utils/extension/build_context_x.dart';
class ProductsGrid extends StatelessWidget {
const ProductsGrid({
required this.products,
required this.selectedProducts,
required this.onIncrement,
required this.onDecrement,
super.key,
});
final List<Product> products;
final Map<Product, int> selectedProducts;
final void Function(Product) onIncrement;
final void Function(Product) onDecrement;
@override
Widget build(BuildContext context) {
final size = MediaQuery.of(context).size;
final crossAxisCount = switch (context.screenWidth) {
> 1200 => 8,
> 800 => 5,
_ => 3,
};
return SingleChildScrollView(
child: Container(
width: size.width * 0.9,
height: size.height * 0.65,
decoration: BoxDecoration(
color: ColorsManager.textFieldGreyColor,
borderRadius: BorderRadius.circular(8),
),
child: GridView.builder(
padding: const EdgeInsets.symmetric(
horizontal: 20,
),
shrinkWrap: true,
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: crossAxisCount,
mainAxisSpacing: 6,
crossAxisSpacing: 4,
childAspectRatio: 0.8,
),
itemCount: products.length,
itemBuilder: (context, index) {
final product = products[index];
return ProductTypeCard(
product: product,
count: selectedProducts[product] ?? 0,
onIncrement: () => onIncrement(product),
onDecrement: () => onDecrement(product),
);
},
),
),
);
}
}

View File

@ -1,6 +1,6 @@
import 'package:dio/dio.dart';
import 'package:syncrow_web/pages/common/bloc/project_manager.dart';
import 'package:syncrow_web/pages/space_management_v2/modules/communities/domain/models/community_model.dart';
import 'package:syncrow_web/pages/space_management_v2/modules/update_community/domain/params/update_community_param.dart';
import 'package:syncrow_web/pages/space_management_v2/modules/update_community/domain/services/update_community_service.dart';
import 'package:syncrow_web/services/api/api_exception.dart';
import 'package:syncrow_web/services/api/http_service.dart';
@ -13,15 +13,15 @@ class RemoteUpdateCommunityService implements UpdateCommunityService {
static const _defaultErrorMessage = 'Failed to update community';
@override
Future<CommunityModel> updateCommunity(UpdateCommunityParam param) async {
Future<CommunityModel> updateCommunity(CommunityModel param) async {
final endpoint = await _makeUrl(param.uuid);
try {
final response = await _httpService.put(
path: 'endpoint',
expectedResponseModel: (data) => CommunityModel.fromJson(
data as Map<String, dynamic>,
),
await _httpService.put(
path: endpoint,
body: {'name': param.name},
expectedResponseModel: (data) => null,
);
return response;
return param;
} on DioException catch (e) {
final message = e.response?.data as Map<String, dynamic>?;
final error = message?['error'] as Map<String, dynamic>?;
@ -36,4 +36,12 @@ class RemoteUpdateCommunityService implements UpdateCommunityService {
throw APIException(formattedErrorMessage);
}
}
Future<String> _makeUrl(String communityUuid) async {
final projectUuid = await ProjectManager.getProjectUUID();
if (projectUuid == null) {
throw APIException('Project UUID is not set');
}
return '/projects/$projectUuid/communities/$communityUuid';
}
}

View File

@ -1,10 +0,0 @@
import 'package:equatable/equatable.dart';
class UpdateCommunityParam extends Equatable {
const UpdateCommunityParam({required this.name});
final String name;
@override
List<Object> get props => [name];
}

View File

@ -1,6 +1,5 @@
import 'package:syncrow_web/pages/space_management_v2/modules/communities/domain/models/community_model.dart';
import 'package:syncrow_web/pages/space_management_v2/modules/update_community/domain/params/update_community_param.dart';
abstract class UpdateCommunityService {
Future<CommunityModel> updateCommunity(UpdateCommunityParam param);
Future<CommunityModel> updateCommunity(CommunityModel community);
}

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/communities/domain/models/community_model.dart';
import 'package:syncrow_web/pages/space_management_v2/modules/update_community/domain/params/update_community_param.dart';
import 'package:syncrow_web/pages/space_management_v2/modules/update_community/domain/services/update_community_service.dart';
import 'package:syncrow_web/services/api/api_exception.dart';
@ -24,7 +23,7 @@ class UpdateCommunityBloc extends Bloc<UpdateCommunityEvent, UpdateCommunityStat
emit(UpdateCommunityLoading());
try {
final updatedCommunity = await _updateCommunityService.updateCommunity(
event.param,
event.communityModel,
);
emit(UpdateCommunitySuccess(updatedCommunity));
} on APIException catch (e) {

View File

@ -8,10 +8,10 @@ sealed class UpdateCommunityEvent extends Equatable {
}
final class UpdateCommunity extends UpdateCommunityEvent {
const UpdateCommunity(this.param);
const UpdateCommunity(this.communityModel);
final UpdateCommunityParam param;
final CommunityModel communityModel ;
@override
List<Object> get props => [param];
List<Object> get props => [communityModel];
}

View File

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

View File

@ -0,0 +1,71 @@
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:syncrow_web/pages/space_management_v2/main_module/shared/helpers/space_management_community_dialog_helper.dart';
import 'package:syncrow_web/pages/space_management_v2/main_module/shared/widgets/community_dialog.dart';
import 'package:syncrow_web/pages/space_management_v2/modules/communities/domain/models/community_model.dart';
import 'package:syncrow_web/pages/space_management_v2/modules/communities/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/update_community/data/services/remote_update_community_service.dart';
import 'package:syncrow_web/pages/space_management_v2/modules/update_community/presentation/bloc/update_community_bloc.dart';
import 'package:syncrow_web/services/api/http_service.dart';
class EditCommunityDialog extends StatelessWidget {
const EditCommunityDialog({
required this.community,
required this.parentContext,
super.key,
});
final CommunityModel community;
final BuildContext parentContext;
@override
Widget build(BuildContext context) {
return BlocProvider(
create: (_) => UpdateCommunityBloc(
RemoteUpdateCommunityService(HTTPService()),
),
child: BlocConsumer<UpdateCommunityBloc, UpdateCommunityState>(
listener: (context, state) {
switch (state) {
case UpdateCommunityInitial() || UpdateCommunityLoading():
SpaceManagementCommunityDialogHelper.showLoadingDialog(context);
break;
case UpdateCommunitySuccess(:final community):
_onUpdateCommunitySuccess(context, community);
break;
case UpdateCommunityFailure():
Navigator.of(context).pop();
break;
}
},
builder: (context, state) => CommunityDialog(
title: const Text('Edit Community'),
initialName: community.name,
errorMessage: state is UpdateCommunityFailure ? state.errorMessage : null,
onSubmit: (name) => context.read<UpdateCommunityBloc>().add(
UpdateCommunity(community.copyWith(name: name)),
),
),
),
);
}
void _onUpdateCommunitySuccess(
BuildContext context,
CommunityModel community,
) {
Navigator.of(context).pop();
Navigator.of(context).pop();
SpaceManagementCommunityDialogHelper.showSuccessSnackBar(
context,
'${community.name} community updated successfully',
);
parentContext.read<CommunitiesBloc>().add(
CommunitiesUpdateCommunity(community),
);
parentContext.read<CommunitiesTreeSelectionBloc>().add(
SelectCommunityEvent(community: community),
);
}
}

View File

@ -1,5 +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/update_space/domain/params/update_space_param.dart';
import 'package:syncrow_web/pages/space_management_v2/modules/update_space/domain/services/update_space_service.dart';
import 'package:syncrow_web/services/api/api_exception.dart';
import 'package:syncrow_web/services/api/http_service.dart';
@ -12,17 +14,23 @@ class RemoteUpdateSpaceService implements UpdateSpaceService {
static const _defaultErrorMessage = 'Failed to update space';
@override
Future<SpaceDetailsModel> updateSpace(SpaceDetailsModel space) async {
Future<SpaceDetailsModel> updateSpace(UpdateSpaceParam param) async {
try {
final response = await _httpService.put(
path: 'endpoint',
body: space.toJson(),
expectedResponseModel: (data) => SpaceDetailsModel.fromJson(
data as Map<String, dynamic>,
),
final path = await _makeUrl(param);
await _httpService.put(
path: path,
body: param.space.toJson(),
expectedResponseModel: (data) {
final response = data as Map<String, dynamic>;
final isSuccess = response['success'] as bool;
if (!isSuccess) {
throw APIException(response['error'] as String);
}
return isSuccess;
},
);
return response;
return param.space;
} on DioException catch (e) {
final message = e.response?.data as Map<String, dynamic>?;
final error = message?['error'] as Map<String, dynamic>?;
@ -37,4 +45,23 @@ class RemoteUpdateSpaceService implements UpdateSpaceService {
throw APIException(formattedErrorMessage);
}
}
Future<String> _makeUrl(UpdateSpaceParam param) async {
final projectUuid = await ProjectManager.getProjectUUID();
if (projectUuid == null || projectUuid.isEmpty) {
throw APIException('Project UUID is not set');
}
final spaceUuid = param.space.uuid;
if (spaceUuid.isEmpty) {
throw APIException('Space UUID is not set');
}
final communityUuid = param.communityUuid;
if (communityUuid.isEmpty) {
throw APIException('Community UUID is not set');
}
return '/projects/$projectUuid/communities/$communityUuid/spaces/$spaceUuid';
}
}

View File

@ -0,0 +1,45 @@
import 'package:syncrow_web/pages/space_management_v2/modules/space_details/domain/models/space_details_model.dart';
class UpdateSpaceParam {
UpdateSpaceParam({
required this.space,
required this.communityUuid,
});
final SpaceDetailsModel space;
final String communityUuid;
Map<String, dynamic> toJson() {
return {
'spaceName': space.spaceName,
'icon': space.icon,
'subspaces': space.subspaces
.map(
(e) => {
'subspaceName': e.name,
'productAllocations': e.productAllocations
.map(
(e) => {
'name': e.tag.name,
'productUuid': e.product.uuid,
'uuid': e.uuid,
},
)
.toList(),
'uuid': e.uuid,
},
)
.toList(),
'productAllocations': space.productAllocations
.map(
(e) => {
'tagName': e.tag.name,
'tagUuid': e.tag.uuid,
'productUuid': e.product.uuid,
},
)
.toList(),
'spaceModelUuid': space.uuid,
};
}
}

View File

@ -1,5 +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/update_space/domain/params/update_space_param.dart';
abstract class UpdateSpaceService {
Future<SpaceDetailsModel> updateSpace(SpaceDetailsModel space);
abstract interface class UpdateSpaceService {
Future<SpaceDetailsModel> updateSpace(UpdateSpaceParam param);
}

View File

@ -0,0 +1,53 @@
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';
part 'space_details_model_event.dart';
class SpaceDetailsModelBloc extends Bloc<SpaceDetailsModelEvent, SpaceDetailsModel> {
SpaceDetailsModelBloc({
required SpaceDetailsModel initialState,
}) : super(initialState) {
on<UpdateSpaceDetailsIcon>(_onUpdateSpaceDetailsIcon);
on<UpdateSpaceDetailsName>(_onUpdateSpaceDetailsName);
on<UpdateSpaceDetailsSubspaces>(_onUpdateSpaceDetailsSubspaces);
on<UpdateSpaceDetailsProductAllocations>(
_onUpdateSpaceDetailsProductAllocations);
on<UpdateSpaceDetails>(_onUpdateSpaceDetails);
}
void _onUpdateSpaceDetailsIcon(
UpdateSpaceDetailsIcon event,
Emitter<SpaceDetailsModel> emit,
) {
emit(state.copyWith(icon: event.icon));
}
void _onUpdateSpaceDetailsName(
UpdateSpaceDetailsName event,
Emitter<SpaceDetailsModel> emit,
) {
emit(state.copyWith(spaceName: event.name));
}
void _onUpdateSpaceDetailsSubspaces(
UpdateSpaceDetailsSubspaces event,
Emitter<SpaceDetailsModel> emit,
) {
emit(state.copyWith(subspaces: event.subspaces));
}
void _onUpdateSpaceDetailsProductAllocations(
UpdateSpaceDetailsProductAllocations event,
Emitter<SpaceDetailsModel> emit,
) {
emit(state.copyWith(productAllocations: event.productAllocations));
}
void _onUpdateSpaceDetails(
UpdateSpaceDetails event,
Emitter<SpaceDetailsModel> emit,
) {
emit(event.space);
}
}

View File

@ -0,0 +1,53 @@
part of 'space_details_model_bloc.dart';
sealed class SpaceDetailsModelEvent extends Equatable {
const SpaceDetailsModelEvent();
@override
List<Object> get props => [];
}
final class UpdateSpaceDetailsIcon extends SpaceDetailsModelEvent {
const UpdateSpaceDetailsIcon(this.icon);
final String icon;
@override
List<Object> get props => [icon];
}
final class UpdateSpaceDetailsName extends SpaceDetailsModelEvent {
const UpdateSpaceDetailsName(this.name);
final String name;
@override
List<Object> get props => [name];
}
final class UpdateSpaceDetailsSubspaces extends SpaceDetailsModelEvent {
const UpdateSpaceDetailsSubspaces(this.subspaces);
final List<Subspace> subspaces;
@override
List<Object> get props => [subspaces];
}
final class UpdateSpaceDetailsProductAllocations extends SpaceDetailsModelEvent {
const UpdateSpaceDetailsProductAllocations(this.productAllocations);
final List<ProductAllocation> productAllocations;
@override
List<Object> get props => [productAllocations];
}
final class UpdateSpaceDetails extends SpaceDetailsModelEvent {
const UpdateSpaceDetails(this.space);
final SpaceDetailsModel space;
@override
List<Object> get props => [space];
}

View File

@ -1,6 +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/update_space/domain/params/update_space_param.dart';
import 'package:syncrow_web/pages/space_management_v2/modules/update_space/domain/services/update_space_service.dart';
import 'package:syncrow_web/services/api/api_exception.dart';
@ -20,7 +21,7 @@ class UpdateSpaceBloc extends Bloc<UpdateSpaceEvent, UpdateSpaceState> {
) async {
emit(UpdateSpaceLoading());
try {
final updatedSpace = await _updateSpaceService.updateSpace(event.space);
final updatedSpace = await _updateSpaceService.updateSpace(event.param);
emit(UpdateSpaceSuccess(updatedSpace));
} on APIException catch (e) {
emit(UpdateSpaceFailure(e.message));

View File

@ -8,10 +8,10 @@ sealed class UpdateSpaceEvent extends Equatable {
}
final class UpdateSpace extends UpdateSpaceEvent {
const UpdateSpace(this.space);
const UpdateSpace(this.param);
final SpaceDetailsModel space;
final UpdateSpaceParam param;
@override
List<Object> get props => [space];
List<Object> get props => [param];
}

View File

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

View File

@ -21,7 +21,6 @@ import 'package:syncrow_web/utils/snack_bar.dart';
class VisitorPasswordBloc
extends Bloc<VisitorPasswordEvent, VisitorPasswordState> {
VisitorPasswordBloc() : super(VisitorPasswordInitial()) {
on<SelectUsageFrequency>(selectUsageFrequency);
on<FetchDevice>(_onFetchDevice);
@ -87,6 +86,9 @@ class VisitorPasswordBloc
SelectTimeVisitorPassword event,
Emitter<VisitorPasswordState> emit,
) async {
// Ensure expirationTimeTimeStamp has a value
effectiveTimeTimeStamp ??= DateTime.now().millisecondsSinceEpoch ~/ 1000;
final DateTime? picked = await showDatePicker(
context: event.context,
initialDate: DateTime.now(),
@ -94,86 +96,124 @@ class VisitorPasswordBloc
lastDate: DateTime.now().add(const Duration(days: 5095)),
);
if (picked != null) {
final TimeOfDay? timePicked = await showTimePicker(
context: event.context,
initialTime: TimeOfDay.now(),
builder: (context, child) {
return Theme(
data: ThemeData.light().copyWith(
colorScheme: const ColorScheme.light(
primary: ColorsManager.primaryColor,
onSurface: Colors.black,
),
buttonTheme: const ButtonThemeData(
colorScheme: ColorScheme.light(
primary: Colors.green,
),
),
if (picked == null) return;
final TimeOfDay? timePicked = await showTimePicker(
context: event.context,
initialTime: TimeOfDay.now(),
builder: (context, child) {
return Theme(
data: ThemeData.light().copyWith(
colorScheme: const ColorScheme.light(
primary: ColorsManager.primaryColor,
onSurface: Colors.black,
),
child: child!,
);
},
);
if (timePicked != null) {
final selectedDateTime = DateTime(
picked.year,
picked.month,
picked.day,
timePicked.hour,
timePicked.minute,
),
child: child!,
);
},
);
final selectedTimestamp =
selectedDateTime.millisecondsSinceEpoch ~/ 1000;
if (timePicked == null) return;
if (event.isStart) {
if (expirationTimeTimeStamp != null &&
selectedTimestamp > expirationTimeTimeStamp!) {
CustomSnackBar.displaySnackBar(
final selectedDateTime = DateTime(
picked.year,
picked.month,
picked.day,
timePicked.hour,
timePicked.minute,
);
final selectedTimestamp = selectedDateTime.millisecondsSinceEpoch ~/ 1000;
final currentTimestamp = DateTime.now().millisecondsSinceEpoch ~/ 1000;
if (event.isStart) {
// START TIME VALIDATION
if (expirationTimeTimeStamp != null &&
selectedTimestamp > expirationTimeTimeStamp!) {
await showDialog<void>(
context: event.context,
builder: (context) => AlertDialog(
title: const Text(
'Effective Time cannot be later than Expiration Time.',
);
return;
}
if(selectedTimestamp < DateTime.now().millisecondsSinceEpoch ~/ 1000) {
if(selectedTimestamp < DateTime.now().millisecondsSinceEpoch ~/ 1000) {
await showDialog<void>(
context: event.context,
builder: (context) => AlertDialog(
title: const Text('Effective Time cannot be earlier than current time.'),
actionsAlignment: MainAxisAlignment.center,
content:
FilledButton(
onPressed: () {
Navigator.of(event.context).pop();
add(SelectTimeVisitorPassword(context: event.context, isStart: true, isRepeat: false));
},
child: const Text('OK'),
),
),
);
}
return;
}
effectiveTimeTimeStamp = selectedTimestamp;
startTimeAccess = selectedDateTime.toString().split('.').first;
} else {
if (effectiveTimeTimeStamp != null &&
selectedTimestamp < effectiveTimeTimeStamp!) {
CustomSnackBar.displaySnackBar(
'Expiration Time cannot be earlier than Effective Time.',
);
return;
}
expirationTimeTimeStamp = selectedTimestamp;
endTimeAccess = selectedDateTime.toString().split('.').first;
}
emit(ChangeTimeState());
emit(VisitorPasswordInitial());
),
actionsAlignment: MainAxisAlignment.center,
content: FilledButton(
onPressed: () {
Navigator.of(event.context).pop();
add(SelectTimeVisitorPassword(
context: event.context,
isStart: true,
isRepeat: false,
));
},
child: const Text('OK'),
),
),
);
return;
}
if (selectedTimestamp < currentTimestamp) {
await showDialog<void>(
context: event.context,
builder: (context) => AlertDialog(
title: const Text(
'Effective Time cannot be earlier than current time.',
),
actionsAlignment: MainAxisAlignment.center,
content: FilledButton(
onPressed: () {
Navigator.of(event.context).pop();
add(SelectTimeVisitorPassword(
context: event.context,
isStart: true,
isRepeat: false,
));
},
child: const Text('OK'),
),
),
);
return;
}
// Save effective time
effectiveTimeTimeStamp = selectedTimestamp;
startTimeAccess = selectedDateTime.toString().split('.').first;
} else {
// END TIME VALIDATION
if (effectiveTimeTimeStamp != null &&
selectedTimestamp < effectiveTimeTimeStamp!) {
await showDialog<void>(
context: event.context,
builder: (context) => AlertDialog(
title: const Text(
'Expiration Time cannot be earlier than Effective Time.',
),
actionsAlignment: MainAxisAlignment.center,
content: FilledButton(
onPressed: () {
Navigator.of(event.context).pop();
add(SelectTimeVisitorPassword(
context: event.context,
isStart: false,
isRepeat: false,
));
},
child: const Text('OK'),
),
),
);
return;
}
// Save expiration time
expirationTimeTimeStamp = selectedTimestamp;
endTimeAccess = selectedDateTime.toString().split('.').first;
}
emit(ChangeTimeState());
emit(VisitorPasswordInitial());
}
bool toggleRepeat(
@ -213,7 +253,7 @@ class VisitorPasswordBloc
FetchDevice event, Emitter<VisitorPasswordState> emit) async {
try {
emit(DeviceLoaded());
final projectUuid = await ProjectManager.getProjectUUID() ?? '';
final projectUuid = await ProjectManager.getProjectUUID() ?? '';
data = await AccessMangApi().fetchDoorLockDeviceList(projectUuid);
emit(TableLoaded(data));

View File

@ -14,7 +14,8 @@ class Assets {
static const String rightLine = 'assets/images/right_line.png';
static const String google = 'assets/images/google.svg';
static const String facebook = 'assets/images/facebook.svg';
static const String invisiblePassword = 'assets/images/Password_invisible.svg';
static const String invisiblePassword =
'assets/images/Password_invisible.svg';
static const String visiblePassword = 'assets/images/password_visible.svg';
static const String accessIcon = 'assets/images/access_icon.svg';
static const String spaseManagementIcon =
@ -33,7 +34,8 @@ class Assets {
static const String emptyTable = 'assets/images/empty_table.svg';
// General assets
static const String motionlessDetection = 'assets/icons/motionless_detection.svg';
static const String motionlessDetection =
'assets/icons/motionless_detection.svg';
static const String acHeating = 'assets/icons/ac_heating.svg';
static const String acPowerOff = 'assets/icons/ac_power_off.svg';
static const String acFanMiddle = 'assets/icons/ac_fan_middle.svg';
@ -70,19 +72,22 @@ class Assets {
'assets/icons/automation_functions/temp_password_unlock.svg';
static const String doorlockNormalOpen =
'assets/icons/automation_functions/doorlock_normal_open.svg';
static const String doorbell = 'assets/icons/automation_functions/doorbell.svg';
static const String doorbell =
'assets/icons/automation_functions/doorbell.svg';
static const String remoteUnlockViaApp =
'assets/icons/automation_functions/remote_unlock_via_app.svg';
static const String doubleLock =
'assets/icons/automation_functions/double_lock.svg';
static const String selfTestResult =
'assets/icons/automation_functions/self_test_result.svg';
static const String lockAlarm = 'assets/icons/automation_functions/lock_alarm.svg';
static const String lockAlarm =
'assets/icons/automation_functions/lock_alarm.svg';
static const String presenceState =
'assets/icons/automation_functions/presence_state.svg';
static const String currentTemp =
'assets/icons/automation_functions/current_temp.svg';
static const String presence = 'assets/icons/automation_functions/presence.svg';
static const String presence =
'assets/icons/automation_functions/presence.svg';
static const String residualElectricity =
'assets/icons/automation_functions/residual_electricity.svg';
static const String hijackAlarm =
@ -99,12 +104,15 @@ class Assets {
// Presence Sensor Assets
static const String sensorMotionIcon = 'assets/icons/sensor_motion_ic.svg';
static const String sensorPresenceIcon = 'assets/icons/sensor_presence_ic.svg';
static const String sensorPresenceIcon =
'assets/icons/sensor_presence_ic.svg';
static const String sensorVacantIcon = 'assets/icons/sensor_vacant_ic.svg';
static const String illuminanceRecordIcon =
'assets/icons/illuminance_record_ic.svg';
static const String presenceRecordIcon = 'assets/icons/presence_record_ic.svg';
static const String helpDescriptionIcon = 'assets/icons/help_description_ic.svg';
static const String presenceRecordIcon =
'assets/icons/presence_record_ic.svg';
static const String helpDescriptionIcon =
'assets/icons/help_description_ic.svg';
static const String lightPulp = 'assets/icons/light_pulb.svg';
static const String acDevice = 'assets/icons/ac_device.svg';
@ -158,10 +166,12 @@ class Assets {
static const String unit = 'assets/icons/unit_icon.svg';
static const String villa = 'assets/icons/villa_icon.svg';
static const String iconEdit = 'assets/icons/icon_edit_icon.svg';
static const String textFieldSearch = 'assets/icons/textfield_search_icon.svg';
static const String textFieldSearch =
'assets/icons/textfield_search_icon.svg';
static const String roundedAddIcon = 'assets/icons/rounded_add_icon.svg';
static const String addIcon = 'assets/icons/add_icon.svg';
static const String smartThermostatIcon = 'assets/icons/smart_thermostat_icon.svg';
static const String smartThermostatIcon =
'assets/icons/smart_thermostat_icon.svg';
static const String smartLightIcon = 'assets/icons/smart_light_icon.svg';
static const String presenceSensor = 'assets/icons/presence_sensor.svg';
static const String Gang3SwitchIcon = 'assets/icons/3_Gang_switch_icon.svg';
@ -209,7 +219,8 @@ class Assets {
//assets/icons/water_leak_normal.svg
static const String waterLeakNormal = 'assets/icons/water_leak_normal.svg';
//assets/icons/water_leak_detected.svg
static const String waterLeakDetected = 'assets/icons/water_leak_detected.svg';
static const String waterLeakDetected =
'assets/icons/water_leak_detected.svg';
//assets/icons/automation_records.svg
static const String automationRecords = 'assets/icons/automation_records.svg';
@ -280,13 +291,16 @@ class Assets {
'assets/icons/functions_icons/sensitivity.svg';
static const String assetsSensitivityOperationIcon =
'assets/icons/functions_icons/sesitivity_operation_icon.svg';
static const String assetsAcPower = 'assets/icons/functions_icons/ac_power.svg';
static const String assetsAcPower =
'assets/icons/functions_icons/ac_power.svg';
static const String assetsAcPowerOFF =
'assets/icons/functions_icons/ac_power_off.svg';
static const String assetsChildLock =
'assets/icons/functions_icons/child_lock.svg';
static const String assetsFreezing = 'assets/icons/functions_icons/freezing.svg';
static const String assetsFanSpeed = 'assets/icons/functions_icons/fan_speed.svg';
static const String assetsFreezing =
'assets/icons/functions_icons/freezing.svg';
static const String assetsFanSpeed =
'assets/icons/functions_icons/fan_speed.svg';
static const String assetsAcCooling =
'assets/icons/functions_icons/ac_cooling.svg';
static const String assetsAcHeating =
@ -295,7 +309,8 @@ class Assets {
'assets/icons/functions_icons/celsius_degrees.svg';
static const String assetsTempreture =
'assets/icons/functions_icons/tempreture.svg';
static const String assetsAcFanLow = 'assets/icons/functions_icons/ac_fan_low.svg';
static const String assetsAcFanLow =
'assets/icons/functions_icons/ac_fan_low.svg';
static const String assetsAcFanMiddle =
'assets/icons/functions_icons/ac_fan_middle.svg';
static const String assetsAcFanHigh =
@ -314,7 +329,8 @@ class Assets {
'assets/icons/functions_icons/far_detection.svg';
static const String assetsFarDetectionFunction =
'assets/icons/functions_icons/far_detection_function.svg';
static const String assetsIndicator = 'assets/icons/functions_icons/indicator.svg';
static const String assetsIndicator =
'assets/icons/functions_icons/indicator.svg';
static const String assetsMotionDetection =
'assets/icons/functions_icons/motion_detection.svg';
static const String assetsMotionlessDetection =
@ -327,7 +343,8 @@ class Assets {
'assets/icons/functions_icons/master_state.svg';
static const String assetsSwitchAlarmSound =
'assets/icons/functions_icons/switch_alarm_sound.svg';
static const String assetsResetOff = 'assets/icons/functions_icons/reset_off.svg';
static const String assetsResetOff =
'assets/icons/functions_icons/reset_off.svg';
// Assets for automation_functions
static const String assetsCardUnlock =
@ -371,13 +388,15 @@ class Assets {
static const String activeUser = 'assets/icons/active_user.svg';
static const String deActiveUser = 'assets/icons/deactive_user.svg';
static const String invitedIcon = 'assets/icons/invited_icon.svg';
static const String rectangleCheckBox = 'assets/icons/rectangle_check_box.png';
static const String rectangleCheckBox =
'assets/icons/rectangle_check_box.png';
static const String CheckBoxChecked = 'assets/icons/box_checked.png';
static const String emptyBox = 'assets/icons/empty_box.png';
static const String completeProcessIcon =
'assets/icons/compleate_process_icon.svg';
static const String completedDoneIcon = 'assets/images/completed_done.svg';
static const String currentProcessIcon = 'assets/icons/current_process_icon.svg';
static const String currentProcessIcon =
'assets/icons/current_process_icon.svg';
static const String uncomplete_ProcessIcon =
'assets/icons/uncompleate_process_icon.svg';
static const String wrongProcessIcon = 'assets/icons/wrong_process_icon.svg';
@ -398,9 +417,11 @@ class Assets {
static const String successIcon = 'assets/icons/success_icon.svg';
static const String spaceLocationIcon = 'assets/icons/spaseLocationIcon.svg';
static const String scenesPlayIcon = 'assets/icons/scenesPlayIcon.png';
static const String scenesPlayIconCheck = 'assets/icons/scenesPlayIconCheck.png';
static const String scenesPlayIconCheck =
'assets/icons/scenesPlayIconCheck.png';
static const String presenceStateIcon = 'assets/icons/presence_state.svg';
static const String currentDistanceIcon = 'assets/icons/current_distance_icon.svg';
static const String currentDistanceIcon =
'assets/icons/current_distance_icon.svg';
static const String farDetectionIcon = 'assets/icons/far_detection_icon.svg';
static const String motionDetectionSensitivityIcon =
@ -423,29 +444,44 @@ class Assets {
static const String cpsMode4 = 'assets/icons/cps_mode4.svg';
static const String closeToMotion = 'assets/icons/close_to_motion.svg';
static const String farAwayMotion = 'assets/icons/far_away_motion.svg';
static const String communicationFault = 'assets/icons/communication_fault.svg';
static const String communicationFault =
'assets/icons/communication_fault.svg';
static const String radarFault = 'assets/icons/radar_fault.svg';
static const String selfTestingSuccess = 'assets/icons/self_testing_success.svg';
static const String selfTestingFailure = 'assets/icons/self_testing_failure.svg';
static const String selfTestingTimeout = 'assets/icons/self_testing_timeout.svg';
static const String selfTestingSuccess =
'assets/icons/self_testing_success.svg';
static const String selfTestingFailure =
'assets/icons/self_testing_failure.svg';
static const String selfTestingTimeout =
'assets/icons/self_testing_timeout.svg';
static const String movingSpeed = 'assets/icons/moving_speed.svg';
static const String boundary = 'assets/icons/boundary.svg';
static const String motionMeter = 'assets/icons/motion_meter.svg';
static const String spatialStaticValue = 'assets/icons/spatial_static_value.svg';
static const String spatialMotionValue = 'assets/icons/spatial_motion_value.svg';
static const String spatialStaticValue =
'assets/icons/spatial_static_value.svg';
static const String spatialMotionValue =
'assets/icons/spatial_motion_value.svg';
static const String presenceJudgementThrshold =
'assets/icons/presence_judgement_threshold.svg';
static const String spaceType = 'assets/icons/space_type.svg';
static const String sportsPara = 'assets/icons/sports_para.svg';
static const String sensitivityFeature1 = 'assets/icons/sensitivity_feature_1.svg';
static const String sensitivityFeature2 = 'assets/icons/sensitivity_feature_2.svg';
static const String sensitivityFeature3 = 'assets/icons/sensitivity_feature_3.svg';
static const String sensitivityFeature4 = 'assets/icons/sensitivity_feature_4.svg';
static const String sensitivityFeature5 = 'assets/icons/sensitivity_feature_5.svg';
static const String sensitivityFeature6 = 'assets/icons/sensitivity_feature_6.svg';
static const String sensitivityFeature7 = 'assets/icons/sensitivity_feature_7.svg';
static const String sensitivityFeature8 = 'assets/icons/sensitivity_feature_8.svg';
static const String sensitivityFeature9 = 'assets/icons/sensitivity_feature_9.svg';
static const String sensitivityFeature1 =
'assets/icons/sensitivity_feature_1.svg';
static const String sensitivityFeature2 =
'assets/icons/sensitivity_feature_2.svg';
static const String sensitivityFeature3 =
'assets/icons/sensitivity_feature_3.svg';
static const String sensitivityFeature4 =
'assets/icons/sensitivity_feature_4.svg';
static const String sensitivityFeature5 =
'assets/icons/sensitivity_feature_5.svg';
static const String sensitivityFeature6 =
'assets/icons/sensitivity_feature_6.svg';
static const String sensitivityFeature7 =
'assets/icons/sensitivity_feature_7.svg';
static const String sensitivityFeature8 =
'assets/icons/sensitivity_feature_8.svg';
static const String sensitivityFeature9 =
'assets/icons/sensitivity_feature_9.svg';
static const String deviceTagIcon = 'assets/icons/device_tag_ic.svg';
static const String targetConfirmTimeIcon =
'assets/icons/target_confirm_time_icon.svg';
@ -453,10 +489,13 @@ class Assets {
static const String indentLevelIcon = 'assets/icons/indent_level_icon.svg';
static const String triggerLevelIcon = 'assets/icons/trigger_level_icon.svg';
static const String blankCalendar = 'assets/icons/blank_calendar.svg';
static const String refreshStatusIcon = 'assets/icons/refresh_status_icon.svg';
static const String energyConsumedIcon = 'assets/icons/energy_consumed_icon.svg';
static const String refreshStatusIcon =
'assets/icons/refresh_status_icon.svg';
static const String energyConsumedIcon =
'assets/icons/energy_consumed_icon.svg';
static const String closeSettingsIcon = 'assets/icons/close_settings_icon.svg';
static const String closeSettingsIcon =
'assets/icons/close_settings_icon.svg';
static const String editNameIconSettings =
'assets/icons/edit_name_icon_settings.svg';
@ -476,4 +515,6 @@ class Assets {
'assets/icons/empty_energy_management_per_device.svg';
static const String emptyHeatmap = 'assets/icons/empty_heatmap.svg';
static const String emptyRangeOfAqi = 'assets/icons/empty_range_of_aqi.svg';
static const String homeIcon = 'assets/icons/home_icon.svg';
static const String groupIcon = 'assets/icons/group_icon.svg';
}

View File

@ -52,4 +52,7 @@ final myTheme = ThemeData(
borderRadius: BorderRadius.circular(4),
),
),
dialogTheme: const DialogThemeData(
backgroundColor: ColorsManager.whiteColors,
),
);