Compare commits

...
Author SHA1 Message Date
Faris Armoush 94f9c1beea Adjust layout in CommunityStructureCanvas by adding horizontal padding to positions and refining target position calculations for improved spacing and alignment. Enhance Stack widget behavior by allowing overflow clipping. 2025-07-22 10:04:38 +03:00
Faris Armoush dfd8c5fa31 Replace Container with AnimatedContainer in CommunityStructureCanvas to enhance visual feedback during state changes. Adjust alpha value for improved visibility based on candidate data presence. 2025-07-22 09:46:56 +03:00
Faris Armoush 60b8ee8b50 Enhance DragTarget logic in CommunityStructureCanvas by refining conditions for rendering and improving readability. Ensure proper handling of dragged data and its parent/community relationships. 2025-07-22 09:37:31 +03:00
Faris Armoush 9d60f913eb Refactor CommunityStructureCanvas to simplify DragTarget logic by replacing SizedBox with SizedBox.shrink() for better performance and readability. 2025-07-22 09:32:57 +03:00
Faris Armoush 40251b846b Integrate ReorderSpaces functionality into CommunityStructureCanvas and enhance RemoteReorderSpacesService with dynamic URL generation. Update ReorderSpacesParam to require parentSpaceUuid and spaces for improved validation and serialization. 2025-07-21 16:43:26 +03:00
Faris Armoush 1323bceca1 Update ReorderSpacesParam to make parentSpaceUuid optional and add toJson method for serialization. 2025-07-21 16:39:31 +03:00
Faris Armoush 35c8a73156 Refactor SpaceManagementPage's initState to ensure HTTPService is initialized before use in CommunitiesBloc. 2025-07-21 16:24:11 +03:00
Faris Armoush ce65b068ff Merge branch 'dev' of https://github.com/SyncrowIOT/web into feature/reorder_spaces_api_integration 2025-07-21 16:22:22 +03:00
Faris Armoush 96f107f972 Refactor SpaceManagementPage to utilize a shared HTTPService instance for API calls in Communities, SpaceDetails, Products, and ReorderSpaces blocs, and injected ReorderSpacesBloc into it. 2025-07-21 16:15:26 +03:00
Faris Armoush a3a7937021 Implemented ReorderSpacesBloc. 2025-07-21 16:14:03 +03:00
Faris Armoush 9bf715501b Implement ReorderSpacesService. 2025-07-21 15:57:31 +03:00
Faris Armoush c65f4a7fab Add ReorderSpacesParam and ReorderSpacesService for managing space reordering functionality. 2025-07-21 15:57:20 +03:00
Faris Armoush 7af8887d4f Add new API endpoint for reordering spaces in the community module. 2025-07-21 15:57:10 +03:00
Faris Armoush 2f89c3486c Release incomplete revamped space management (#360)
<!--
  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 User Story
[SP-1686](https://syncrow.atlassian.net/browse/SP-1686)

## Description
Released the new space management rewritten version, which for now
includes, CRUD operations for spaces, and community tree with the new
API.

Rewrote main files, since they had a lot of code duplication, i
extracted duplicated code into one component for all flavors to use.

Enhanced routing solution, to make sure there's only one instance of the
router, as the documentation of `go_router` guides us to do.

## Type of Change

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

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


[SP-1686]:
https://syncrow.atlassian.net/browse/SP-1686?atlOrigin=eyJpIjoiNWRkNTljNzYxNjVmNDY3MDlhMDU5Y2ZhYzA5YTRkZjUiLCJwIjoiZ2l0aHViLWNvbS1KU1cifQ
2025-07-21 15:33:06 +03:00
Faris Armoush 3a4fce966c Merge branch 'dev' of https://github.com/SyncrowIOT/web into release_incomplete_revamped_space_management 2025-07-21 15:06:40 +03:00
Faris Armoush 6bdd28ec57 Refactor main entry points to utilize SyncrowApp, removing legacy MyApp implementation and associated dependencies, since there was too much duplicated code. 2025-07-21 14:50:29 +03:00
Faris Armoush 652163fdae Updated Space management route in app_routes.dart to use the new, incomplete, revamped space management. 2025-07-21 14:47:42 +03:00
Faris Armoush d12b4c0c65 Sp 1721 fe implement delete space feature (#351)
<!--
  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-1721](https://syncrow.atlassian.net/browse/SP-1721)

## Description

Implemented delete space feature.
Smoothened out the connective lines in the canvas.
Synced state between selection and communities bloc on delete.
Implemented create space feature.

## Type of Change

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

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


[SP-1721]:
https://syncrow.atlassian.net/browse/SP-1721?atlOrigin=eyJpIjoiNWRkNTljNzYxNjVmNDY3MDlhMDU5Y2ZhYzA5YTRkZjUiLCJwIjoiZ2l0aHViLWNvbS1KU1cifQ
2025-07-17 16:49:20 +03:00
mohammadnemer1 22c8c54fab Enhance booking system: update API endpoints, improve event loading w… (#357)
…ith caching, and refine UI components

<!--
  Thanks for contributing!

Provide a description of your changes below and a general summary in the
title

Please look at the following checklist to ensure that your PR can be
accepted quickly:
-->



## Description

<!--- Describe your changes in detail -->
Enhance booking system: update API endpoints, improve event loading with
caching, and refine UI components

## 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-17 16:23:03 +03:00
Rafeek-khoudare 95300071e9 [FE] When user navigates to devices page the devices are already listed although no community is selected also when we select a community the API is being called repeatedly too many times (#356)
<!--
  Thanks for contributing!

Provide a description of your changes below and a general summary in the
title

Please look at the following checklist to ensure that your PR can be
accepted quickly:
-->

## Jira Ticket
[SP-1589](https://syncrow.atlassian.net/browse/SP-1589)

## Description
fixed the issue of community selection (if empty no devices should
appear)
## Type of Change

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

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


[SP-1589]:
https://syncrow.atlassian.net/browse/SP-1589?atlOrigin=eyJpIjoiNWRkNTljNzYxNjVmNDY3MDlhMDU5Y2ZhYzA5YTRkZjUiLCJwIjoiZ2l0aHViLWNvbS1KU1cifQ
2025-07-17 15:07:46 +03:00
Faris Armoush 06f00da02c Update shadow properties in CommunityStructureHeader for improved visual aesthetics. Adjusted blur radius and offset to enhance the header's appearance in the space management interface. 2025-07-17 13:00:38 +03:00
mohammad 7876af9756 Refactor memory service implementation: rename MemoryBookableSpaceService to MemoryCalendarService for clarity and consistency 2025-07-17 12:46:07 +03:00
mohammad fe2f4a872b Refactor memory event handling: replace MemoryBookableSpaceService with a new implementation and integrate caching logic in CalendarEventsBloc 2025-07-17 12:38:58 +03:00
mohammad c9b8fbb0c2 Refactor calendar event loading: replace parameters with LoadEventsParam class and implement memory caching for improved performance 2025-07-17 11:20:32 +03:00
Faris Armoush 7b5b40a03c Refactor recursivelyInsert method in SpacesRecursiveHelper to use named parameters. Update CommunityStructureCanvas to reflect these changes, ensuring correct space insertion under the specified parent. 2025-07-16 16:30:38 +03:00
Faris Armoush 8522c0bbc3 Made SpacesRecursiveHelper private, to avoid creating unnecessary instances, since all its methods are static. 2025-07-16 16:26:15 +03:00
mohammad c6729f476f Enhance booking system: update API endpoints, improve event loading with caching, and refine UI components 2025-07-16 15:36:49 +03:00
Faris Armoush fc70669f1d sends correct parentUuid key in create space. 2025-07-16 15:27:31 +03:00
Faris Armoush f03c28f7fd Add recursive insertion method in SpacesRecursiveHelper for managing space hierarchy. Update CommunityStructureCanvas to utilize this method for inserting new spaces under the correct parent, enhancing community state management during space creation. 2025-07-16 11:47:27 +03:00
Faris Armoush 6ec972a520 Integrate CommunitiesTreeSelectionBloc into CreateSpaceButton to handle space selection upon successful space creation. Update community state in CommunitiesBloc to reflect new space addition, enhancing user experience and maintainability. 2025-07-16 11:18:24 +03:00
Faris Armoush 62f67f5a5f Refactor CreateSpaceButton and CommunityStructureCanvas to utilize CommunityModel directly, enhancing data handling during space creation. Implement success callback for space creation to update community state in CommunitiesBloc, improving user experience and maintainability. 2025-07-16 11:16:41 +03:00
Rafeek-Khoudare 3e634dc7a2 fix communities filtiring issue 2025-07-16 11:02:32 +03:00
Faris Armoush 8e303af0d7 Update RemoteCreateSpaceService to correct API endpoint for space creation, changing the return path to include '/spaces' for accurate resource targeting. 2025-07-16 11:01:26 +03:00
Faris Armoush 4ef4858cee Added x, and y to the toJson method of CreateSpaceParam, since the API still needs them as required properties. Revert the once the BE removes those properties. 2025-07-16 10:29:43 +03:00
Faris Armoush 9a203b2fd9 Add CreateSpaceBloc, CreateSpaceEvent, and CreateSpaceState for managing space creation logic. Implement event handling and state management to enhance user experience during space creation. 2025-07-16 10:01:51 +03:00
Faris Armoush 39c5fd1bca Refactor SpaceDetailsModel to integrate Subspace and ProductAllocation models, enhancing data structure and serialization. Update related widgets to utilize Subspace instead of SpaceDetailsModel for improved clarity and maintainability. 2025-07-16 09:57:29 +03:00
Faris Armoush 308eb65d46 Update error handling in RemoteUpdateSpaceService to retrieve error messages from the 'message' field instead of 'error', enhancing clarity in error reporting. 2025-07-16 09:56:55 +03:00
Faris Armoush d65f9ceea9 Enhance AddDeviceTypeWidget to support initial product counts and update selection logic. Modify AssignTagsDialog to pass initial products from space allocations, improving user experience and maintainability. 2025-07-15 14:53:04 +03:00
Faris Armoush f539b0ac8d Rename UniqueSubspacesDecorator to UniqueSpaceDetailsSpacesDecoratorService 2025-07-15 14:38:44 +03:00
Faris Armoush 5a3cf93748 Improved UniqueSubspacesDecorator implementation to improve handling of duplicate subspace names. 2025-07-15 14:37:16 +03:00
Faris Armoush e740652507 Refactor PlusButtonWidget and SpaceCardWidget to improve widget structure and interaction handling. Replace GestureDetector with IconButton for better usability and update positioning logic for the PlusButtonWidget, enhancing maintainability and readability. 2025-07-15 13:11:22 +03:00
Faris Armoush c60078c96a Refactor CommunityStructureCanvas to improve widget structure by rearranging the InteractiveViewer and GestureDetector hierarchy. This change enhances readability and maintainability while ensuring proper interaction handling. 2025-07-15 13:04:37 +03:00
Faris Armoush 903c5dd29b Refactor SpacesRecursiveHelper to improve variable naming and enhance readability. Update mapping logic to clarify the distinction between updated and non-null spaces, ensuring better maintainability of the recursive space handling. 2025-07-15 12:55:49 +03:00
Faris Armoush df39fca050 Refactor CommunityStructureHeaderActionButtons to simplify null handling for selectedSpace and improve widget structure. Ensure buttons are always displayed when selectedSpace is not null, enhancing readability and maintainability. 2025-07-15 12:49:37 +03:00
Faris Armoush f832c5d884 Refactor SpaceManagementCommunityStructure to improve widget structure and visibility handling. Introduce separate methods for building the canvas and empty state, enhancing readability and maintainability. 2025-07-15 12:30:24 +03:00
Faris Armoush fa930571dc Ensure proper handling of null selectedSpace in CommunityStructureCanvas during widget updates to prevent unnecessary processing. 2025-07-15 12:27:45 +03:00
Rafeek-khoudare 75b9f4a4e6 changed the title from Routine to Workflow Automation (#354)
<!--
  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-1851](https://syncrow.atlassian.net/browse/SP-1851)

## Description

change the Title to Work Flow 

## 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-1851]:
https://syncrow.atlassian.net/browse/SP-1851?atlOrigin=eyJpIjoiNWRkNTljNzYxNjVmNDY3MDlhMDU5Y2ZhYzA5YTRkZjUiLCJwIjoiZ2l0aHViLWNvbS1KU1cifQ
2025-07-15 11:54:29 +03:00
Rafeek-Khoudare fe4063ef8f changed the title from Routine to Workflow Automation 2025-07-15 11:20:13 +03:00
Faris Armoush acefe7b355 Refactor RemoteDeleteSpaceService to use a private HTTPService instance and update URL construction with ApiEndpoints for improved maintainability. Update DeleteSpaceDialog to reflect changes in service initialization. 2025-07-15 11:09:04 +03:00
mohammadnemer1 029b5d32e0 Adjust table scroll behavior and modify height for improved layout (#353)
<!--
  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 -->
Adjust table scroll behavior and modify height for improved layout

## 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-15 10:43:22 +03:00
Faris Armoush b223194950 Add loading and status widgets for delete space dialog; refactor dialog to utilize new components for improved user feedback during space deletion process. 2025-07-15 10:07:12 +03:00
mohammad 428c81efff Adjust table scroll behavior and modify height for improved layout 2025-07-15 10:05:07 +03:00
Rafeek-khoudare 288c252f46 SP-1696-fe-edit-user-dialog-enhancements (#347)
<!--
  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-1696](https://syncrow.atlassian.net/browse/SP-1696)

## Description

add company Name and replace it with job title

## 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-1696]:
https://syncrow.atlassian.net/browse/SP-1696?atlOrigin=eyJpIjoiNWRkNTljNzYxNjVmNDY3MDlhMDU5Y2ZhYzA5YTRkZjUiLCJwIjoiZ2l0aHViLWNvbS1KU1cifQ
2025-07-15 09:08:15 +03:00
Rafeek-khoudare 7399dee687 [FE] Redundant API calls on Routines page when selecting a community from the tree (#345)
…rom the tree

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

## Description

fix Redundant API calls when choosing devices and use Que Par to send
spaces instead of send api for every space

## 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-1800]:
https://syncrow.atlassian.net/browse/SP-1800?atlOrigin=eyJpIjoiNWRkNTljNzYxNjVmNDY3MDlhMDU5Y2ZhYzA5YTRkZjUiLCJwIjoiZ2l0aHViLWNvbS1KU1cifQ
2025-07-15 08:32:08 +03:00
Rafeek-Khoudare 08e2ed4b4c Merge branch 'dev' into revert-SP-1589 2025-07-15 08:31:45 +03:00
Rafeek-Khoudare 59e04708cd Merge branch 'main' into revert-SP-1589 2025-07-15 08:30:33 +03:00
Rafeek-Khoudare 338d4f5737 fix typo 2025-07-15 08:19:43 +03:00
Faris Armoush 466f5b89c7 Enhanced SpacesConnectionsArrowPainter and CommunityStructureCanvas to support dynamic card widths; enhance SpaceCell widget layout and shadow properties for improved UI consistency. 2025-07-14 16:56:51 +03:00
Faris Armoush de5d8df01c Update SpaceCell widget shadow properties for improved visual appearance 2025-07-14 16:19:57 +03:00
mohammadnemer1 5532935a3a Enhance UI components: update color management, adjust button styles,… (#350)
… and improve text formatting for better readability

<!--
  Thanks for contributing!

Provide a description of your changes below and a general summary in the
title

Please look at the following checklist to ensure that your PR can be
accepted quickly:
-->



## Description

<!--- Describe your changes in detail -->
Enhance UI components: update color 
## 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-14 16:15:42 +03:00
Faris Armoush 5218641705 Refactor didUpdateWidget in CommunityStructureCanvas to ensure proper widget lifecycle management 2025-07-14 16:05:44 +03:00
Faris Armoush ab6a6851f2 Update control points in SpacesConnectionsArrowPainter for smoother arrow rendering 2025-07-14 15:23:01 +03:00
mohammadnemer1 249cbfc242 Merge branch 'dev' into Build-Schedule-List-View-Support-State-Persistence 2025-07-14 15:20:08 +03:00
mohammad 8167926620 Enhance UI components: update color management, adjust button styles, and improve text formatting for better readability 2025-07-14 15:14:56 +03:00
Faris Armoush beb33e37fa Add SpacesRecursiveHelper for recursive space updates and deletions; refactor CommunityStructureHeader to use CommunityStructureHeaderActionButtonsComposer for improved action handling. 2025-07-14 14:59:58 +03:00
Faris Armoush 3bee17c574 Merge branch 'dev' of https://github.com/SyncrowIOT/web into SP-1721-FE-Implement-Delete-Space-Feature 2025-07-14 14:44:55 +03:00
Faris Armoush f4b5c6fb52 Implement delete space functionality in CommunityStructureHeader: Integrate DeleteSpaceDialog for space deletion confirmation and update routing for space management page. 2025-07-14 14:44:08 +03:00
Faris Armoush 086f3cedf8 Refactor RemoteDeleteSpaceService: Simplify success response handling and improve error message formatting 2025-07-14 14:30:17 +03:00
mohammadnemer1 559091faa0 Add calendar event management features and UI components and Implemen… (#349)
…t Calendar logic

<!--
  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 -->
Implement Calendar logic

## 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-14 14:18:43 +03:00
Faris Armoush 035c03c6b2 Fix error handling in DeleteSpaceBloc: update failure message to include exception details 2025-07-14 14:17:01 +03:00
Faris Armoush eba351c9be Sp 1717 fe draw create edit space dialog (#348)
<!--
  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-1717](https://syncrow.atlassian.net/browse/SP-1717)

## Description

Implemented Reordering in spaces, but without API integration.
Implemented syncing data between selection and communities bloc.
Implemented Edit community feature.
Implemented button in canvas that opens a create dialog.


## 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-1717]:
https://syncrow.atlassian.net/browse/SP-1717?atlOrigin=eyJpIjoiNWRkNTljNzYxNjVmNDY3MDlhMDU5Y2ZhYzA5YTRkZjUiLCJwIjoiZ2l0aHViLWNvbS1KU1cifQ
2025-07-14 12:57:21 +03:00
Faris Armoush cf1b34ee0a Add x_delete icon asset. 2025-07-14 12:17:07 +03:00
Faris Armoush 5663e2084e Created DeleteSpaceBloc. 2025-07-14 10:57:34 +03:00
Faris Armoush 3cd0125310 Refactor space deletion: Introduce DeleteSpaceParam and DeleteSpaceService for enhanced space management functionality 2025-07-14 10:54:13 +03:00
Faris Armoush e0980b324c Add DeleteSpaceParam and DeleteSpaceService for space deletion functionality 2025-07-14 10:54:07 +03:00
mohammad 65d541d594 Add calendar event management features and UI components and Implement Calendar logic 2025-07-14 10:46:12 +03:00
Rafeek-Khoudare 2681c837f5 add company name and replace it with job title 2025-07-11 12:10:53 +03:00
Rafeek-Khoudare b6664ec1ba fix Redundant API calls on Routines page when selecting a community from the tree 2025-07-11 10:53:04 +03:00
Rafeek Alkhoudare dcf1df9b4a sp1613 delete condition word 2025-05-21 07:25:34 -05:00
97 changed files with 2675 additions and 1165 deletions
+5
View File
@@ -0,0 +1,5 @@
<svg width="35" height="35" viewBox="0 0 35 35" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M17.5 34.9999C27.1649 34.9999 34.9999 27.1649 34.9999 17.4999C34.9999 7.83499 27.1649 0 17.5 0C7.83499 0 0 7.83499 0 17.5C0 27.1651 7.83499 34.9999 17.5 34.9999Z" fill="#FF6465"/>
<path opacity="0.1" d="M4.70804 17.5C4.70804 8.63343 11.3024 1.30805 19.854 0.158115C19.0839 0.0545507 18.2984 0 17.5 0C7.835 0 0 7.835 0 17.5C0 27.1651 7.83499 35 17.4999 35C18.2983 35 19.0839 34.9455 19.8539 34.8419C11.3024 33.6919 4.70804 26.3665 4.70804 17.5Z" fill="black"/>
<path d="M21.4229 17.5003L26.0301 12.8931C26.365 12.5582 26.365 12.0152 26.0301 11.6804L23.3197 8.96992C22.9848 8.63503 22.4418 8.63503 22.107 8.96992L17.4997 13.5772L12.8924 8.96992C12.5576 8.63503 12.0146 8.63503 11.6798 8.96992L8.96931 11.6804C8.63442 12.0153 8.63442 12.5582 8.96931 12.8931L13.5766 17.5003L8.96931 22.1076C8.63442 22.4425 8.63442 22.9855 8.96931 23.3204L11.6798 26.0308C12.0146 26.3657 12.5576 26.3657 12.8924 26.0308L17.4997 21.4235L22.1071 26.0308C22.442 26.3657 22.9849 26.3657 23.3198 26.0308L26.0302 23.3204C26.3651 22.9855 26.3651 22.4425 26.0302 22.1076L21.4229 17.5003Z" fill="white"/>
</svg>

After

Width:  |  Height:  |  Size: 1.2 KiB

+2 -69
View File
@@ -1,22 +1,9 @@
import 'package:firebase_core/firebase_core.dart'; import 'package:firebase_core/firebase_core.dart';
import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_dotenv/flutter_dotenv.dart'; import 'package:flutter_dotenv/flutter_dotenv.dart';
import 'package:go_router/go_router.dart';
import 'package:syncrow_web/firebase_options.dart'; import 'package:syncrow_web/firebase_options.dart';
import 'package:syncrow_web/pages/auth/bloc/auth_bloc.dart';
import 'package:syncrow_web/pages/home/bloc/home_bloc.dart';
import 'package:syncrow_web/pages/home/bloc/home_event.dart';
import 'package:syncrow_web/pages/routines/bloc/create_routine_bloc/create_routine_bloc.dart';
import 'package:syncrow_web/pages/routines/bloc/routine_bloc/routine_bloc.dart';
import 'package:syncrow_web/pages/space_tree/bloc/space_tree_bloc.dart';
import 'package:syncrow_web/pages/visitor_password/bloc/visitor_password_bloc.dart';
import 'package:syncrow_web/services/locator.dart'; import 'package:syncrow_web/services/locator.dart';
import 'package:syncrow_web/utils/app_routes.dart'; import 'package:syncrow_web/syncrow_app.dart';
import 'package:syncrow_web/utils/constants/routes_const.dart';
import 'package:syncrow_web/utils/navigation_service.dart';
import 'package:syncrow_web/utils/theme/theme.dart';
Future<void> main() async { Future<void> main() async {
try { try {
@@ -33,59 +20,5 @@ Future<void> main() async {
); );
initialSetup(); initialSetup();
} catch (_) {} } catch (_) {}
runApp(MyApp()); runApp(const SyncrowApp());
}
class MyApp extends StatelessWidget {
MyApp({super.key});
final GoRouter _router = GoRouter(
initialLocation: RoutesConst.auth,
routes: AppRoutes.getRoutes(),
redirect: (context, state) async {
final checkToken = await AuthBloc.getTokenAndValidate();
final loggedIn = checkToken == 'Success';
final goingToLogin = state.uri.toString() == RoutesConst.auth;
if (!loggedIn && !goingToLogin) return RoutesConst.auth;
if (loggedIn && goingToLogin) return RoutesConst.home;
return null;
},
);
@override
Widget build(BuildContext context) {
return MultiBlocProvider(
providers: [
BlocProvider<CreateRoutineBloc>(
create: (context) => CreateRoutineBloc(),
),
BlocProvider(create: (context) => HomeBloc()..add(const FetchUserInfo())),
BlocProvider<VisitorPasswordBloc>(
create: (context) => VisitorPasswordBloc(),
),
BlocProvider<RoutineBloc>(
create: (context) => RoutineBloc(),
),
BlocProvider<SpaceTreeBloc>(
create: (context) => SpaceTreeBloc(),
),
],
child: MaterialApp.router(
debugShowCheckedModeBanner: false,
scrollBehavior: const MaterialScrollBehavior().copyWith(
dragDevices: {
PointerDeviceKind.mouse,
PointerDeviceKind.touch,
PointerDeviceKind.stylus,
PointerDeviceKind.unknown,
},
),
key: NavigationService.navigatorKey,
// scaffoldMessengerKey: NavigationService.snackbarKey,
theme: myTheme,
routerConfig: _router,
));
}
} }
+2 -69
View File
@@ -1,22 +1,9 @@
import 'package:firebase_core/firebase_core.dart'; import 'package:firebase_core/firebase_core.dart';
import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_dotenv/flutter_dotenv.dart'; import 'package:flutter_dotenv/flutter_dotenv.dart';
import 'package:go_router/go_router.dart';
import 'package:syncrow_web/firebase_options.dart'; import 'package:syncrow_web/firebase_options.dart';
import 'package:syncrow_web/pages/auth/bloc/auth_bloc.dart';
import 'package:syncrow_web/pages/home/bloc/home_bloc.dart';
import 'package:syncrow_web/pages/home/bloc/home_event.dart';
import 'package:syncrow_web/pages/routines/bloc/create_routine_bloc/create_routine_bloc.dart';
import 'package:syncrow_web/pages/routines/bloc/routine_bloc/routine_bloc.dart';
import 'package:syncrow_web/pages/space_tree/bloc/space_tree_bloc.dart';
import 'package:syncrow_web/pages/visitor_password/bloc/visitor_password_bloc.dart';
import 'package:syncrow_web/services/locator.dart'; import 'package:syncrow_web/services/locator.dart';
import 'package:syncrow_web/utils/app_routes.dart'; import 'package:syncrow_web/syncrow_app.dart';
import 'package:syncrow_web/utils/constants/routes_const.dart';
import 'package:syncrow_web/utils/navigation_service.dart';
import 'package:syncrow_web/utils/theme/theme.dart';
Future<void> main() async { Future<void> main() async {
try { try {
@@ -33,59 +20,5 @@ Future<void> main() async {
); );
initialSetup(); initialSetup();
} catch (_) {} } catch (_) {}
runApp(MyApp()); runApp(const SyncrowApp());
}
class MyApp extends StatelessWidget {
MyApp({super.key});
final GoRouter _router = GoRouter(
initialLocation: RoutesConst.auth,
routes: AppRoutes.getRoutes(),
redirect: (context, state) async {
final checkToken = await AuthBloc.getTokenAndValidate();
final loggedIn = checkToken == 'Success';
final goingToLogin = state.uri.toString() == RoutesConst.auth;
if (!loggedIn && !goingToLogin) return RoutesConst.auth;
if (loggedIn && goingToLogin) return RoutesConst.home;
return null;
},
);
@override
Widget build(BuildContext context) {
return MultiBlocProvider(
providers: [
BlocProvider<CreateRoutineBloc>(
create: (context) => CreateRoutineBloc(),
),
BlocProvider(create: (context) => HomeBloc()..add(const FetchUserInfo())),
BlocProvider<VisitorPasswordBloc>(
create: (context) => VisitorPasswordBloc(),
),
BlocProvider<RoutineBloc>(
create: (context) => RoutineBloc(),
),
BlocProvider<SpaceTreeBloc>(
create: (context) => SpaceTreeBloc(),
),
],
child: MaterialApp.router(
debugShowCheckedModeBanner: false,
scrollBehavior: const MaterialScrollBehavior().copyWith(
dragDevices: {
PointerDeviceKind.mouse,
PointerDeviceKind.touch,
PointerDeviceKind.stylus,
PointerDeviceKind.unknown,
},
),
key: NavigationService.navigatorKey,
// scaffoldMessengerKey: NavigationService.snackbarKey,
theme: myTheme,
routerConfig: _router,
));
}
} }
+6 -70
View File
@@ -1,26 +1,16 @@
import 'package:firebase_core/firebase_core.dart'; import 'package:firebase_core/firebase_core.dart';
import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_dotenv/flutter_dotenv.dart'; import 'package:flutter_dotenv/flutter_dotenv.dart';
import 'package:go_router/go_router.dart';
import 'package:syncrow_web/firebase_options.dart'; import 'package:syncrow_web/firebase_options.dart';
import 'package:syncrow_web/pages/auth/bloc/auth_bloc.dart';
import 'package:syncrow_web/pages/home/bloc/home_bloc.dart';
import 'package:syncrow_web/pages/home/bloc/home_event.dart';
import 'package:syncrow_web/pages/routines/bloc/create_routine_bloc/create_routine_bloc.dart';
import 'package:syncrow_web/pages/routines/bloc/routine_bloc/routine_bloc.dart';
import 'package:syncrow_web/pages/space_tree/bloc/space_tree_bloc.dart';
import 'package:syncrow_web/pages/visitor_password/bloc/visitor_password_bloc.dart';
import 'package:syncrow_web/services/locator.dart'; import 'package:syncrow_web/services/locator.dart';
import 'package:syncrow_web/utils/app_routes.dart'; import 'package:syncrow_web/syncrow_app.dart';
import 'package:syncrow_web/utils/constants/routes_const.dart';
import 'package:syncrow_web/utils/navigation_service.dart';
import 'package:syncrow_web/utils/theme/theme.dart';
Future<void> main() async { Future<void> main() async {
try { try {
const environment = String.fromEnvironment('FLAVOR', defaultValue: 'staging'); const environment = String.fromEnvironment(
'FLAVOR',
defaultValue: 'staging',
);
await dotenv.load(fileName: '.env.$environment'); await dotenv.load(fileName: '.env.$environment');
WidgetsFlutterBinding.ensureInitialized(); WidgetsFlutterBinding.ensureInitialized();
await Firebase.initializeApp( await Firebase.initializeApp(
@@ -30,59 +20,5 @@ Future<void> main() async {
); );
initialSetup(); initialSetup();
} catch (_) {} } catch (_) {}
runApp(MyApp()); runApp(const SyncrowApp());
}
class MyApp extends StatelessWidget {
MyApp({super.key});
final GoRouter _router = GoRouter(
initialLocation: RoutesConst.auth,
routes: AppRoutes.getRoutes(),
redirect: (context, state) async {
final checkToken = await AuthBloc.getTokenAndValidate();
final loggedIn = checkToken == 'Success';
final goingToLogin = state.uri.toString() == RoutesConst.auth;
if (!loggedIn && !goingToLogin) return RoutesConst.auth;
if (loggedIn && goingToLogin) return RoutesConst.home;
return null;
},
);
@override
Widget build(BuildContext context) {
return MultiBlocProvider(
providers: [
BlocProvider<CreateRoutineBloc>(
create: (context) => CreateRoutineBloc(),
),
BlocProvider(create: (context) => HomeBloc()..add(const FetchUserInfo())),
BlocProvider<VisitorPasswordBloc>(
create: (context) => VisitorPasswordBloc(),
),
BlocProvider<RoutineBloc>(
create: (context) => RoutineBloc(),
),
BlocProvider<SpaceTreeBloc>(
create: (context) => SpaceTreeBloc(),
),
],
child: MaterialApp.router(
debugShowCheckedModeBanner: false,
scrollBehavior: const MaterialScrollBehavior().copyWith(
dragDevices: {
PointerDeviceKind.mouse,
PointerDeviceKind.touch,
PointerDeviceKind.stylus,
PointerDeviceKind.unknown,
},
),
key: NavigationService.navigatorKey,
// scaffoldMessengerKey: NavigationService.snackbarKey,
theme: myTheme,
routerConfig: _router,
));
}
} }
@@ -0,0 +1,63 @@
import 'package:syncrow_web/pages/access_management/booking_system/data/services/remote_calendar_service.dart';
import 'package:syncrow_web/pages/access_management/booking_system/domain/LoadEventsParam.dart';
import 'package:syncrow_web/pages/access_management/booking_system/domain/models/calendar_event_booking.dart';
import 'package:syncrow_web/pages/access_management/booking_system/domain/services/calendar_system_service.dart';
class MemoryCalendarService implements CalendarSystemService {
final Map<String, CalendarEventsResponse> _eventsCache = {};
@override
Future<CalendarEventsResponse> getCalendarEvents({
required LoadEventsParam params,
}) async {
final key = params.generateKey();
return _eventsCache[key]!;
}
void setEvents(
LoadEventsParam param,
CalendarEventsResponse events,
) {
final key = param.generateKey();
_eventsCache[key] = events;
}
void addEvent(LoadEventsParam param, CalendarEventsResponse event) {
final key = param.generateKey();
_eventsCache[key] = event;
}
void clear() {
_eventsCache.clear();
}
}
class MemoryCalendarServiceWithRemoteFallback implements CalendarSystemService {
final MemoryCalendarService memoryService;
final RemoteCalendarService remoteService;
MemoryCalendarServiceWithRemoteFallback({
required this.memoryService,
required this.remoteService,
});
@override
Future<CalendarEventsResponse> getCalendarEvents({
required LoadEventsParam params,
}) async {
final key = params.generateKey();
final doesExistInMemory = memoryService._eventsCache.containsKey(key);
if (doesExistInMemory) {
return memoryService.getCalendarEvents(params: params);
} else {
final remoteResult =
await remoteService.getCalendarEvents(params: params);
memoryService.setEvents(params, remoteResult);
return remoteResult;
}
}
}
@@ -0,0 +1,45 @@
import 'package:dio/dio.dart';
import 'package:syncrow_web/pages/access_management/booking_system/domain/LoadEventsParam.dart';
import 'package:syncrow_web/pages/access_management/booking_system/domain/models/calendar_event_booking.dart';
import 'package:syncrow_web/pages/access_management/booking_system/domain/services/calendar_system_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 RemoteCalendarService implements CalendarSystemService {
const RemoteCalendarService(this._httpService);
final HTTPService _httpService;
static const _defaultErrorMessage = 'Failed to load Calendar';
@override
Future<CalendarEventsResponse> getCalendarEvents({
required LoadEventsParam params,
}) async {
final month = params.startDate.month.toString().padLeft(2, '0');
final year = params.startDate.year.toString();
try {
return await _httpService.get<CalendarEventsResponse>(
path: ApiEndpoints.getBookings
.replaceAll('{mm}', month)
.replaceAll('{yyyy}', year)
.replaceAll('{space}', params.id),
expectedResponseModel: (json) {
return CalendarEventsResponse.fromJson(json as Map<String, dynamic>);
},
);
} on DioException catch (e) {
final responseData = e.response?.data;
if (responseData is Map<String, dynamic>) {
final errorMessage = responseData['error']?['message'] as String? ??
responseData['message'] as String? ??
_defaultErrorMessage;
throw APIException(errorMessage);
}
throw APIException(_defaultErrorMessage);
} catch (e) {
throw APIException('$_defaultErrorMessage: ${e.toString()}');
}
}
}
@@ -0,0 +1,34 @@
import 'package:equatable/equatable.dart';
class LoadEventsParam extends Equatable {
final DateTime startDate;
final DateTime endDate;
final String id;
const LoadEventsParam({
required this.startDate,
required this.endDate,
required this.id,
});
@override
List<Object?> get props => [startDate, endDate, id];
LoadEventsParam copyWith({
DateTime? startDate,
DateTime? endDate,
String? id,
}) {
return LoadEventsParam(
startDate: startDate ?? this.startDate,
endDate: endDate ?? this.endDate,
id: id ?? this.id,
);
}
}
extension KeyGenerator on LoadEventsParam {
String generateKey() {
return '$id-${startDate.year}-${startDate.month.toString().padLeft(2, '0')}';
}
}
@@ -0,0 +1,134 @@
class CalendarEventBooking {
final String uuid;
final DateTime date;
final String startTime;
final String endTime;
final int cost;
final BookingUser user;
final BookingSpace space;
CalendarEventBooking({
required this.uuid,
required this.date,
required this.startTime,
required this.endTime,
required this.cost,
required this.user,
required this.space,
});
factory CalendarEventBooking.fromJson(Map<String, dynamic> json) {
return CalendarEventBooking(
uuid: json['uuid'] as String? ?? '',
date: json['date'] != null
? DateTime.parse(json['date'] as String)
: DateTime.now(),
startTime: json['startTime'] as String? ?? '',
endTime: json['endTime'] as String? ?? '',
cost: _parseInt(json['cost']),
user: json['user'] != null
? BookingUser.fromJson(json['user'] as Map<String, dynamic>)
: BookingUser.empty(),
space: json['space'] != null
? BookingSpace.fromJson(json['space'] as Map<String, dynamic>)
: BookingSpace.empty(),
);
}
static int _parseInt(dynamic value) {
if (value is int) return value;
if (value is String) return int.tryParse(value) ?? 0;
return 0;
}
}
class BookingUser {
final String uuid;
final String firstName;
final String lastName;
final String email;
final String? companyName;
BookingUser({
required this.uuid,
required this.firstName,
required this.lastName,
required this.email,
this.companyName,
});
factory BookingUser.fromJson(Map<String, dynamic> json) {
return BookingUser(
uuid: json['uuid'] as String? ?? '',
firstName: json['firstName'] as String? ?? '',
lastName: json['lastName'] as String? ?? '',
email: json['email'] as String? ?? '',
companyName: json['companyName'] as String?,
);
}
factory BookingUser.empty() {
return BookingUser(
uuid: '',
firstName: '',
lastName: '',
email: '',
companyName: null,
);
}
}
class BookingSpace {
final String uuid;
final String spaceName;
BookingSpace({
required this.uuid,
required this.spaceName,
});
factory BookingSpace.fromJson(Map<String, dynamic> json) {
return BookingSpace(
uuid: json['uuid'] as String? ?? '',
spaceName: json['spaceName'] as String? ?? '',
);
}
factory BookingSpace.empty() {
return BookingSpace(
uuid: '',
spaceName: '',
);
}
}
class CalendarEventsResponse {
final int statusCode;
final String message;
final List<CalendarEventBooking> data;
final bool success;
CalendarEventsResponse({
required this.statusCode,
required this.message,
required this.data,
required this.success,
});
factory CalendarEventsResponse.fromJson(Map<String, dynamic> json) {
return CalendarEventsResponse(
statusCode: _parseInt(json['statusCode']),
message: json['message'] as String? ?? '',
data: (json['data'] as List? ?? [])
.map((e) => CalendarEventBooking.fromJson(e as Map<String, dynamic>))
.toList(),
success: json['success'] as bool? ?? false,
);
}
}
int _parseInt(dynamic value) {
if (value is int) return value;
if (value is String) return int.tryParse(value) ?? 0;
return 0;
}
@@ -0,0 +1,8 @@
import 'package:syncrow_web/pages/access_management/booking_system/domain/LoadEventsParam.dart';
import 'package:syncrow_web/pages/access_management/booking_system/domain/models/calendar_event_booking.dart';
abstract class CalendarSystemService {
Future<CalendarEventsResponse> getCalendarEvents({
required LoadEventsParam params,
});
}
@@ -2,79 +2,66 @@ import 'dart:async';
import 'package:bloc/bloc.dart'; import 'package:bloc/bloc.dart';
import 'package:calendar_view/calendar_view.dart'; import 'package:calendar_view/calendar_view.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:syncrow_web/pages/access_management/booking_system/domain/LoadEventsParam.dart';
import 'package:syncrow_web/pages/access_management/booking_system/domain/models/calendar_event_booking.dart';
import 'package:syncrow_web/pages/access_management/booking_system/domain/services/calendar_system_service.dart';
import 'package:syncrow_web/pages/access_management/booking_system/data/services/memory_bookable_space_service.dart';
part 'events_event.dart'; part 'events_event.dart';
part 'events_state.dart'; part 'events_state.dart';
class CalendarEventsBloc extends Bloc<CalendarEventsEvent, CalendarEventState> { class CalendarEventsBloc extends Bloc<CalendarEventsEvent, CalendarEventState> {
final EventController eventController = EventController(); final EventController eventController = EventController();
final CalendarSystemService calendarService;
CalendarEventsBloc() : super(EventsInitial()) { CalendarEventsBloc({
required this.calendarService,
}) : super(EventsInitial()) {
on<LoadEvents>(_onLoadEvents); on<LoadEvents>(_onLoadEvents);
on<AddEvent>(_onAddEvent); on<AddEvent>(_onAddEvent);
on<StartTimer>(_onStartTimer);
on<DisposeResources>(_onDisposeResources); on<DisposeResources>(_onDisposeResources);
on<GoToWeek>(_onGoToWeek); on<GoToWeek>(_onGoToWeek);
} }
Future<void> _onLoadEvents( Future<void> _onLoadEvents(
LoadEvents event, LoadEvents event,
Emitter<CalendarEventState> emit, Emitter<CalendarEventState> emit,
) async { ) async {
final param = event.param;
final month = param.endDate.month;
final year = param.endDate.year;
final spaceId = param.id;
emit(EventsLoading()); emit(EventsLoading());
try { try {
final events = _generateDummyEventsForWeek(event.weekStart); final response = await calendarService.getCalendarEvents(params: param);
final events = response.data.map(_toCalendarEventData).toList();
eventController.addAll(events); eventController.addAll(events);
emit(EventsLoaded( emit(EventsLoaded(
events: events, events: events,
initialDate: event.weekStart, spaceId: spaceId,
weekDays: _getWeekDays(event.weekStart), month: month,
year: year,
)); ));
} catch (e) { } catch (e) {
emit(EventsError('Failed to load events')); emit(EventsError('Failed to load events'));
} }
} }
List<CalendarEventData> _generateDummyEventsForWeek(DateTime weekStart) {
final events = <CalendarEventData>[];
for (int i = 0; i < 7; i++) {
final date = weekStart.add(Duration(days: i));
events.add(CalendarEventData(
date: date,
startTime: date.copyWith(hour: 9, minute: 0),
endTime: date.copyWith(hour: 10, minute: 30),
title: 'Team Meeting',
description: 'Daily standup',
color: Colors.blue,
));
events.add(CalendarEventData(
date: date,
startTime: date.copyWith(hour: 14, minute: 0),
endTime: date.copyWith(hour: 15, minute: 0),
title: 'Client Call',
description: 'Project discussion',
color: Colors.green,
));
}
return events;
}
void _onAddEvent(AddEvent event, Emitter<CalendarEventState> emit) { void _onAddEvent(AddEvent event, Emitter<CalendarEventState> emit) {
eventController.add(event.event); eventController.add(event.event);
if (state is EventsLoaded) { if (state is EventsLoaded) {
final loaded = state as EventsLoaded; final loaded = state as EventsLoaded;
emit(EventsLoaded( emit(EventsLoaded(
events: [...eventController.events], events: [...eventController.events],
initialDate: loaded.initialDate, spaceId: loaded.spaceId,
weekDays: loaded.weekDays, month: loaded.month,
year: loaded.year,
)); ));
} }
} }
void _onStartTimer(StartTimer event, Emitter<CalendarEventState> emit) {}
void _onDisposeResources( void _onDisposeResources(
DisposeResources event, Emitter<CalendarEventState> emit) { DisposeResources event, Emitter<CalendarEventState> emit) {
eventController.dispose(); eventController.dispose();
@@ -86,47 +73,46 @@ class CalendarEventsBloc extends Bloc<CalendarEventsEvent, CalendarEventState> {
final newWeekDays = _getWeekDays(event.weekDate); final newWeekDays = _getWeekDays(event.weekDate);
emit(EventsLoaded( emit(EventsLoaded(
events: loaded.events, events: loaded.events,
initialDate: event.weekDate, spaceId: loaded.spaceId,
weekDays: newWeekDays, month: loaded.month,
year: loaded.year,
)); ));
} }
} }
List<CalendarEventData> _generateDummyEvents() { CalendarEventData _toCalendarEventData(CalendarEventBooking booking) {
final now = DateTime.now(); final date = booking.date;
return [
CalendarEventData( final localDate = date.toLocal();
date: now,
startTime: now.copyWith(hour: 8, minute: 00, second: 0), final startParts = booking.startTime.split(':').map(int.parse).toList();
endTime: now.copyWith(hour: 9, minute: 00, second: 0), final endParts = booking.endTime.split(':').map(int.parse).toList();
title: 'Team Meeting',
description: 'Weekly team sync', final startTime = DateTime(
localDate.year,
localDate.month,
localDate.day,
startParts[0],
startParts[1],
);
final endTime = DateTime(
localDate.year,
localDate.month,
localDate.day,
endParts[0],
endParts[1],
);
return CalendarEventData(
date: startTime,
startTime: startTime,
endTime: endTime,
title: '${booking.user.firstName} ${booking.user.lastName}',
description: 'Cost: ${booking.cost}',
color: Colors.blue, color: Colors.blue,
), event: booking,
CalendarEventData( );
date: now,
startTime: now.copyWith(hour: 9, minute: 00, second: 0),
endTime: now.copyWith(hour: 10, minute: 30, second: 0),
title: 'Team Meeting',
description: 'Weekly team sync',
color: Colors.blue,
),
CalendarEventData(
date: now.add(const Duration(days: 1)),
startTime: now.copyWith(hour: 14, day: now.day + 1),
endTime: now.copyWith(hour: 15, day: now.day + 1),
title: 'Client Call',
description: 'Project discussion',
color: Colors.green,
),
CalendarEventData(
date: now.add(const Duration(days: 2)),
startTime: now.copyWith(hour: 11, day: now.day + 2),
endTime: now.copyWith(hour: 12, day: now.day + 2),
title: 'Lunch with Team',
color: Colors.orange,
),
];
} }
List<DateTime> _getWeekDays(DateTime date) { List<DateTime> _getWeekDays(DateTime date) {
@@ -6,13 +6,14 @@ abstract class CalendarEventsEvent {
} }
class LoadEvents extends CalendarEventsEvent { class LoadEvents extends CalendarEventsEvent {
final DateTime weekStart; final LoadEventsParam param;
const LoadEvents({required this.weekStart}); const LoadEvents(this.param);
} }
class AddEvent extends CalendarEventsEvent { class AddEvent extends CalendarEventsEvent {
final CalendarEventData event; final CalendarEventData event;
AddEvent(this.event); const AddEvent(this.event);
} }
class StartTimer extends CalendarEventsEvent {} class StartTimer extends CalendarEventsEvent {}
@@ -23,3 +24,8 @@ class GoToWeek extends CalendarEventsEvent {
final DateTime weekDate; final DateTime weekDate;
GoToWeek(this.weekDate); GoToWeek(this.weekDate);
} }
class CheckWeekHasEvents extends CalendarEventsEvent {
final DateTime weekStart;
const CheckWeekHasEvents(this.weekStart);
}
@@ -7,15 +7,17 @@ class EventsInitial extends CalendarEventState {}
class EventsLoading extends CalendarEventState {} class EventsLoading extends CalendarEventState {}
class EventsLoaded extends CalendarEventState { final class EventsLoaded extends CalendarEventState {
final List<CalendarEventData> events; final List<CalendarEventData> events;
final DateTime initialDate; final String spaceId;
final List<DateTime> weekDays; final int month;
final int year;
EventsLoaded({ EventsLoaded({
required this.events, required this.events,
required this.initialDate, required this.spaceId,
required this.weekDays, required this.month,
required this.year,
}); });
} }
@@ -1,15 +1,20 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:intl/intl.dart';
import 'package:calendar_view/calendar_view.dart'; import 'package:calendar_view/calendar_view.dart';
import 'package:syncrow_web/pages/access_management/booking_system/data/services/remote_calendar_service.dart';
import 'package:syncrow_web/pages/access_management/booking_system/domain/LoadEventsParam.dart';
import 'package:syncrow_web/pages/access_management/booking_system/presentation/bloc/calendar/events_bloc.dart';
import 'package:syncrow_web/pages/access_management/booking_system/presentation/bloc/date_selection/date_selection_bloc.dart'; import 'package:syncrow_web/pages/access_management/booking_system/presentation/bloc/date_selection/date_selection_bloc.dart';
import 'package:syncrow_web/pages/access_management/booking_system/presentation/bloc/date_selection/date_selection_event.dart'; import 'package:syncrow_web/pages/access_management/booking_system/presentation/bloc/date_selection/date_selection_event.dart';
import 'package:syncrow_web/pages/access_management/booking_system/presentation/bloc/date_selection/date_selection_state.dart'; import 'package:syncrow_web/pages/access_management/booking_system/presentation/bloc/date_selection/date_selection_state.dart';
import 'package:syncrow_web/pages/access_management/booking_system/presentation/bloc/selected_bookable_space_bloc/selected_bookable_space_bloc.dart'; import 'package:syncrow_web/pages/access_management/booking_system/presentation/bloc/selected_bookable_space_bloc/selected_bookable_space_bloc.dart';
import 'package:syncrow_web/pages/access_management/booking_system/data/services/memory_bookable_space_service.dart';
import 'package:syncrow_web/pages/access_management/booking_system/presentation/view/widgets/booking_sidebar.dart'; import 'package:syncrow_web/pages/access_management/booking_system/presentation/view/widgets/booking_sidebar.dart';
import 'package:syncrow_web/pages/access_management/booking_system/presentation/view/widgets/custom_calendar_page.dart'; import 'package:syncrow_web/pages/access_management/booking_system/presentation/view/widgets/custom_calendar_page.dart';
import 'package:syncrow_web/pages/access_management/booking_system/presentation/view/widgets/icon_text_button.dart'; import 'package:syncrow_web/pages/access_management/booking_system/presentation/view/widgets/icon_text_button.dart';
import 'package:syncrow_web/pages/access_management/booking_system/presentation/view/widgets/week_navigation.dart';
import 'package:syncrow_web/pages/access_management/booking_system/presentation/view/widgets/weekly_calendar_page.dart'; import 'package:syncrow_web/pages/access_management/booking_system/presentation/view/widgets/weekly_calendar_page.dart';
import 'package:syncrow_web/services/api/http_service.dart';
import 'package:syncrow_web/utils/color_manager.dart'; import 'package:syncrow_web/utils/color_manager.dart';
import 'package:syncrow_web/utils/constants/assets.dart'; import 'package:syncrow_web/utils/constants/assets.dart';
@@ -35,33 +40,22 @@ class _BookingPageState extends State<BookingPage> {
super.dispose(); super.dispose();
} }
List<CalendarEventData> _generateDummyEventsForWeek(DateTime weekStart) { void _dispatchLoadEvents(BuildContext context) {
final List<CalendarEventData> events = []; final selectedRoom =
for (int i = 0; i < 7; i++) { context.read<SelectedBookableSpaceBloc>().state.selectedBookableSpace;
final date = weekStart.add(Duration(days: i)); final dateState = context.read<DateSelectionBloc>().state;
events.add(CalendarEventData(
date: date,
startTime: date.copyWith(hour: 9, minute: 0),
endTime: date.copyWith(hour: 10, minute: 30),
title: 'Team Meeting',
description: 'Daily standup',
color: Colors.blue,
));
events.add(CalendarEventData(
date: date,
startTime: date.copyWith(hour: 14, minute: 0),
endTime: date.copyWith(hour: 15, minute: 0),
title: 'Client Call',
description: 'Project discussion',
color: Colors.green,
));
}
return events;
}
void _loadEventsForWeek(DateTime weekStart) { if (selectedRoom != null) {
_eventController.removeWhere((_) => true); context.read<CalendarEventsBloc>().add(
_eventController.addAll(_generateDummyEventsForWeek(weekStart)); LoadEvents(
LoadEventsParam(
startDate: dateState.weekStart,
endDate: dateState.weekStart.add(const Duration(days: 6)),
id: selectedRoom.uuid,
),
),
);
}
} }
@override @override
@@ -70,13 +64,31 @@ class _BookingPageState extends State<BookingPage> {
providers: [ providers: [
BlocProvider(create: (_) => SelectedBookableSpaceBloc()), BlocProvider(create: (_) => SelectedBookableSpaceBloc()),
BlocProvider(create: (_) => DateSelectionBloc()), BlocProvider(create: (_) => DateSelectionBloc()),
BlocProvider(
create: (_) => CalendarEventsBloc(
calendarService: MemoryCalendarServiceWithRemoteFallback(
remoteService: RemoteCalendarService(
HTTPService(),
),
memoryService: MemoryCalendarService(),
),
)),
], ],
child: BlocListener<DateSelectionBloc, DateSelectionState>( child: Builder(
listenWhen: (previous, current) => builder: (context) =>
previous.weekStart != current.weekStart, BlocListener<CalendarEventsBloc, CalendarEventState>(
listenWhen: (prev, curr) => curr is EventsLoaded,
listener: (context, state) { listener: (context, state) {
_loadEventsForWeek(state.weekStart); if (state is EventsLoaded) {
_eventController.removeWhere((_) => true);
_eventController.addAll(state.events);
}
}, },
child: BlocListener<SelectedBookableSpaceBloc,
SelectedBookableSpaceState>(
listener: (context, state) => _dispatchLoadEvents(context),
child: BlocListener<DateSelectionBloc, DateSelectionState>(
listener: (context, state) => _dispatchLoadEvents(context),
child: Row( child: Row(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
@@ -111,7 +123,8 @@ class _BookingPageState extends State<BookingPage> {
), ),
), ),
Expanded( Expanded(
child: BlocBuilder<DateSelectionBloc, DateSelectionState>( child: BlocBuilder<DateSelectionBloc,
DateSelectionState>(
builder: (context, dateState) { builder: (context, dateState) {
return CustomCalendarPage( return CustomCalendarPage(
selectedDate: dateState.selectedDate, selectedDate: dateState.selectedDate,
@@ -120,9 +133,8 @@ class _BookingPageState extends State<BookingPage> {
context context
.read<DateSelectionBloc>() .read<DateSelectionBloc>()
.add(SelectDate(newDate)); .add(SelectDate(newDate));
context context.read<DateSelectionBloc>().add(
.read<DateSelectionBloc>() SelectDateFromSidebarCalendar(newDate));
.add(SelectDateFromSidebarCalendar(newDate));
}, },
); );
}, },
@@ -133,7 +145,7 @@ class _BookingPageState extends State<BookingPage> {
), ),
), ),
Expanded( Expanded(
flex: 4, flex: 5,
child: Padding( child: Padding(
padding: const EdgeInsets.all(20.0), padding: const EdgeInsets.all(20.0),
child: Column( child: Column(
@@ -157,76 +169,56 @@ class _BookingPageState extends State<BookingPage> {
), ),
], ],
), ),
BlocBuilder<DateSelectionBloc, DateSelectionState>( BlocBuilder<DateSelectionBloc,
DateSelectionState>(
builder: (context, state) { builder: (context, state) {
final weekStart = state.weekStart; final weekStart = state.weekStart;
final weekEnd = final weekEnd =
weekStart.add(const Duration(days: 6)); weekStart.add(const Duration(days: 6));
return Container( return WeekNavigation(
padding: const EdgeInsets.symmetric( weekStart: weekStart,
horizontal: 10, vertical: 5), weekEnd: weekEnd,
decoration: BoxDecoration( onPreviousWeek: () {
color: ColorsManager.circleRolesBackground,
borderRadius: BorderRadius.circular(10),
boxShadow: const [
BoxShadow(
color: ColorsManager.lightGrayColor,
blurRadius: 4,
offset: Offset(0, 1),
),
],
),
child: Row(
children: [
IconButton(
iconSize: 15,
icon: const Icon(Icons.arrow_back_ios,
color: ColorsManager.lightGrayColor),
onPressed: () {
context context
.read<DateSelectionBloc>() .read<DateSelectionBloc>()
.add(PreviousWeek()); .add(PreviousWeek());
}, },
), onNextWeek: () {
const SizedBox(width: 10),
Text(
_getMonthYearText(weekStart, weekEnd),
style: const TextStyle(
color: ColorsManager.lightGrayColor,
fontSize: 14,
fontWeight: FontWeight.w400,
),
),
const SizedBox(width: 10),
IconButton(
iconSize: 15,
icon: const Icon(Icons.arrow_forward_ios,
color: ColorsManager.lightGrayColor),
onPressed: () {
context context
.read<DateSelectionBloc>() .read<DateSelectionBloc>()
.add(NextWeek()); .add(NextWeek());
}, },
),
],
),
); );
}, },
), ),
], ],
), ),
Expanded( Expanded(
flex: 5,
child: BlocBuilder<SelectedBookableSpaceBloc, child: BlocBuilder<SelectedBookableSpaceBloc,
SelectedBookableSpaceState>( SelectedBookableSpaceState>(
builder: (context, roomState) { builder: (context, roomState) {
final selectedRoom = roomState.selectedBookableSpace; final selectedRoom =
roomState.selectedBookableSpace;
return BlocBuilder<DateSelectionBloc, return BlocBuilder<DateSelectionBloc,
DateSelectionState>( DateSelectionState>(
builder: (context, dateState) { builder: (context, dateState) {
return WeeklyCalendarPage( return BlocListener<CalendarEventsBloc,
startTime: CalendarEventState>(
selectedRoom?.bookableConfig.startTime, listenWhen: (prev, curr) =>
endTime: selectedRoom?.bookableConfig.endTime, curr is EventsLoaded,
listener: (context, state) {
if (state is EventsLoaded) {
_eventController
.removeWhere((_) => true);
_eventController.addAll(state.events);
}
},
child: WeeklyCalendarPage(
startTime: selectedRoom
?.bookableConfig.startTime,
endTime: selectedRoom
?.bookableConfig.endTime,
weekStart: dateState.weekStart, weekStart: dateState.weekStart,
selectedDate: dateState.selectedDate, selectedDate: dateState.selectedDate,
eventController: _eventController, eventController: _eventController,
@@ -234,6 +226,7 @@ class _BookingPageState extends State<BookingPage> {
.watch<DateSelectionBloc>() .watch<DateSelectionBloc>()
.state .state
.selectedDateFromSideBarCalender, .selectedDateFromSideBarCalender,
),
); );
}, },
); );
@@ -247,20 +240,9 @@ class _BookingPageState extends State<BookingPage> {
], ],
), ),
), ),
),
),
),
); );
} }
String _getMonthYearText(DateTime start, DateTime end) {
final startMonth = DateFormat('MMM').format(start);
final endMonth = DateFormat('MMM').format(end);
final year = start.year == end.year
? start.year.toString()
: '${start.year}-${end.year}';
if (start.month == end.month) {
return '$startMonth $year';
} else {
return '$startMonth - $endMonth $year';
}
}
} }
@@ -72,11 +72,7 @@ class __SidebarContentState extends State<_SidebarContent> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return BlocConsumer<SidebarBloc, SidebarState>( return BlocConsumer<SidebarBloc, SidebarState>(
listener: (context, state) { listener: (context, state) {},
if (state.currentPage == 1 && searchController.text.isNotEmpty) {
searchController.clear();
}
},
builder: (context, state) { builder: (context, state) {
return Column( return Column(
children: [ children: [
@@ -147,6 +143,7 @@ class __SidebarContentState extends State<_SidebarContent> {
IconButton( IconButton(
icon: const Icon(Icons.close), icon: const Icon(Icons.close),
onPressed: () { onPressed: () {
searchController.clear();
context.read<SidebarBloc>().add(ResetSearch()); context.read<SidebarBloc>().add(ResetSearch());
}, },
), ),
@@ -0,0 +1,106 @@
import 'package:calendar_view/calendar_view.dart';
import 'package:flutter/material.dart';
import 'package:intl/intl.dart';
import 'package:syncrow_web/pages/access_management/booking_system/domain/models/calendar_event_booking.dart';
import 'package:syncrow_web/utils/color_manager.dart';
class EventTileWidget extends StatelessWidget {
final List<CalendarEventData<Object?>> events;
const EventTileWidget({
super.key,
required this.events,
});
@override
Widget build(BuildContext context) {
return Container(
margin: const EdgeInsets.symmetric(vertical: 2, horizontal: 2),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: events.map((event) {
final booking = event.event is CalendarEventBooking
? event.event! as CalendarEventBooking
: null;
final companyName = booking?.user.companyName ?? 'Unknown Company';
final startTime = DateFormat('hh:mm a').format(event.startTime!);
final endTime = DateFormat('hh:mm a').format(event.endTime!);
final isEventEnded =
event.endTime != null && event.endTime!.isBefore(DateTime.now());
final duration = event.endTime!.difference(event.startTime!);
final bool isLongEnough = duration.inMinutes >= 31;
return Expanded(
child: Container(
width: double.infinity,
padding: EdgeInsets.all(5),
decoration: BoxDecoration(
color: isEventEnded
? ColorsManager.grayColor.withOpacity(0.1)
: ColorsManager.blue1.withOpacity(0.1),
borderRadius: BorderRadius.circular(6),
border: const Border(
left: BorderSide(
color: ColorsManager.grayColor,
width: 4,
),
),
),
child: isLongEnough
? Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'$startTime - $endTime',
overflow: TextOverflow.ellipsis,
style: TextStyle(
fontSize: 12,
color: isEventEnded
? ColorsManager.grayColor.withOpacity(0.9)
: ColorsManager.blackColor,
fontWeight: FontWeight.w400,
),
),
const SizedBox(height: 2),
Text(
event.title,
overflow: TextOverflow.ellipsis,
style: TextStyle(
fontSize: 14,
color: isEventEnded
? ColorsManager.grayColor
: ColorsManager.blackColor,
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: 2),
Text(
companyName,
overflow: TextOverflow.ellipsis,
style: TextStyle(
fontSize: 14,
color: isEventEnded
? ColorsManager.grayColor.withOpacity(0.9)
: ColorsManager.blackColor,
fontWeight: FontWeight.w400,
),
),
],
)
: Text(
event.title,
overflow: TextOverflow.ellipsis,
style: TextStyle(
fontSize: 14,
color: isEventEnded
? ColorsManager.grayColor
: ColorsManager.blackColor,
fontWeight: FontWeight.bold,
),
),
),
);
}).toList(),
),
);
}
}
@@ -0,0 +1,91 @@
import 'package:flutter/material.dart';
import 'dart:math' as math;
class HatchedColumnBackground extends StatelessWidget {
final Color backgroundColor;
final Color lineColor;
final double opacity;
final double stripeSpacing;
final BorderRadius? borderRadius;
const HatchedColumnBackground({
super.key,
required this.backgroundColor,
required this.lineColor,
this.opacity = 0.15,
this.stripeSpacing = 12,
this.borderRadius,
});
@override
Widget build(BuildContext context) {
return CustomPaint(
painter: _HatchedBackgroundPainter(
backgroundColor: backgroundColor,
opacity: opacity,
lineColor: lineColor,
stripeSpacing: stripeSpacing,
borderRadius: borderRadius,
),
size: Size.infinite,
);
}
}
class _HatchedBackgroundPainter extends CustomPainter {
final Color backgroundColor;
final double opacity;
final Color lineColor;
final double stripeSpacing;
final BorderRadius? borderRadius;
_HatchedBackgroundPainter({
required this.backgroundColor,
required this.opacity,
required this.lineColor,
required this.stripeSpacing,
this.borderRadius,
});
@override
void paint(Canvas canvas, Size size) {
final rect = Rect.fromLTWH(0, 0, size.width, size.height);
final RRect rrect = borderRadius?.toRRect(rect) ??
RRect.fromRectAndRadius(rect, Radius.zero);
final backgroundPaint = Paint()
..color = backgroundColor.withOpacity(0.02)
..style = PaintingStyle.fill;
canvas.drawRRect(rrect, backgroundPaint);
canvas.save();
canvas.clipRRect(rrect);
final linePaint = Paint()
..color = lineColor
..strokeWidth = 0.5
..style = PaintingStyle.stroke;
final maxExtent =
math.sqrt(size.width * size.width + size.height * size.height);
canvas.translate(0, size.height);
canvas.rotate(-math.pi / 4);
double y = -maxExtent;
while (y < maxExtent) {
canvas.drawLine(
Offset(-maxExtent, y),
Offset(maxExtent, y),
linePaint,
);
y += stripeSpacing;
}
canvas.restore();
}
@override
bool shouldRepaint(covariant _HatchedBackgroundPainter oldDelegate) {
return backgroundColor != oldDelegate.backgroundColor ||
opacity != oldDelegate.opacity ||
lineColor != oldDelegate.lineColor ||
stripeSpacing != oldDelegate.stripeSpacing ||
borderRadius != oldDelegate.borderRadius;
}
}
@@ -24,17 +24,21 @@ class RoomListItem extends StatelessWidget {
activeColor: ColorsManager.primaryColor, activeColor: ColorsManager.primaryColor,
title: Text( title: Text(
room.spaceName, room.spaceName,
maxLines: 2,
style: Theme.of(context).textTheme.bodyMedium?.copyWith( style: Theme.of(context).textTheme.bodyMedium?.copyWith(
color: ColorsManager.lightGrayColor, color: ColorsManager.lightGrayColor,
fontWeight: FontWeight.w700, fontWeight: FontWeight.w700,
overflow: TextOverflow.ellipsis,
fontSize: 12), fontSize: 12),
), ),
subtitle: Text( subtitle: Text(
room.virtualLocation, room.virtualLocation,
maxLines: 2,
style: Theme.of(context).textTheme.bodySmall?.copyWith( style: Theme.of(context).textTheme.bodySmall?.copyWith(
fontSize: 10, fontSize: 10,
fontWeight: FontWeight.w400, fontWeight: FontWeight.w400,
color: ColorsManager.textGray, color: ColorsManager.textGray,
overflow: TextOverflow.ellipsis,
), ),
), ),
); );
@@ -0,0 +1,49 @@
import 'package:flutter/material.dart';
import 'package:syncrow_web/utils/color_manager.dart';
class TimeLineWidget extends StatelessWidget {
final DateTime date;
const TimeLineWidget({Key? key, required this.date}) : super(key: key);
@override
Widget build(BuildContext context) {
int hour =
date.hour == 0 ? 12 : (date.hour > 12 ? date.hour - 12 : date.hour);
String period = date.hour >= 12 ? 'PM' : 'AM';
return Container(
height: 60,
alignment: Alignment.center,
child: RichText(
text: TextSpan(
children: [
TextSpan(
text: '$hour',
style: const TextStyle(
fontWeight: FontWeight.w700,
fontSize: 24,
color: ColorsManager.blackColor,
),
),
WidgetSpan(
child: Padding(
padding: const EdgeInsets.only(left: 2, top: 6),
child: Text(
period,
style: const TextStyle(
fontWeight: FontWeight.w400,
fontSize: 12,
color: ColorsManager.blackColor,
letterSpacing: 1,
),
),
),
alignment: PlaceholderAlignment.baseline,
baseline: TextBaseline.alphabetic,
),
],
),
),
);
}
}
@@ -0,0 +1,39 @@
import 'package:flutter/material.dart';
import 'package:intl/intl.dart';
import 'package:syncrow_web/utils/color_manager.dart';
class WeekDayHeader extends StatelessWidget {
final DateTime date;
final bool isSelectedDay;
const WeekDayHeader({
Key? key,
required this.date,
required this.isSelectedDay,
}) : super(key: key);
@override
Widget build(BuildContext context) {
return Column(
children: [
Text(
DateFormat('EEE').format(date).toUpperCase(),
style: TextStyle(
fontWeight: FontWeight.w400,
fontSize: 14,
color: isSelectedDay ? Colors.blue : Colors.black,
),
),
Text(
DateFormat('d').format(date),
style: TextStyle(
fontWeight: FontWeight.w700,
fontSize: 20,
color:
isSelectedDay ? ColorsManager.blue1 : ColorsManager.blackColor,
),
),
],
);
}
}
@@ -0,0 +1,76 @@
import 'package:flutter/material.dart';
import 'package:intl/intl.dart';
import 'package:syncrow_web/utils/color_manager.dart';
class WeekNavigation extends StatelessWidget {
final DateTime weekStart;
final DateTime weekEnd;
final VoidCallback onPreviousWeek;
final VoidCallback onNextWeek;
const WeekNavigation({
Key? key,
required this.weekStart,
required this.weekEnd,
required this.onPreviousWeek,
required this.onNextWeek,
}) : super(key: key);
@override
Widget build(BuildContext context) {
return Container(
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 5),
decoration: BoxDecoration(
color: ColorsManager.circleRolesBackground,
borderRadius: BorderRadius.circular(10),
boxShadow: const [
BoxShadow(
color: ColorsManager.lightGrayColor,
blurRadius: 4,
offset: Offset(0, 1),
),
],
),
child: Row(
children: [
IconButton(
iconSize: 15,
icon: const Icon(Icons.arrow_back_ios,
color: ColorsManager.lightGrayColor),
onPressed: onPreviousWeek,
),
const SizedBox(width: 10),
Text(
_getMonthYearText(weekStart, weekEnd),
style: const TextStyle(
color: ColorsManager.lightGrayColor,
fontSize: 14,
fontWeight: FontWeight.w400,
),
),
const SizedBox(width: 10),
IconButton(
iconSize: 15,
icon: const Icon(Icons.arrow_forward_ios,
color: ColorsManager.lightGrayColor),
onPressed: onNextWeek,
),
],
),
);
}
String _getMonthYearText(DateTime start, DateTime end) {
final startMonth = DateFormat('MMM').format(start);
final endMonth = DateFormat('MMM').format(end);
final year = start.year == end.year
? start.year.toString()
: '${start.year}-${end.year}';
if (start.month == end.month) {
return '$startMonth $year';
} else {
return '$startMonth - $endMonth $year';
}
}
}
@@ -1,6 +1,8 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:calendar_view/calendar_view.dart'; import 'package:calendar_view/calendar_view.dart';
import 'package:intl/intl.dart'; import 'package:syncrow_web/pages/access_management/booking_system/presentation/view/widgets/event_tile_widget.dart';
import 'package:syncrow_web/pages/access_management/booking_system/presentation/view/widgets/time_line_widget.dart';
import 'package:syncrow_web/pages/access_management/booking_system/presentation/view/widgets/week_day_header.dart';
import 'package:syncrow_web/utils/color_manager.dart'; import 'package:syncrow_web/utils/color_manager.dart';
class WeeklyCalendarPage extends StatelessWidget { class WeeklyCalendarPage extends StatelessWidget {
@@ -20,6 +22,12 @@ class WeeklyCalendarPage extends StatelessWidget {
this.endTime, this.endTime,
this.selectedDateFromSideBarCalender, this.selectedDateFromSideBarCalender,
}); });
static const double timeLineWidth = 65;
static const int totalDays = 7;
static const double dayColumnWidth = 220;
final double calendarContentWidth =
timeLineWidth + (totalDays * dayColumnWidth);
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
@@ -49,109 +57,107 @@ class WeeklyCalendarPage extends StatelessWidget {
); );
} }
final weekDays = _getWeekDays(weekStart);
final selectedDayIndex =
weekDays.indexWhere((d) => isSameDay(d, selectedDate));
final selectedSidebarIndex = selectedDateFromSideBarCalender == null
? -1
: weekDays
.indexWhere((d) => isSameDay(d, selectedDateFromSideBarCalender!));
const double timeLineWidth = 80; const double timeLineWidth = 65;
const int totalDays = 7;
return LayoutBuilder( return LayoutBuilder(
builder: (context, constraints) { builder: (context, constraints) {
final double calendarWidth = constraints.maxWidth;
final double dayColumnWidth =
(calendarWidth - timeLineWidth) / totalDays - 0.1;
return Padding( bool isInRange(DateTime date, DateTime start, DateTime end) {
padding: const EdgeInsets.only(left: 25.0, right: 25.0, top: 25), !date.isBefore(start) && !date.isAfter(end);
// remove this line and Check if the date is within the range
return false;
}
return SingleChildScrollView(
scrollDirection: Axis.horizontal,
child: SizedBox(
width: calendarContentWidth,
child: Padding(
padding:
const EdgeInsets.only(left: 25.0, right: 25.0, top: 25),
child: Stack( child: Stack(
children: [ children: [
WeekView( Container(
child: WeekView(
minuteSlotSize: MinuteSlotSize.minutes15,
weekDetectorBuilder: ({
required date,
required height,
required heightPerMinute,
required minuteSlotSize,
required width,
}) {
final isSelected = isSameDay(date, selectedDate);
final isSidebarSelected =
selectedDateFromSideBarCalender != null &&
isSameDay(
date, selectedDateFromSideBarCalender!);
if (isSidebarSelected && !isSelected) {
return Container(
height: height,
width: width,
decoration: BoxDecoration(
color: Colors.orange.withOpacity(0.13),
borderRadius: BorderRadius.circular(8),
),
);
} else if (isSelected) {
return Container(
height: height,
width: width,
decoration: BoxDecoration(
color:
ColorsManager.spaceColor.withOpacity(0.07),
borderRadius: BorderRadius.circular(8),
),
);
}
return const SizedBox.shrink();
},
// weekDetectorBuilder: ({
// required date,
// required height,
// required heightPerMinute,
// required minuteSlotSize,
// required width,
// }) {
// return isInRange(date, highlightStart, highlightEnd)
// ? HatchedColumnBackground(
// backgroundColor: ColorsManager.grey800,
// lineColor: ColorsManager.textGray,
// opacity: 0.3,
// stripeSpacing: 12,
// borderRadius: BorderRadius.circular(8),
// )
// : const SizedBox();
// },
pageViewPhysics: const NeverScrollableScrollPhysics(), pageViewPhysics: const NeverScrollableScrollPhysics(),
key: ValueKey(weekStart), key: ValueKey(weekStart),
controller: eventController, controller: eventController,
initialDay: weekStart, initialDay: weekStart,
startHour: startHour - 1, startHour: startHour - 1,
endHour: endHour, endHour: endHour,
heightPerMinute: 1.1, heightPerMinute: 1.7,
showLiveTimeLineInAllDays: false, showLiveTimeLineInAllDays: false,
showVerticalLines: true, showVerticalLines: true,
emulateVerticalOffsetBy: -80, emulateVerticalOffsetBy: -80,
startDay: WeekDays.monday, startDay: WeekDays.monday,
liveTimeIndicatorSettings: const LiveTimeIndicatorSettings( liveTimeIndicatorSettings:
const LiveTimeIndicatorSettings(
showBullet: false, showBullet: false,
height: 0, height: 0,
), ),
weekDayBuilder: (date) { weekDayBuilder: (date) {
final index = weekDays.indexWhere((d) => isSameDay(d, date)); return WeekDayHeader(
final isSelectedDay = index == selectedDayIndex; date: date,
return Column( isSelectedDay: isSameDay(date, selectedDate),
children: [
Text(
DateFormat('EEE').format(date).toUpperCase(),
style: TextStyle(
fontWeight: FontWeight.w400,
fontSize: 14,
color: isSelectedDay ? Colors.blue : Colors.black,
),
),
Text(
DateFormat('d').format(date),
style: TextStyle(
fontWeight: FontWeight.w700,
fontSize: 20,
color: isSelectedDay
? ColorsManager.blue1
: ColorsManager.blackColor,
),
),
],
); );
}, },
timeLineBuilder: (date) { timeLineBuilder: (date) {
int hour = date.hour == 0 return TimeLineWidget(date: date);
? 12
: (date.hour > 12 ? date.hour - 12 : date.hour);
String period = date.hour >= 12 ? 'PM' : 'AM';
return Container(
height: 60,
alignment: Alignment.center,
child: RichText(
text: TextSpan(
children: [
TextSpan(
text: '$hour',
style: const TextStyle(
fontWeight: FontWeight.w700,
fontSize: 24,
color: ColorsManager.blackColor,
),
),
WidgetSpan(
child: Padding(
padding: const EdgeInsets.only(left: 2, top: 6),
child: Text(
period,
style: const TextStyle(
fontWeight: FontWeight.w400,
fontSize: 12,
color: ColorsManager.blackColor,
letterSpacing: 1,
),
),
),
alignment: PlaceholderAlignment.baseline,
baseline: TextBaseline.alphabetic,
),
],
),
),
);
}, },
timeLineWidth: timeLineWidth, timeLineWidth: timeLineWidth,
weekPageHeaderBuilder: (start, end) => Container(), weekPageHeaderBuilder: (start, end) => Container(),
@@ -163,8 +169,12 @@ class WeeklyCalendarPage extends StatelessWidget {
mainAxisAlignment: MainAxisAlignment.end, mainAxisAlignment: MainAxisAlignment.end,
children: [ children: [
Text( Text(
firstDayOfWeek.timeZoneName.replaceAll(':00', ''), firstDayOfWeek.timeZoneName
style: Theme.of(context).textTheme.bodyMedium?.copyWith( .replaceAll(':00', ''),
style: Theme.of(context)
.textTheme
.bodyMedium
?.copyWith(
fontSize: 12, fontSize: 12,
color: ColorsManager.blackColor, color: ColorsManager.blackColor,
fontWeight: FontWeight.w400, fontWeight: FontWeight.w400,
@@ -174,82 +184,11 @@ class WeeklyCalendarPage extends StatelessWidget {
), ),
), ),
eventTileBuilder: (date, events, boundary, start, end) { eventTileBuilder: (date, events, boundary, start, end) {
return Container( return EventTileWidget(
margin: events: events,
const EdgeInsets.symmetric(vertical: 2, horizontal: 2),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: events.map((event) {
final bool isEventEnded = event.endTime != null &&
event.endTime!.isBefore(DateTime.now());
return Expanded(
child: Container(
width: double.infinity,
padding: const EdgeInsets.all(6),
decoration: BoxDecoration(
color: isEventEnded
? ColorsManager.lightGrayBorderColor
: ColorsManager.blue1.withOpacity(0.25),
borderRadius: BorderRadius.circular(6),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
DateFormat('h:mm a').format(event.startTime!),
style: const TextStyle(
fontWeight: FontWeight.bold,
fontSize: 12,
color: Colors.black87,
),
),
const SizedBox(height: 2),
Text(
event.title,
style: const TextStyle(
fontSize: 12,
color: ColorsManager.blackColor,
),
),
],
),
),
);
}).toList(),
),
); );
}, },
), ),
if (selectedDayIndex >= 0)
Positioned(
left: (timeLineWidth + 3) +
(dayColumnWidth - 8) * (selectedDayIndex - 0.01),
top: 0,
bottom: 0,
width: dayColumnWidth,
child: IgnorePointer(
child: Container(
margin: const EdgeInsets.symmetric(
vertical: 0, horizontal: 4),
color: ColorsManager.spaceColor.withOpacity(0.07),
),
),
),
if (selectedSidebarIndex >= 0 &&
selectedSidebarIndex != selectedDayIndex)
Positioned(
left: (timeLineWidth + 3) +
(dayColumnWidth - 8) * (selectedSidebarIndex - 0.01),
top: 0,
bottom: 0,
width: dayColumnWidth,
child: IgnorePointer(
child: Container(
margin: const EdgeInsets.symmetric(
vertical: 0, horizontal: 4),
color: Colors.orange.withOpacity(0.14),
),
),
), ),
Positioned( Positioned(
right: 0, right: 0,
@@ -264,16 +203,13 @@ class WeeklyCalendarPage extends StatelessWidget {
), ),
], ],
), ),
); ),
));
}, },
); );
} }
List<DateTime> _getWeekDays(DateTime date) {
final int weekday = date.weekday;
final DateTime monday = date.subtract(Duration(days: weekday - 1));
return List.generate(7, (i) => monday.add(Duration(days: i)));
}
} }
bool isSameDay(DateTime d1, DateTime d2) { bool isSameDay(DateTime d1, DateTime d2) {
+3 -2
View File
@@ -132,6 +132,8 @@ class _DynamicTableState extends State<DynamicTable> {
child: SingleChildScrollView( child: SingleChildScrollView(
controller: _horizontalScrollController, controller: _horizontalScrollController,
scrollDirection: Axis.horizontal, scrollDirection: Axis.horizontal,
physics:
widget.isEmpty ? const NeverScrollableScrollPhysics() : null,
child: SizedBox( child: SizedBox(
width: _totalTableWidth, width: _totalTableWidth,
child: Column( child: Column(
@@ -164,7 +166,6 @@ class _DynamicTableState extends State<DynamicTable> {
], ],
), ),
), ),
Expanded( Expanded(
child: widget.isEmpty child: widget.isEmpty
? _buildEmptyState() ? _buildEmptyState()
@@ -265,7 +266,7 @@ class _DynamicTableState extends State<DynamicTable> {
), ),
], ],
), ),
SizedBox(height: widget.size.height * 0.5), SizedBox(height: widget.size.height * 0.2),
], ],
), ),
); );
@@ -46,15 +46,16 @@ class DeviceManagementBloc
final projectUuid = await ProjectManager.getProjectUUID() ?? ''; final projectUuid = await ProjectManager.getProjectUUID() ?? '';
if (spaceBloc.state.selectedCommunities.isEmpty) { if (spaceBloc.state.selectedCommunities.isEmpty) {
devices = await DevicesManagementApi().fetchDevices('', '', projectUuid); devices = await DevicesManagementApi().fetchDevices(
projectUuid,
);
} else { } else {
for (final community in spaceBloc.state.selectedCommunities) { for (var community in spaceBloc.state.selectedCommunities) {
final spacesList = final spacesList =
spaceBloc.state.selectedCommunityAndSpaces[community] ?? []; spaceBloc.state.selectedCommunityAndSpaces[community] ?? [];
for (final space in spacesList) { devices.addAll(await DevicesManagementApi().fetchDevices(projectUuid,
devices.addAll(await DevicesManagementApi() spacesId: spacesList,
.fetchDevices(community, space, projectUuid)); communities: spaceBloc.state.selectedCommunities));
}
} }
} }
@@ -158,7 +159,8 @@ class DeviceManagementBloc
add(FilterDevices(_getFilterFromIndex(_selectedIndex))); add(FilterDevices(_getFilterFromIndex(_selectedIndex)));
} }
void _onSelectDevice(SelectDevice event, Emitter<DeviceManagementState> emit) { void _onSelectDevice(
SelectDevice event, Emitter<DeviceManagementState> emit) {
final selectedUuid = event.selectedDevice.uuid; final selectedUuid = event.selectedDevice.uuid;
if (_selectedDevices.any((device) => device.uuid == selectedUuid)) { if (_selectedDevices.any((device) => device.uuid == selectedUuid)) {
@@ -254,7 +256,8 @@ class DeviceManagementBloc
_onlineCount = _devices.where((device) => device.online == true).length; _onlineCount = _devices.where((device) => device.online == true).length;
_offlineCount = _devices.where((device) => device.online == false).length; _offlineCount = _devices.where((device) => device.online == false).length;
_lowBatteryCount = _devices _lowBatteryCount = _devices
.where((device) => device.batteryLevel != null && device.batteryLevel! < 20) .where((device) =>
device.batteryLevel != null && device.batteryLevel! < 20)
.length; .length;
} }
@@ -271,7 +274,8 @@ class DeviceManagementBloc
} }
} }
void _onSearchDevices(SearchDevices event, Emitter<DeviceManagementState> emit) { void _onSearchDevices(
SearchDevices event, Emitter<DeviceManagementState> emit) {
if ((event.community == null || event.community!.isEmpty) && if ((event.community == null || event.community!.isEmpty) &&
(event.unitName == null || event.unitName!.isEmpty) && (event.unitName == null || event.unitName!.isEmpty) &&
(event.deviceNameOrProductName == null || (event.deviceNameOrProductName == null ||
@@ -435,8 +439,8 @@ class DeviceManagementBloc
final selectedDevices = loaded.selectedDevice?.map((device) { final selectedDevices = loaded.selectedDevice?.map((device) {
if (device.uuid == event.deviceId) { if (device.uuid == event.deviceId) {
return device.copyWith( return device.copyWith(
subspace: subspace: device.subspace
device.subspace?.copyWith(subspaceName: event.newSubSpaceName)); ?.copyWith(subspaceName: event.newSubSpaceName));
} }
return device; return device;
}).toList(); }).toList();
@@ -24,12 +24,12 @@ class DeviceManagementPage extends StatefulWidget with HelperResponsiveLayout {
} }
class _DeviceManagementPageState extends State<DeviceManagementPage> { class _DeviceManagementPageState extends State<DeviceManagementPage> {
@override @override
void initState() { void initState() {
context.read<SpaceTreeBloc>().add(InitialEvent()); context.read<SpaceTreeBloc>().add(InitialEvent());
super.initState(); super.initState();
} }
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return MultiBlocProvider( return MultiBlocProvider(
@@ -90,7 +90,7 @@ class _DeviceManagementPageState extends State<DeviceManagementPage> {
const TriggerSwitchTabsEvent(isRoutineTab: true)); const TriggerSwitchTabsEvent(isRoutineTab: true));
}, },
child: Text( child: Text(
'Routines', 'Workflow Automation',
style: context.textTheme.titleMedium?.copyWith( style: context.textTheme.titleMedium?.copyWith(
color: state.routineTab color: state.routineTab
? ColorsManager.whiteColors ? ColorsManager.whiteColors
@@ -29,7 +29,9 @@ class CountdownModeButtons extends StatelessWidget {
children: [ children: [
Expanded( Expanded(
child: DefaultButton( child: DefaultButton(
elevation: 2.5,
height: 40, height: 40,
borderRadius: 8,
onPressed: () => Navigator.pop(context), onPressed: () => Navigator.pop(context),
backgroundColor: ColorsManager.boxColor, backgroundColor: ColorsManager.boxColor,
child: Text('Cancel', style: context.textTheme.bodyMedium), child: Text('Cancel', style: context.textTheme.bodyMedium),
@@ -39,6 +41,8 @@ class CountdownModeButtons extends StatelessWidget {
Expanded( Expanded(
child: isActive child: isActive
? DefaultButton( ? DefaultButton(
elevation: 2.5,
borderRadius: 8,
height: 40, height: 40,
onPressed: () { onPressed: () {
context.read<ScheduleBloc>().add( context.read<ScheduleBloc>().add(
@@ -49,10 +53,12 @@ class CountdownModeButtons extends StatelessWidget {
), ),
); );
}, },
backgroundColor: Colors.red, backgroundColor: ColorsManager.red100,
child: const Text('Stop'), child: const Text('Stop'),
) )
: DefaultButton( : DefaultButton(
elevation: 2.5,
borderRadius: 8,
height: 40, height: 40,
onPressed: () { onPressed: () {
context.read<ScheduleBloc>().add( context.read<ScheduleBloc>().add(
@@ -63,7 +69,7 @@ class CountdownModeButtons extends StatelessWidget {
countDownCode: countDownCode), countDownCode: countDownCode),
); );
}, },
backgroundColor: ColorsManager.primaryColor, backgroundColor: ColorsManager.primaryColorWithOpacity,
child: const Text('Save'), child: const Text('Save'),
), ),
), ),
@@ -226,6 +226,7 @@ class _CountdownInchingViewState extends State<CountdownInchingView> {
index.toString().padLeft(2, '0'), index.toString().padLeft(2, '0'),
style: TextStyle( style: TextStyle(
fontSize: 24, fontSize: 24,
fontWeight: FontWeight.w400,
color: isActive ? ColorsManager.grayColor : Colors.black, color: isActive ? ColorsManager.grayColor : Colors.black,
), ),
), ),
@@ -240,7 +241,8 @@ class _CountdownInchingViewState extends State<CountdownInchingView> {
label, label,
style: const TextStyle( style: const TextStyle(
color: ColorsManager.grayColor, color: ColorsManager.grayColor,
fontSize: 18, fontSize: 24,
fontWeight: FontWeight.w400,
), ),
), ),
], ],
@@ -31,12 +31,11 @@ class BuildScheduleView extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return BlocProvider( return BlocProvider(
create: (_) => ScheduleBloc( create: (_) => ScheduleBloc(deviceId: deviceUuid,)
deviceId: deviceUuid,
)
..add(ScheduleGetEvent(category: category)) ..add(ScheduleGetEvent(category: category))
..add(ScheduleFetchStatusEvent( ..add(ScheduleFetchStatusEvent(
deviceId: deviceUuid, countdownCode: countdownCode ?? '')), deviceId: deviceUuid,
countdownCode: countdownCode ?? '')),
child: Dialog( child: Dialog(
backgroundColor: Colors.white, backgroundColor: Colors.white,
insetPadding: const EdgeInsets.all(20), insetPadding: const EdgeInsets.all(20),
@@ -77,7 +76,8 @@ class BuildScheduleView extends StatelessWidget {
category: category, category: category,
time: '', time: '',
function: Status( function: Status(
code: code.toString(), value: null), code: code.toString(),
value: true),
days: [], days: [],
), ),
isEdit: false, isEdit: false,
@@ -13,9 +13,9 @@ class ScheduleHeader extends StatelessWidget {
Text( Text(
'Scheduling', 'Scheduling',
style: TextStyle( style: TextStyle(
fontWeight: FontWeight.bold, color: ColorsManager.primaryColorWithOpacity,
fontSize: 22, fontWeight: FontWeight.w700,
color: ColorsManager.dialogBlueTitle, fontSize: 30,
), ),
), ),
Container( Container(
@@ -27,7 +27,7 @@ class ScheduleManagementUI extends StatelessWidget {
width: 170, width: 170,
height: 40, height: 40,
child: DefaultButton( child: DefaultButton(
borderColor: ColorsManager.boxColor, borderColor: ColorsManager.grayColor.withOpacity(0.5),
padding: 2, padding: 2,
backgroundColor: ColorsManager.graysColor, backgroundColor: ColorsManager.graysColor,
borderRadius: 15, borderRadius: 15,
@@ -19,6 +19,8 @@ class ScheduleModeButtons extends StatelessWidget {
children: [ children: [
Expanded( Expanded(
child: DefaultButton( child: DefaultButton(
elevation: 2.5,
borderRadius: 8,
height: 40, height: 40,
onPressed: () { onPressed: () {
Navigator.pop(context); Navigator.pop(context);
@@ -33,9 +35,11 @@ class ScheduleModeButtons extends StatelessWidget {
const SizedBox(width: 20), const SizedBox(width: 20),
Expanded( Expanded(
child: DefaultButton( child: DefaultButton(
elevation: 2.5,
borderRadius: 8,
height: 40, height: 40,
onPressed: onSave, onPressed: onSave,
backgroundColor: ColorsManager.primaryColor, backgroundColor: ColorsManager.primaryColorWithOpacity,
child: const Text('Save'), child: const Text('Save'),
), ),
), ),
@@ -35,12 +35,12 @@ class ScheduleModeSelector extends StatelessWidget {
), ),
const SizedBox(height: 4), const SizedBox(height: 4),
Row( Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [ children: [
_buildRadioTile( _buildRadioTile(
context, 'Countdown', ScheduleModes.countdown, currentMode), context, 'Countdown', ScheduleModes.countdown, currentMode),
_buildRadioTile( _buildRadioTile(
context, 'Schedule', ScheduleModes.schedule, currentMode), context, 'Schedule', ScheduleModes.schedule, currentMode),
const Spacer(flex: 1),
// _buildRadioTile( // _buildRadioTile(
// context, 'Circulate', ScheduleModes.circulate, currentMode), // context, 'Circulate', ScheduleModes.circulate, currentMode),
// _buildRadioTile( // _buildRadioTile(
@@ -65,6 +65,7 @@ class ScheduleModeSelector extends StatelessWidget {
style: context.textTheme.bodySmall!.copyWith( style: context.textTheme.bodySmall!.copyWith(
fontSize: 13, fontSize: 13,
color: ColorsManager.blackColor, color: ColorsManager.blackColor,
fontWeight: FontWeight.w400,
), ),
), ),
leading: Radio<ScheduleModes>( leading: Radio<ScheduleModes>(
@@ -1,6 +1,8 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:syncrow_web/pages/device_managment/schedule_device/schedule_widgets/schedule_mode_buttons.dart';
import 'package:syncrow_web/pages/device_managment/water_heater/models/schedule_entry.dart'; import 'package:syncrow_web/pages/device_managment/water_heater/models/schedule_entry.dart';
import 'package:syncrow_web/pages/device_managment/all_devices/models/device_status.dart'; import 'package:syncrow_web/pages/device_managment/all_devices/models/device_status.dart';
import 'package:syncrow_web/utils/color_manager.dart';
class ScheduleDialogHelper { class ScheduleDialogHelper {
static const List<String> allDays = [ static const List<String> allDays = [
@@ -56,8 +58,9 @@ class ScheduleDialogHelper {
Text( Text(
isEdit ? 'Edit Schedule' : 'Add Schedule', isEdit ? 'Edit Schedule' : 'Add Schedule',
style: Theme.of(context).textTheme.titleLarge!.copyWith( style: Theme.of(context).textTheme.titleLarge!.copyWith(
color: Colors.blue, color: ColorsManager.primaryColorWithOpacity,
fontWeight: FontWeight.bold, fontWeight: FontWeight.w700,
fontSize: 30,
), ),
), ),
const SizedBox(), const SizedBox(),
@@ -69,9 +72,9 @@ class ScheduleDialogHelper {
height: 40, height: 40,
child: ElevatedButton( child: ElevatedButton(
style: ElevatedButton.styleFrom( style: ElevatedButton.styleFrom(
backgroundColor: Colors.grey[200], backgroundColor: ColorsManager.boxColor,
shape: RoundedRectangleBorder( shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(15), borderRadius: BorderRadius.circular(8),
), ),
), ),
onPressed: () async { onPressed: () async {
@@ -110,19 +113,8 @@ class ScheduleDialogHelper {
], ],
), ),
actions: [ actions: [
SizedBox( ScheduleModeButtons(
width: 100, onSave: () {
child: OutlinedButton(
onPressed: () {
Navigator.pop(ctx, null);
},
child: const Text('Cancel'),
),
),
SizedBox(
width: 100,
child: ElevatedButton(
onPressed: () {
dynamic temp; dynamic temp;
if (deviceType == 'CUR_2') { if (deviceType == 'CUR_2') {
temp = functionOn! ? 'open' : 'close'; temp = functionOn! ? 'open' : 'close';
@@ -141,8 +133,7 @@ class ScheduleDialogHelper {
); );
Navigator.pop(ctx, entry); Navigator.pop(ctx, entry);
}, },
child: const Text('Save'), ),
)),
], ],
); );
}, },
@@ -153,6 +153,7 @@ class EditUserModel {
final String? jobTitle; // can be empty final String? jobTitle; // can be empty
final String roleType; // e.g. "ADMIN" final String roleType; // e.g. "ADMIN"
final List<UserSpaceModel> spaces; final List<UserSpaceModel> spaces;
final String? companyName;
EditUserModel({ EditUserModel({
required this.uuid, required this.uuid,
@@ -167,6 +168,7 @@ class EditUserModel {
required this.jobTitle, required this.jobTitle,
required this.roleType, required this.roleType,
required this.spaces, required this.spaces,
this.companyName,
}); });
/// Create a [UserData] from JSON data /// Create a [UserData] from JSON data
@@ -182,6 +184,7 @@ class EditUserModel {
invitedBy: json['invitedBy'] as String, invitedBy: json['invitedBy'] as String,
phoneNumber: json['phoneNumber'] ?? '', phoneNumber: json['phoneNumber'] ?? '',
jobTitle: json['jobTitle'] ?? '', jobTitle: json['jobTitle'] ?? '',
companyName: json['companyName'] as String?,
roleType: json['roleType'] as String, roleType: json['roleType'] as String,
spaces: (json['spaces'] as List<dynamic>) spaces: (json['spaces'] as List<dynamic>)
.map((e) => UserSpaceModel.fromJson(e as Map<String, dynamic>)) .map((e) => UserSpaceModel.fromJson(e as Map<String, dynamic>))
@@ -12,7 +12,7 @@ class RolesUserModel {
final dynamic jobTitle; final dynamic jobTitle;
final dynamic createdDate; final dynamic createdDate;
final dynamic createdTime; final dynamic createdTime;
final String? companyName;
RolesUserModel({ RolesUserModel({
required this.uuid, required this.uuid,
required this.createdAt, required this.createdAt,
@@ -27,6 +27,7 @@ class RolesUserModel {
this.jobTitle, this.jobTitle,
required this.createdDate, required this.createdDate,
required this.createdTime, required this.createdTime,
this.companyName,
}); });
factory RolesUserModel.fromJson(Map<String, dynamic> json) { factory RolesUserModel.fromJson(Map<String, dynamic> json) {
@@ -47,6 +48,7 @@ class RolesUserModel {
: json['jobTitle'], : json['jobTitle'],
createdDate: json['createdDate'], createdDate: json['createdDate'],
createdTime: json['createdTime'], createdTime: json['createdTime'],
companyName: json['companyName'] as String?,
); );
} }
} }
@@ -52,7 +52,7 @@ class UsersBloc extends Bloc<UsersEvent, UsersState> {
final TextEditingController lastNameController = TextEditingController(); final TextEditingController lastNameController = TextEditingController();
final TextEditingController emailController = TextEditingController(); final TextEditingController emailController = TextEditingController();
final TextEditingController phoneController = TextEditingController(); final TextEditingController phoneController = TextEditingController();
final TextEditingController jobTitleController = TextEditingController(); final TextEditingController companyNameController = TextEditingController();
final TextEditingController roleSearchController = TextEditingController(); final TextEditingController roleSearchController = TextEditingController();
bool? isCompleteBasics; bool? isCompleteBasics;
@@ -352,7 +352,7 @@ class UsersBloc extends Bloc<UsersEvent, UsersState> {
bool res = await UserPermissionApi().sendInviteUser( bool res = await UserPermissionApi().sendInviteUser(
email: emailController.text, email: emailController.text,
firstName: firstNameController.text, firstName: firstNameController.text,
jobTitle: jobTitleController.text, companyName: companyNameController.text,
lastName: lastNameController.text, lastName: lastNameController.text,
phoneNumber: phoneController.text, phoneNumber: phoneController.text,
roleUuid: roleSelected, roleUuid: roleSelected,
@@ -405,7 +405,7 @@ class UsersBloc extends Bloc<UsersEvent, UsersState> {
bool res = await UserPermissionApi().editInviteUser( bool res = await UserPermissionApi().editInviteUser(
userId: event.userId, userId: event.userId,
firstName: firstNameController.text, firstName: firstNameController.text,
jobTitle: jobTitleController.text, companyName: companyNameController.text,
lastName: lastNameController.text, lastName: lastNameController.text,
phoneNumber: phoneController.text, phoneNumber: phoneController.text,
roleUuid: roleSelected, roleUuid: roleSelected,
@@ -529,7 +529,7 @@ class UsersBloc extends Bloc<UsersEvent, UsersState> {
lastNameController.text = res.lastName; lastNameController.text = res.lastName;
emailController.text = res.email; emailController.text = res.email;
phoneController.text = res.phoneNumber ?? ''; phoneController.text = res.phoneNumber ?? '';
jobTitleController.text = res.jobTitle ?? ''; companyNameController.text = res.companyName ?? '';
res.roleType; res.roleType;
res.spaces.map((space) { res.spaces.map((space) {
selectedIds.add(space.uuid); selectedIds.add(space.uuid);
@@ -645,7 +645,7 @@ class UsersBloc extends Bloc<UsersEvent, UsersState> {
lastNameController.dispose(); lastNameController.dispose();
emailController.dispose(); emailController.dispose();
phoneController.dispose(); phoneController.dispose();
jobTitleController.dispose(); companyNameController.dispose();
roleSearchController.dispose(); roleSearchController.dispose();
return super.close(); return super.close();
} }
@@ -317,7 +317,7 @@ class BasicsView extends StatelessWidget {
child: Row( child: Row(
children: [ children: [
Text( Text(
'Job Title', 'Company Name',
style: context.textTheme.bodyMedium?.copyWith( style: context.textTheme.bodyMedium?.copyWith(
fontWeight: FontWeight.w400, fontWeight: FontWeight.w400,
fontSize: 13, fontSize: 13,
@@ -328,11 +328,11 @@ class BasicsView extends StatelessWidget {
Padding( Padding(
padding: const EdgeInsets.all(8.0), padding: const EdgeInsets.all(8.0),
child: TextFormField( child: TextFormField(
controller: _blocRole.jobTitleController, controller: _blocRole.companyNameController,
style: style:
const TextStyle(color: ColorsManager.blackColor), const TextStyle(color: ColorsManager.blackColor),
decoration: inputTextFormDeco( decoration: inputTextFormDeco(
hintText: "Job Title (Optional)") hintText: 'Company Name (Optional)')
.copyWith( .copyWith(
hintStyle: context.textTheme.bodyMedium?.copyWith( hintStyle: context.textTheme.bodyMedium?.copyWith(
fontWeight: FontWeight.w400, fontWeight: FontWeight.w400,
@@ -411,7 +411,7 @@ class UsersPage extends StatelessWidget {
titles: const [ titles: const [
"Full Name", "Full Name",
"Email Address", "Email Address",
"Job Title", "Company Name",
"Role", "Role",
"Creation Date", "Creation Date",
"Creation Time", "Creation Time",
@@ -424,7 +424,7 @@ class UsersPage extends StatelessWidget {
return [ return [
Text('${user.firstName} ${user.lastName}'), Text('${user.firstName} ${user.lastName}'),
Text(user.email), Text(user.email),
Text(user.jobTitle), Center(child: Text(user.companyName ?? '-')),
Text(user.roleType ?? ''), Text(user.roleType ?? ''),
Text(user.createdDate ?? ''), Text(user.createdDate ?? ''),
Text(user.createdTime ?? ''), Text(user.createdTime ?? ''),
@@ -936,16 +936,19 @@ Future<void> _onLoadScenes(
for (var communityId in spaceBloc.state.selectedCommunities) { for (var communityId in spaceBloc.state.selectedCommunities) {
List<String> spacesList = List<String> spacesList =
spaceBloc.state.selectedCommunityAndSpaces[communityId] ?? []; spaceBloc.state.selectedCommunityAndSpaces[communityId] ?? [];
for (var spaceId in spacesList) {
devices.addAll(await DevicesManagementApi() devices.addAll(await DevicesManagementApi().fetchDevices(
.fetchDevices(communityId, spaceId, projectUuid)); projectUuid,
} spacesId: spacesList,
communities: spaceBloc.state.selectedCommunities,
));
} }
} else { } else {
devices.addAll(await DevicesManagementApi().fetchDevices( devices.addAll(await DevicesManagementApi().fetchDevices(
createRoutineBloc.selectedCommunityId, projectUuid,
createRoutineBloc.selectedSpaceId, spacesId: [createRoutineBloc.selectedSpaceId],
projectUuid)); communities: spaceBloc.state.selectedCommunities,
));
} }
emit(state.copyWith(isLoading: false, devices: devices)); emit(state.copyWith(isLoading: false, devices: devices));
@@ -96,9 +96,7 @@ class _WallPresenceSensorState extends State<FlushPresenceSensor> {
child: Column( child: Column(
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
children: [ children: [
DialogHeader(widget.dialogType == 'THEN' const DialogHeader('Presence Sensor'),
? 'Presence Sensor Functions'
: 'Presence Sensor Condition'),
Expanded(child: _buildMainContent(context, state)), Expanded(child: _buildMainContent(context, state)),
_buildDialogFooter(context, state), _buildDialogFooter(context, state),
], ],
@@ -0,0 +1,71 @@
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';
abstract final class SpacesRecursiveHelper {
const SpacesRecursiveHelper._();
static List<SpaceModel> recusrivelyUpdate(
List<SpaceModel> spaces,
SpaceDetailsModel updatedSpace,
) {
return spaces.map((space) {
final isUpdatedSpace = space.uuid == updatedSpace.uuid;
if (isUpdatedSpace) {
return space.copyWith(
spaceName: updatedSpace.spaceName,
icon: updatedSpace.icon,
);
}
final hasChildren = space.children.isNotEmpty;
if (hasChildren) {
return space.copyWith(
children: recusrivelyUpdate(space.children, updatedSpace),
);
}
return space;
}).toList();
}
static List<SpaceModel> recusrivelyDelete(
List<SpaceModel> spaces,
String spaceUuid,
) {
final updatedSpaces = spaces.map((space) {
if (space.uuid == spaceUuid) return null;
if (space.children.isNotEmpty) {
return space.copyWith(
children: recusrivelyDelete(space.children, spaceUuid),
);
}
return space;
}).toList();
final nonNullSpaces = updatedSpaces.whereType<SpaceModel>().toList();
return nonNullSpaces;
}
static List<SpaceModel> recursivelyInsert({
required List<SpaceModel> spaces,
required String parentUuid,
required SpaceModel newSpace,
}) {
return spaces.map((space) {
final isParentSpace = space.uuid == parentUuid;
if (isParentSpace) {
return space.copyWith(
children: [...space.children, newSpace],
);
}
final hasChildren = space.children.isNotEmpty;
if (hasChildren) {
return space.copyWith(
children: recursivelyInsert(
spaces: space.children,
parentUuid: parentUuid,
newSpace: newSpace,
),
);
}
return space;
}).toList();
}
}
@@ -5,13 +5,14 @@ import 'package:syncrow_web/utils/color_manager.dart';
class SpacesConnectionsArrowPainter extends CustomPainter { class SpacesConnectionsArrowPainter extends CustomPainter {
final List<SpaceConnectionModel> connections; final List<SpaceConnectionModel> connections;
final Map<String, Offset> positions; final Map<String, Offset> positions;
final double cardWidth = 150.0; final Map<String, double> cardWidths;
final double cardHeight = 90.0; final double cardHeight = 90.0;
final Set<String> highlightedUuids; final Set<String> highlightedUuids;
SpacesConnectionsArrowPainter({ SpacesConnectionsArrowPainter({
required this.connections, required this.connections,
required this.positions, required this.positions,
required this.cardWidths,
required this.highlightedUuids, required this.highlightedUuids,
}); });
@@ -29,19 +30,30 @@ class SpacesConnectionsArrowPainter extends CustomPainter {
final from = positions[connection.from]; final from = positions[connection.from];
final to = positions[connection.to]; final to = positions[connection.to];
final fromWidth = cardWidths[connection.from] ?? 150.0;
final toWidth = cardWidths[connection.to] ?? 150.0;
if (from != null && to != null) { if (from != null && to != null) {
final startPoint = final startPoint =
Offset(from.dx + cardWidth / 2, from.dy + cardHeight - 10); Offset(from.dx + fromWidth / 2, from.dy + cardHeight - 10);
final endPoint = Offset(to.dx + cardWidth / 2, to.dy); final endPoint = Offset(to.dx + toWidth / 2, to.dy);
final path = Path()..moveTo(startPoint.dx, startPoint.dy); final path = Path()..moveTo(startPoint.dx, startPoint.dy);
final controlPoint1 = Offset(startPoint.dx, startPoint.dy + 20); if ((startPoint.dx - endPoint.dx).abs() < 1.0) {
final controlPoint2 = Offset(endPoint.dx, endPoint.dy - 60); path.lineTo(endPoint.dx, endPoint.dy);
} else {
path.cubicTo(controlPoint1.dx, controlPoint1.dy, controlPoint2.dx, final controlPoint1 = Offset(startPoint.dx, startPoint.dy + 100);
controlPoint2.dy, endPoint.dx, endPoint.dy); final controlPoint2 = Offset(endPoint.dx, endPoint.dy - 100);
path.cubicTo(
controlPoint1.dx,
controlPoint1.dy,
controlPoint2.dx,
controlPoint2.dy,
endPoint.dx,
endPoint.dy,
);
}
canvas.drawPath(path, paint); canvas.drawPath(path, paint);
@@ -51,7 +63,7 @@ class SpacesConnectionsArrowPainter extends CustomPainter {
: ColorsManager.blackColor.withValues(alpha: 0.5) : ColorsManager.blackColor.withValues(alpha: 0.5)
..style = PaintingStyle.fill ..style = PaintingStyle.fill
..blendMode = BlendMode.srcIn; ..blendMode = BlendMode.srcIn;
canvas.drawCircle(endPoint, 4, circlePaint); canvas.drawCircle(endPoint, 6, circlePaint);
} }
} }
} }
@@ -9,8 +9,10 @@ import 'package:syncrow_web/pages/space_management_v2/modules/communities/presen
import 'package:syncrow_web/pages/space_management_v2/modules/communities/presentation/communities_tree_selection_bloc/communities_tree_selection_bloc.dart'; import 'package:syncrow_web/pages/space_management_v2/modules/communities/presentation/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/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/products/presentation/bloc/products_bloc.dart';
import 'package:syncrow_web/pages/space_management_v2/modules/reorder_spaces/data/services/remote_reorder_spaces_service.dart';
import 'package:syncrow_web/pages/space_management_v2/modules/reorder_spaces/presentation/bloc/reorder_spaces_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/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/data/services/unique_space_details_spaces_decorator_service.dart';
import 'package:syncrow_web/pages/space_management_v2/modules/space_details/presentation/bloc/space_details_bloc.dart'; import 'package:syncrow_web/pages/space_management_v2/modules/space_details/presentation/bloc/space_details_bloc.dart';
import 'package:syncrow_web/services/api/http_service.dart'; import 'package:syncrow_web/services/api/http_service.dart';
import 'package:syncrow_web/utils/theme/responsive_text_theme.dart'; import 'package:syncrow_web/utils/theme/responsive_text_theme.dart';
@@ -25,15 +27,16 @@ class SpaceManagementPage extends StatefulWidget {
class _SpaceManagementPageState extends State<SpaceManagementPage> { class _SpaceManagementPageState extends State<SpaceManagementPage> {
late final CommunitiesBloc communitiesBloc; late final CommunitiesBloc communitiesBloc;
late final HTTPService _httpService;
@override @override
void initState() { void initState() {
_httpService = HTTPService();
communitiesBloc = CommunitiesBloc( communitiesBloc = CommunitiesBloc(
communitiesService: DebouncedCommunitiesService( communitiesService: DebouncedCommunitiesService(
RemoteCommunitiesService(HTTPService()), RemoteCommunitiesService(_httpService),
), ),
)..add(const LoadCommunities(LoadCommunitiesParam())); )..add(const LoadCommunities(LoadCommunitiesParam()));
super.initState(); super.initState();
} }
@@ -49,14 +52,19 @@ class _SpaceManagementPageState extends State<SpaceManagementPage> {
), ),
BlocProvider( BlocProvider(
create: (context) => SpaceDetailsBloc( create: (context) => SpaceDetailsBloc(
UniqueSubspacesDecorator( UniqueSpaceDetailsSpacesDecoratorService(
RemoteSpaceDetailsService(httpService: HTTPService()), RemoteSpaceDetailsService(httpService: _httpService),
), ),
), ),
), ),
BlocProvider( BlocProvider(
create: (context) => ProductsBloc( create: (context) => ProductsBloc(
RemoteProductsService(HTTPService()), RemoteProductsService(_httpService),
),
),
BlocProvider(
create: (context) => ReorderSpacesBloc(
RemoteReorderSpacesService(_httpService),
), ),
), ),
], ],
@@ -1,5 +1,6 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:syncrow_web/pages/space_management_v2/main_module/helpers/spaces_recursive_helper.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_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/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/painters/spaces_connections_arrow_painter.dart';
@@ -10,6 +11,8 @@ import 'package:syncrow_web/pages/space_management_v2/modules/communities/domain
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/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/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/presentation/communities_tree_selection_bloc/communities_tree_selection_bloc.dart';
import 'package:syncrow_web/pages/space_management_v2/modules/reorder_spaces/domain/params/reorder_spaces_param.dart';
import 'package:syncrow_web/pages/space_management_v2/modules/reorder_spaces/presentation/bloc/reorder_spaces_bloc.dart';
import 'package:syncrow_web/pages/space_management_v2/modules/space_details/presentation/helpers/space_details_dialog_helper.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'; import 'package:syncrow_web/utils/extension/build_context_x.dart';
@@ -30,10 +33,11 @@ class CommunityStructureCanvas extends StatefulWidget {
class _CommunityStructureCanvasState extends State<CommunityStructureCanvas> class _CommunityStructureCanvasState extends State<CommunityStructureCanvas>
with SingleTickerProviderStateMixin { with SingleTickerProviderStateMixin {
final Map<String, Offset> _positions = {}; final Map<String, Offset> _positions = {};
final double _cardWidth = 150.0; final Map<String, double> _cardWidths = {};
final double _cardHeight = 90.0; final double _cardHeight = 90.0;
final double _horizontalSpacing = 150.0; final double _horizontalSpacing = 150.0;
final double _verticalSpacing = 120.0; final double _verticalSpacing = 120.0;
static const double _minCardWidth = 150.0;
late final TransformationController _transformationController; late final TransformationController _transformationController;
late final AnimationController _animationController; late final AnimationController _animationController;
@@ -52,6 +56,7 @@ class _CommunityStructureCanvasState extends State<CommunityStructureCanvas>
@override @override
void didUpdateWidget(covariant CommunityStructureCanvas oldWidget) { void didUpdateWidget(covariant CommunityStructureCanvas oldWidget) {
super.didUpdateWidget(oldWidget); super.didUpdateWidget(oldWidget);
if (widget.selectedSpace == null) return;
if (widget.selectedSpace?.uuid != oldWidget.selectedSpace?.uuid) { if (widget.selectedSpace?.uuid != oldWidget.selectedSpace?.uuid) {
WidgetsBinding.instance.addPostFrameCallback((_) { WidgetsBinding.instance.addPostFrameCallback((_) {
if (mounted) { if (mounted) {
@@ -68,6 +73,34 @@ class _CommunityStructureCanvasState extends State<CommunityStructureCanvas>
super.dispose(); super.dispose();
} }
double _calculateCardWidth(String text) {
final style = context.textTheme.bodyLarge?.copyWith(
fontWeight: FontWeight.bold,
);
final textPainter = TextPainter(
text: TextSpan(text: text, style: style),
maxLines: 1,
textDirection: TextDirection.ltr,
)..layout();
const iconWidth = 40.0;
const horizontalPadding = 10.0;
const contentPadding = 10.0;
final calculatedWidth =
iconWidth + horizontalPadding + textPainter.width + contentPadding;
return calculatedWidth.clamp(_minCardWidth, double.infinity);
}
void _calculateAllCardWidths(List<SpaceModel> spaces) {
for (final space in spaces) {
_cardWidths[space.uuid] = _calculateCardWidth(space.spaceName);
if (space.children.isNotEmpty) {
_calculateAllCardWidths(space.children);
}
}
}
Set<String> _getAllDescendantUuids(SpaceModel space) { Set<String> _getAllDescendantUuids(SpaceModel space) {
final uuids = <String>{}; final uuids = <String>{};
for (final child in space.children) { for (final child in space.children) {
@@ -102,11 +135,12 @@ class _CommunityStructureCanvasState extends State<CommunityStructureCanvas>
final position = _positions[space.uuid]; final position = _positions[space.uuid];
if (position == null) return; if (position == null) return;
final cardWidth = _cardWidths[space.uuid] ?? _minCardWidth;
const scale = 1; const scale = 1;
final viewSize = context.size; final viewSize = context.size;
if (viewSize == null) return; if (viewSize == null) return;
final x = -position.dx * scale + (viewSize.width / 2) - (_cardWidth * scale / 2); final x = -position.dx * scale + (viewSize.width / 2) - (cardWidth * scale / 2);
final y = final y =
-position.dy * scale + (viewSize.height / 2) - (_cardHeight * scale / 2); -position.dy * scale + (viewSize.height / 2) - (_cardHeight * scale / 2);
@@ -132,6 +166,16 @@ class _CommunityStructureCanvasState extends State<CommunityStructureCanvas>
context.read<CommunitiesBloc>().add( context.read<CommunitiesBloc>().add(
CommunitiesUpdateCommunity(newCommunity), CommunitiesUpdateCommunity(newCommunity),
); );
context.read<ReorderSpacesBloc>().add(
ReorderSpacesEvent(
ReorderSpacesParam(
communityUuid: widget.community.uuid,
parentSpaceUuid: data.parent?.uuid ?? '',
spaces: children,
),
),
);
} }
void _onSpaceTapped(SpaceModel? space) { void _onSpaceTapped(SpaceModel? space) {
@@ -155,13 +199,16 @@ class _CommunityStructureCanvasState extends State<CommunityStructureCanvas>
Map<int, double> levelXOffset, Map<int, double> levelXOffset,
) { ) {
for (final space in spaces) { for (final space in spaces) {
final cardWidth = _cardWidths[space.uuid] ?? _minCardWidth;
double childSubtreeWidth = 0; double childSubtreeWidth = 0;
if (space.children.isNotEmpty) { if (space.children.isNotEmpty) {
_calculateLayout(space.children, depth + 1, levelXOffset); _calculateLayout(space.children, depth + 1, levelXOffset);
final firstChildPos = _positions[space.children.first.uuid]; final firstChildPos = _positions[space.children.first.uuid];
final lastChildPos = _positions[space.children.last.uuid]; final lastChildPos = _positions[space.children.last.uuid];
if (firstChildPos != null && lastChildPos != null) { if (firstChildPos != null && lastChildPos != null) {
childSubtreeWidth = (lastChildPos.dx + _cardWidth) - firstChildPos.dx; final lastChildWidth =
_cardWidths[space.children.last.uuid] ?? _minCardWidth;
childSubtreeWidth = (lastChildPos.dx + lastChildWidth) - firstChildPos.dx;
} }
} }
@@ -170,7 +217,7 @@ class _CommunityStructureCanvasState extends State<CommunityStructureCanvas>
if (space.children.isNotEmpty) { if (space.children.isNotEmpty) {
final firstChildPos = _positions[space.children.first.uuid]!; final firstChildPos = _positions[space.children.first.uuid]!;
x = firstChildPos.dx + (childSubtreeWidth - _cardWidth) / 2; x = firstChildPos.dx + (childSubtreeWidth - cardWidth) / 2;
} else { } else {
x = currentX; x = currentX;
} }
@@ -187,7 +234,7 @@ class _CommunityStructureCanvasState extends State<CommunityStructureCanvas>
final y = depth * (_verticalSpacing + _cardHeight); final y = depth * (_verticalSpacing + _cardHeight);
_positions[space.uuid] = Offset(x, y); _positions[space.uuid] = Offset(x, y);
levelXOffset[depth] = x + _cardWidth + _horizontalSpacing; levelXOffset[depth] = x + cardWidth + _horizontalSpacing;
} }
} }
@@ -202,11 +249,21 @@ class _CommunityStructureCanvasState extends State<CommunityStructureCanvas>
List<Widget> _buildTreeWidgets() { List<Widget> _buildTreeWidgets() {
_positions.clear(); _positions.clear();
_cardWidths.clear();
final community = widget.community; final community = widget.community;
_calculateAllCardWidths(community.spaces);
final levelXOffset = <int, double>{}; final levelXOffset = <int, double>{};
_calculateLayout(community.spaces, 0, levelXOffset); _calculateLayout(community.spaces, 0, levelXOffset);
const horizontalCanvasPadding = 100.0;
final originalPositions = Map.of(_positions);
_positions.clear();
for (final entry in originalPositions.entries) {
_positions[entry.key] = entry.value.translate(horizontalCanvasPadding, 0);
}
final selectedSpace = widget.selectedSpace; final selectedSpace = widget.selectedSpace;
final highlightedUuids = <String>{}; final highlightedUuids = <String>{};
if (selectedSpace != null) { if (selectedSpace != null) {
@@ -224,14 +281,14 @@ class _CommunityStructureCanvasState extends State<CommunityStructureCanvas>
community: widget.community, community: widget.community,
); );
final createButtonX = levelXOffset[0] ?? 0.0; final createButtonX = (levelXOffset[0] ?? 0.0) + horizontalCanvasPadding;
const createButtonY = 0.0; const createButtonY = 0.0;
widgets.add( widgets.add(
Positioned( Positioned(
left: createButtonX, left: createButtonX,
top: createButtonY, top: createButtonY,
child: CreateSpaceButton(communityUuid: widget.community.uuid), child: CreateSpaceButton(community: widget.community),
), ),
); );
@@ -240,6 +297,7 @@ class _CommunityStructureCanvasState extends State<CommunityStructureCanvas>
painter: SpacesConnectionsArrowPainter( painter: SpacesConnectionsArrowPainter(
connections: connections, connections: connections,
positions: _positions, positions: _positions,
cardWidths: _cardWidths,
highlightedUuids: highlightedUuids, highlightedUuids: highlightedUuids,
), ),
child: Stack(alignment: AlignmentDirectional.center, children: widgets), child: Stack(alignment: AlignmentDirectional.center, children: widgets),
@@ -255,10 +313,12 @@ class _CommunityStructureCanvasState extends State<CommunityStructureCanvas>
CommunityModel? community, CommunityModel? community,
SpaceModel? parent, SpaceModel? parent,
}) { }) {
const targetWidth = 40.0;
final padding = (_horizontalSpacing - targetWidth) / 2;
if (spaces.isNotEmpty) { if (spaces.isNotEmpty) {
final firstChildPos = _positions[spaces.first.uuid]!; final firstChildPos = _positions[spaces.first.uuid]!;
final targetPos = Offset( final targetPos = Offset(
firstChildPos.dx - (_horizontalSpacing / 4), firstChildPos.dx - padding - targetWidth,
firstChildPos.dy, firstChildPos.dy,
); );
widgets.add(_buildDropTarget(parent, community, 0, targetPos)); widgets.add(_buildDropTarget(parent, community, 0, targetPos));
@@ -271,6 +331,7 @@ class _CommunityStructureCanvasState extends State<CommunityStructureCanvas>
continue; continue;
} }
final cardWidth = _cardWidths[space.uuid] ?? _minCardWidth;
final isHighlighted = highlightedUuids.contains(space.uuid); final isHighlighted = highlightedUuids.contains(space.uuid);
final hasNoSelectedSpace = widget.selectedSpace == null; final hasNoSelectedSpace = widget.selectedSpace == null;
@@ -278,20 +339,29 @@ class _CommunityStructureCanvasState extends State<CommunityStructureCanvas>
buildSpaceContainer: () { buildSpaceContainer: () {
return Opacity( return Opacity(
opacity: hasNoSelectedSpace || isHighlighted ? 1.0 : 0.5, opacity: hasNoSelectedSpace || isHighlighted ? 1.0 : 0.5,
child: Tooltip(
message: space.spaceName,
preferBelow: false,
child: SpaceCell( child: SpaceCell(
onTap: () => _onSpaceTapped(space), onTap: () => _onSpaceTapped(space),
icon: space.icon, icon: space.icon,
name: space.spaceName, name: space.spaceName,
), ),
),
); );
}, },
onTap: () => SpaceDetailsDialogHelper.showCreate( onTap: () => SpaceDetailsDialogHelper.showCreate(
context, context,
communityUuid: widget.community.uuid, communityUuid: widget.community.uuid,
parentUuid: space.uuid,
onSuccess: (updatedSpaceModel) {
final updatedSpaces = SpacesRecursiveHelper.recursivelyInsert(
spaces: widget.community.spaces,
parentUuid: space.uuid,
newSpace: updatedSpaceModel,
);
context.read<CommunitiesBloc>().add(
CommunitiesUpdateCommunity(
widget.community.copyWith(spaces: updatedSpaces),
),
);
},
), ),
); );
@@ -305,7 +375,7 @@ class _CommunityStructureCanvasState extends State<CommunityStructureCanvas>
Positioned( Positioned(
left: position.dx, left: position.dx,
top: position.dy, top: position.dy,
width: _cardWidth, width: cardWidth,
height: _cardHeight, height: _cardHeight,
child: Draggable<SpaceReorderDataModel>( child: Draggable<SpaceReorderDataModel>(
data: reorderData, data: reorderData,
@@ -314,7 +384,7 @@ class _CommunityStructureCanvasState extends State<CommunityStructureCanvas>
child: Opacity( child: Opacity(
opacity: 0.2, opacity: 0.2,
child: SizedBox( child: SizedBox(
width: _cardWidth, width: cardWidth,
height: _cardHeight, height: _cardHeight,
child: spaceCard, child: spaceCard,
), ),
@@ -330,7 +400,7 @@ class _CommunityStructureCanvasState extends State<CommunityStructureCanvas>
); );
final targetPos = Offset( final targetPos = Offset(
position.dx + _cardWidth + (_horizontalSpacing / 4) - 20, position.dx + cardWidth + padding,
position.dy, position.dy,
); );
widgets.add(_buildDropTarget(parent, community, i + 1, targetPos)); widgets.add(_buildDropTarget(parent, community, i + 1, targetPos));
@@ -365,24 +435,33 @@ class _CommunityStructureCanvasState extends State<CommunityStructureCanvas>
child: DragTarget<SpaceReorderDataModel>( child: DragTarget<SpaceReorderDataModel>(
builder: (context, candidateData, rejectedData) { builder: (context, candidateData, rejectedData) {
if (_draggedData == null) { if (_draggedData == null) {
return const SizedBox(); return const SizedBox.shrink();
} }
final isTargetForDragged = (_draggedData?.parent?.uuid == parent?.uuid && final children = parent?.children ?? community?.spaces ?? [];
_draggedData?.community == null) || final isSameParent = (_draggedData!.parent?.uuid == parent?.uuid &&
(_draggedData?.community?.uuid == community?.uuid && _draggedData!.community == null) ||
_draggedData?.parent == null); (_draggedData!.community?.uuid == community?.uuid &&
_draggedData!.parent == null);
if (!isTargetForDragged) { if (!isSameParent) {
return const SizedBox(); return const SizedBox.shrink();
} }
return Container( final oldIndex =
children.indexWhere((s) => s.uuid == _draggedData!.space.uuid);
if (oldIndex != -1 && (oldIndex == index || oldIndex == index - 1)) {
return const SizedBox.shrink();
}
return AnimatedContainer(
duration: const Duration(milliseconds: 150),
width: 40, width: 40,
alignment: Alignment.center,
height: _cardHeight, height: _cardHeight,
decoration: BoxDecoration( decoration: BoxDecoration(
color: context.theme.colorScheme.primary.withValues( color: context.theme.colorScheme.primary.withValues(
alpha: candidateData.isNotEmpty ? 0.7 : 0.3, alpha: candidateData.isNotEmpty ? 0.9 : 0.3,
), ),
borderRadius: BorderRadius.circular(8), borderRadius: BorderRadius.circular(8),
), ),
@@ -405,6 +484,9 @@ class _CommunityStructureCanvasState extends State<CommunityStructureCanvas>
final oldIndex = final oldIndex =
children.indexWhere((s) => s.uuid == data.data.space.uuid); children.indexWhere((s) => s.uuid == data.data.space.uuid);
if (oldIndex == -1) {
return true;
}
if (oldIndex == index || oldIndex == index - 1) { if (oldIndex == index || oldIndex == index - 1) {
return false; return false;
} }
@@ -418,7 +500,9 @@ class _CommunityStructureCanvasState extends State<CommunityStructureCanvas>
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final treeWidgets = _buildTreeWidgets(); final treeWidgets = _buildTreeWidgets();
return InteractiveViewer( return GestureDetector(
onTap: _resetSelectionAndZoom,
child: InteractiveViewer(
transformationController: _transformationController, transformationController: _transformationController,
boundaryMargin: EdgeInsets.symmetric( boundaryMargin: EdgeInsets.symmetric(
horizontal: context.screenWidth * 0.3, horizontal: context.screenWidth * 0.3,
@@ -427,12 +511,10 @@ class _CommunityStructureCanvasState extends State<CommunityStructureCanvas>
minScale: 0.5, minScale: 0.5,
maxScale: 3.0, maxScale: 3.0,
constrained: false, constrained: false,
child: GestureDetector(
onTap: _resetSelectionAndZoom,
child: SizedBox( child: SizedBox(
width: context.screenWidth * 5, width: context.screenWidth * 5,
height: context.screenHeight * 5, height: context.screenHeight * 5,
child: Stack(children: treeWidgets), child: Stack(clipBehavior: Clip.none, children: treeWidgets),
), ),
), ),
); );
@@ -2,41 +2,17 @@ import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_svg/svg.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/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/main_module/widgets/community_structure_header_action_buttons_composer.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/communities/presentation/communities_tree_selection_bloc/communities_tree_selection_bloc.dart';
import 'package:syncrow_web/pages/space_management_v2/modules/space_details/domain/models/space_details_model.dart';
import 'package:syncrow_web/pages/space_management_v2/modules/space_details/presentation/helpers/space_details_dialog_helper.dart';
import 'package:syncrow_web/utils/color_manager.dart'; import 'package:syncrow_web/utils/color_manager.dart';
import 'package:syncrow_web/utils/constants/assets.dart'; import 'package:syncrow_web/utils/constants/assets.dart';
import 'package:syncrow_web/utils/extension/build_context_x.dart';
class CommunityStructureHeader extends StatelessWidget { class CommunityStructureHeader extends StatelessWidget {
const CommunityStructureHeader({super.key}); const CommunityStructureHeader({super.key});
List<SpaceModel> _updateRecursive(
List<SpaceModel> spaces,
SpaceDetailsModel updatedSpace,
) {
return spaces.map((space) {
if (space.uuid == updatedSpace.uuid) {
return space.copyWith(
spaceName: updatedSpace.spaceName,
icon: updatedSpace.icon,
);
}
if (space.children.isNotEmpty) {
return space.copyWith(
children: _updateRecursive(space.children, updatedSpace),
);
}
return space;
}).toList();
}
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final theme = Theme.of(context);
final screenWidth = MediaQuery.of(context).size.width; final screenWidth = MediaQuery.of(context).size.width;
return Container( return Container(
padding: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 16.0), padding: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 16.0),
@@ -44,9 +20,9 @@ class CommunityStructureHeader extends StatelessWidget {
color: ColorsManager.whiteColors, color: ColorsManager.whiteColors,
boxShadow: [ boxShadow: [
BoxShadow( BoxShadow(
color: ColorsManager.shadowBlackColor, color: ColorsManager.shadowBlackColor.withValues(alpha: 0.1),
blurRadius: 8, blurRadius: 20,
offset: const Offset(0, 4), offset: const Offset(0, 1),
), ),
], ],
), ),
@@ -57,7 +33,7 @@ class CommunityStructureHeader extends StatelessWidget {
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
Expanded( Expanded(
child: _buildCommunityInfo(context, theme, screenWidth), child: _buildCommunityInfo(context, screenWidth),
), ),
const SizedBox(width: 16), const SizedBox(width: 16),
], ],
@@ -67,8 +43,7 @@ class CommunityStructureHeader extends StatelessWidget {
); );
} }
Widget _buildCommunityInfo( Widget _buildCommunityInfo(BuildContext context, double screenWidth) {
BuildContext context, ThemeData theme, double screenWidth) {
final selectedCommunity = final selectedCommunity =
context.watch<CommunitiesTreeSelectionBloc>().state.selectedCommunity; context.watch<CommunitiesTreeSelectionBloc>().state.selectedCommunity;
final selectedSpace = final selectedSpace =
@@ -78,7 +53,7 @@ class CommunityStructureHeader extends StatelessWidget {
children: [ children: [
Text( Text(
'Community Structure', 'Community Structure',
style: theme.textTheme.headlineLarge?.copyWith( style: context.textTheme.headlineLarge?.copyWith(
color: ColorsManager.blackColor, color: ColorsManager.blackColor,
), ),
), ),
@@ -91,7 +66,7 @@ class CommunityStructureHeader extends StatelessWidget {
Flexible( Flexible(
child: SelectableText( child: SelectableText(
selectedCommunity.name, selectedCommunity.name,
style: theme.textTheme.bodyLarge?.copyWith( style: context.textTheme.bodyLarge?.copyWith(
color: ColorsManager.blackColor, color: ColorsManager.blackColor,
), ),
maxLines: 1, maxLines: 1,
@@ -115,27 +90,8 @@ class CommunityStructureHeader extends StatelessWidget {
), ),
), ),
const SizedBox(width: 8), const SizedBox(width: 8),
CommunityStructureHeaderActionButtons( CommunityStructureHeaderActionButtonsComposer(
onDelete: (space) {}, selectedCommunity: selectedCommunity,
onDuplicate: (space) {},
onEdit: (space) => SpaceDetailsDialogHelper.showEdit(
context,
spaceModel: selectedSpace!,
communityUuid: selectedCommunity.uuid,
onSuccess: (updatedSpaceDetails) {
final communitiesBloc = context.read<CommunitiesBloc>();
final updatedSpaces = _updateRecursive(
selectedCommunity.spaces,
updatedSpaceDetails,
);
final community = selectedCommunity.copyWith(
spaces: updatedSpaces,
);
communitiesBloc.add(CommunitiesUpdateCommunity(community));
},
),
selectedSpace: selectedSpace, selectedSpace: selectedSpace,
), ),
], ],
@@ -19,11 +19,12 @@ class CommunityStructureHeaderActionButtons extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
if (selectedSpace == null) return const SizedBox.shrink();
return Wrap( return Wrap(
alignment: WrapAlignment.end, alignment: WrapAlignment.end,
spacing: 10, spacing: 10,
children: [ children: [
if (selectedSpace != null) ...[
CommunityStructureHeaderButton( CommunityStructureHeaderButton(
label: 'Edit', label: 'Edit',
svgAsset: Assets.editSpace, svgAsset: Assets.editSpace,
@@ -40,7 +41,6 @@ class CommunityStructureHeaderActionButtons extends StatelessWidget {
onPressed: () => onDelete(selectedSpace!), onPressed: () => onDelete(selectedSpace!),
), ),
], ],
],
); );
} }
} }
@@ -0,0 +1,69 @@
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:syncrow_web/pages/space_management_v2/main_module/helpers/spaces_recursive_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/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/delete_space/presentation/widgets/delete_space_dialog.dart';
import 'package:syncrow_web/pages/space_management_v2/modules/space_details/presentation/helpers/space_details_dialog_helper.dart';
class CommunityStructureHeaderActionButtonsComposer extends StatelessWidget {
const CommunityStructureHeaderActionButtonsComposer({
required this.selectedCommunity,
required this.selectedSpace,
super.key,
});
final CommunityModel selectedCommunity;
final SpaceModel? selectedSpace;
@override
Widget build(BuildContext context) {
return CommunityStructureHeaderActionButtons(
onDelete: (space) => showDialog<void>(
context: context,
barrierDismissible: false,
builder: (_) => DeleteSpaceDialog(
space: space,
community: selectedCommunity,
onSuccess: () {
final updatedSpaces = SpacesRecursiveHelper.recusrivelyDelete(
selectedCommunity.spaces,
space.uuid,
);
final community = selectedCommunity.copyWith(
spaces: updatedSpaces,
);
context.read<CommunitiesBloc>().add(
CommunitiesUpdateCommunity(community),
);
context.read<CommunitiesTreeSelectionBloc>().add(
SelectCommunityEvent(community: selectedCommunity),
);
},
),
),
onDuplicate: (space) {},
onEdit: (space) => SpaceDetailsDialogHelper.showEdit(
context,
spaceModel: selectedSpace!,
communityUuid: selectedCommunity.uuid,
onSuccess: (updatedSpaceDetails) {
final communitiesBloc = context.read<CommunitiesBloc>();
final updatedSpaces = SpacesRecursiveHelper.recusrivelyUpdate(
selectedCommunity.spaces,
updatedSpaceDetails,
);
final community = selectedCommunity.copyWith(
spaces: updatedSpaces,
);
communitiesBloc.add(CommunitiesUpdateCommunity(community));
},
),
selectedSpace: selectedSpace,
);
}
}
@@ -1,14 +1,18 @@
import 'package:flutter/material.dart'; 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/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/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/color_manager.dart';
class CreateSpaceButton extends StatefulWidget { class CreateSpaceButton extends StatefulWidget {
const CreateSpaceButton({ const CreateSpaceButton({
required this.communityUuid, required this.community,
super.key, super.key,
}); });
final String communityUuid; final CommunityModel community;
@override @override
State<CreateSpaceButton> createState() => _CreateSpaceButtonState(); State<CreateSpaceButton> createState() => _CreateSpaceButtonState();
@@ -25,7 +29,21 @@ class _CreateSpaceButtonState extends State<CreateSpaceButton> {
child: InkWell( child: InkWell(
onTap: () => SpaceDetailsDialogHelper.showCreate( onTap: () => SpaceDetailsDialogHelper.showCreate(
context, context,
communityUuid: widget.communityUuid, communityUuid: widget.community.uuid,
onSuccess: (updatedSpaceModel) {
final newCommunity = widget.community.copyWith(
spaces: [...widget.community.spaces, updatedSpaceModel],
);
context.read<CommunitiesBloc>().add(
CommunitiesUpdateCommunity(newCommunity),
);
context.read<CommunitiesTreeSelectionBloc>().add(
SelectSpaceEvent(
space: updatedSpaceModel,
community: newCommunity,
),
);
},
), ),
child: MouseRegion( child: MouseRegion(
onEnter: (_) => setState(() => _isHovered = true), onEnter: (_) => setState(() => _isHovered = true),
@@ -2,32 +2,23 @@ import 'package:flutter/material.dart';
import 'package:syncrow_web/utils/color_manager.dart'; import 'package:syncrow_web/utils/color_manager.dart';
class PlusButtonWidget extends StatelessWidget { class PlusButtonWidget extends StatelessWidget {
final Offset offset; final void Function() onTap;
final void Function() onButtonTap;
const PlusButtonWidget({ const PlusButtonWidget({
required this.onTap,
super.key, super.key,
required this.offset,
required this.onButtonTap,
}); });
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return GestureDetector( return IconButton.filled(
onTap: onButtonTap, onPressed: onTap,
child: Container( style: IconButton.styleFrom(backgroundColor: ColorsManager.spaceColor),
width: 30, icon: const Icon(
height: 30,
decoration: const BoxDecoration(
color: ColorsManager.spaceColor,
shape: BoxShape.circle,
),
child: const Icon(
Icons.add, Icons.add,
color: ColorsManager.whiteColors, color: ColorsManager.whiteColors,
size: 20, size: 20,
), ),
),
); );
} }
} }
@@ -29,10 +29,9 @@ class _SpaceCardWidgetState extends State<SpaceCardWidget> {
widget.buildSpaceContainer(), widget.buildSpaceContainer(),
if (isHovered) if (isHovered)
Positioned( Positioned(
bottom: 0, bottom: -5,
child: PlusButtonWidget( child: PlusButtonWidget(
offset: Offset.zero, onTap: widget.onTap,
onButtonTap: widget.onTap,
), ),
), ),
], ],
@@ -20,22 +20,20 @@ class SpaceCell extends StatelessWidget {
return InkWell( return InkWell(
onTap: onTap, onTap: onTap,
child: Container( child: Container(
width: 150, padding: const EdgeInsetsDirectional.only(end: 10),
height: 70, height: 70,
decoration: _containerDecoration(), decoration: _containerDecoration(),
child: Row( child: Row(
spacing: 10,
mainAxisSize: MainAxisSize.min,
children: [ children: [
_buildIconContainer(), _buildIconContainer(),
const SizedBox(width: 10), Text(
Expanded(
child: Text(
name, name,
style: context.textTheme.bodyLarge?.copyWith( style: context.textTheme.bodyLarge?.copyWith(
fontWeight: FontWeight.bold, fontWeight: FontWeight.bold,
color: ColorsManager.blackColor, color: ColorsManager.blackColor,
), ),
overflow: TextOverflow.ellipsis,
),
), ),
], ],
), ),
@@ -3,6 +3,9 @@ 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_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/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/main_module/widgets/create_space_button.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/communities/presentation/communities_tree_selection_bloc/communities_tree_selection_bloc.dart';
class SpaceManagementCommunityStructure extends StatelessWidget { class SpaceManagementCommunityStructure extends StatelessWidget {
@@ -10,31 +13,59 @@ class SpaceManagementCommunityStructure extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final selectionBloc = context.watch<CommunitiesTreeSelectionBloc>().state; return BlocBuilder<CommunitiesTreeSelectionBloc, CommunitiesTreeSelectionState>(
final selectedCommunity = selectionBloc.selectedCommunity; builder: (context, state) {
final selectedSpace = selectionBloc.selectedSpace; final selectedCommunity = state.selectedCommunity;
const spacer = Spacer(flex: 6); final selectedSpace = state.selectedSpace;
return Visibility(
visible: selectedCommunity!.spaces.isNotEmpty, if (selectedCommunity == null) {
replacement: Row( return const SizedBox.shrink();
children: [ }
spacer,
Expanded( return Column(
child: CreateSpaceButton(communityUuid: selectedCommunity.uuid),
),
spacer
],
),
child: Column(
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
children: [ children: [
const CommunityStructureHeader(), const CommunityStructureHeader(),
Expanded( BlocBuilder<CommunitiesBloc, CommunitiesState>(
builder: (context, state) {
final community = state.communities.firstWhere(
(element) => element.uuid == selectedCommunity.uuid,
orElse: () => selectedCommunity,
);
return Visibility(
visible: community.spaces.isNotEmpty,
replacement: _buildEmptyWidget(community),
child: _buildCanvas(community, selectedSpace),
);
},
),
],
);
},
);
}
Widget _buildCanvas(
CommunityModel selectedCommunity,
SpaceModel? selectedSpace,
) {
return Expanded(
child: CommunityStructureCanvas( child: CommunityStructureCanvas(
community: selectedCommunity, community: selectedCommunity,
selectedSpace: selectedSpace, selectedSpace: selectedSpace,
), ),
), );
}
Widget _buildEmptyWidget(CommunityModel selectedCommunity) {
const spacer = Spacer(flex: 6);
return Expanded(
child: Row(
children: [
spacer,
Expanded(child: CreateSpaceButton(community: selectedCommunity)),
spacer,
], ],
), ),
); );
@@ -0,0 +1,63 @@
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/space_model.dart';
import 'package:syncrow_web/pages/space_management_v2/modules/create_space/domain/params/create_space_param.dart';
import 'package:syncrow_web/pages/space_management_v2/modules/create_space/domain/services/create_space_service.dart';
import 'package:syncrow_web/services/api/api_exception.dart';
import 'package:syncrow_web/services/api/http_service.dart';
final class RemoteCreateSpaceService implements CreateSpaceService {
const RemoteCreateSpaceService(this._httpService);
final HTTPService _httpService;
static const _defaultErrorMessage = 'Failed to create space';
@override
Future<SpaceModel> createSpace(CreateSpaceParam param) async {
try {
final path = await _makeUrl(param);
final response = await _httpService.post(
path: path,
body: param.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 SpaceModel.fromJson(response['data'] as Map<String, dynamic>);
},
);
return response;
} on DioException catch (e) {
final message = e.response?.data as Map<String, dynamic>?;
final error = message?['error'] as Map<String, dynamic>?;
final errorMessage = error?['error'] as String? ?? '';
final formattedErrorMessage = [
_defaultErrorMessage,
errorMessage,
].join(': ');
throw APIException(formattedErrorMessage);
} catch (e) {
final formattedErrorMessage = [_defaultErrorMessage, '$e'].join(': ');
throw APIException(formattedErrorMessage);
}
}
Future<String> _makeUrl(CreateSpaceParam param) async {
final projectUuid = await ProjectManager.getProjectUUID();
if (projectUuid == null || projectUuid.isEmpty) {
throw APIException('Project UUID is not set');
}
final communityUuid = param.communityUuid;
if (communityUuid.isEmpty) {
throw APIException('Community UUID is not set');
}
return '/projects/$projectUuid/communities/$communityUuid/spaces';
}
}
@@ -0,0 +1,22 @@
import 'package:syncrow_web/pages/space_management_v2/modules/space_details/domain/models/space_details_model.dart';
class CreateSpaceParam {
final String communityUuid;
final SpaceDetailsModel space;
final String? parentUuid;
const CreateSpaceParam({
required this.communityUuid,
required this.space,
required this.parentUuid,
});
Map<String, dynamic> toJson() {
return {
'parentUuid': parentUuid,
...space.toJson(),
'x': 0,
'y': 0,
};
}
}
@@ -0,0 +1,6 @@
import 'package:syncrow_web/pages/space_management_v2/modules/communities/domain/models/space_model.dart';
import 'package:syncrow_web/pages/space_management_v2/modules/create_space/domain/params/create_space_param.dart';
abstract interface class CreateSpaceService {
Future<SpaceModel> createSpace(CreateSpaceParam param);
}
@@ -0,0 +1,34 @@
import 'package:bloc/bloc.dart';
import 'package:equatable/equatable.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/create_space/domain/params/create_space_param.dart';
import 'package:syncrow_web/pages/space_management_v2/modules/create_space/domain/services/create_space_service.dart';
import 'package:syncrow_web/services/api/api_exception.dart';
part 'create_space_event.dart';
part 'create_space_state.dart';
class CreateSpaceBloc extends Bloc<CreateSpaceEvent, CreateSpaceState> {
CreateSpaceBloc(
this._createSpaceService,
) : super(const CreateSpaceInitial()) {
on<CreateSpace>(_onCreateSpace);
}
final CreateSpaceService _createSpaceService;
Future<void> _onCreateSpace(
CreateSpace event,
Emitter<CreateSpaceState> emit,
) async {
emit(const CreateSpaceLoading());
try {
final result = await _createSpaceService.createSpace(event.param);
emit(CreateSpaceSuccess(result));
} on APIException catch (e) {
emit(CreateSpaceFailure(e.message));
} catch (e) {
emit(CreateSpaceFailure(e.toString()));
}
}
}
@@ -0,0 +1,17 @@
part of 'create_space_bloc.dart';
sealed class CreateSpaceEvent extends Equatable {
const CreateSpaceEvent();
@override
List<Object> get props => [];
}
final class CreateSpace extends CreateSpaceEvent {
const CreateSpace(this.param);
final CreateSpaceParam param;
@override
List<Object> get props => [param];
}
@@ -0,0 +1,31 @@
part of 'create_space_bloc.dart';
sealed class CreateSpaceState extends Equatable {
const CreateSpaceState();
@override
List<Object> get props => [];
}
final class CreateSpaceInitial extends CreateSpaceState {
const CreateSpaceInitial();
}
final class CreateSpaceLoading extends CreateSpaceState {
const CreateSpaceLoading();
}
final class CreateSpaceSuccess extends CreateSpaceState {
const CreateSpaceSuccess(this.space);
final SpaceModel space;
@override
List<Object> get props => [space];
}
final class CreateSpaceFailure extends CreateSpaceState {
const CreateSpaceFailure(this.errorMessage);
final String errorMessage;
}
@@ -0,0 +1,64 @@
import 'package:dio/dio.dart';
import 'package:syncrow_web/pages/common/bloc/project_manager.dart';
import 'package:syncrow_web/pages/space_management_v2/modules/delete_space/domain/params/delete_space_param.dart';
import 'package:syncrow_web/pages/space_management_v2/modules/delete_space/domain/services/delete_space_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 RemoteDeleteSpaceService implements DeleteSpaceService {
const RemoteDeleteSpaceService(this._httpService);
final HTTPService _httpService;
@override
Future<void> delete(DeleteSpaceParam param) async {
try {
await _httpService.delete(
path: await _makeUrl(param),
expectedResponseModel: (json) {
final response = json as Map<String, dynamic>;
final hasSuccessfullyDeletedSpace = response['success'] as bool? ?? false;
if (!hasSuccessfullyDeletedSpace) {
throw APIException('Failed to delete space');
}
return hasSuccessfullyDeletedSpace;
},
);
} on DioException catch (e) {
final message = e.response?.data as Map<String, dynamic>?;
throw APIException(_getErrorMessageFromBody(message));
} catch (e) {
throw APIException(e.toString());
}
}
String _getErrorMessageFromBody(Map<String, dynamic>? body) {
if (body == null) return 'Failed to delete space';
final error = body['error'] as Map<String, dynamic>?;
final errorMessage = error?['message'] as String? ?? '';
return errorMessage;
}
Future<String> _makeUrl(DeleteSpaceParam param) async {
final projectUuid = await ProjectManager.getProjectUUID();
if (projectUuid == null) {
throw APIException('Project UUID is not set');
}
if (param.communityUuid.isEmpty) {
throw APIException('Community UUID is not set');
}
if (param.spaceUuid.isEmpty) {
throw APIException('Space UUID is not set');
}
return ApiEndpoints.deleteSpace
.replaceAll('{projectId}', projectUuid)
.replaceAll('{communityId}', param.communityUuid)
.replaceAll('{spaceId}', param.spaceUuid);
}
}
@@ -0,0 +1,9 @@
class DeleteSpaceParam {
const DeleteSpaceParam({
required this.spaceUuid,
required this.communityUuid,
});
final String spaceUuid;
final String communityUuid;
}
@@ -0,0 +1,5 @@
import 'package:syncrow_web/pages/space_management_v2/modules/delete_space/domain/params/delete_space_param.dart';
abstract interface class DeleteSpaceService {
Future<void> delete(DeleteSpaceParam param);
}
@@ -0,0 +1,31 @@
import 'package:bloc/bloc.dart';
import 'package:equatable/equatable.dart';
import 'package:syncrow_web/pages/space_management_v2/modules/delete_space/domain/params/delete_space_param.dart';
import 'package:syncrow_web/pages/space_management_v2/modules/delete_space/domain/services/delete_space_service.dart';
import 'package:syncrow_web/services/api/api_exception.dart';
part 'delete_space_event.dart';
part 'delete_space_state.dart';
class DeleteSpaceBloc extends Bloc<DeleteSpaceEvent, DeleteSpaceState> {
DeleteSpaceBloc(this._deleteSpaceService) : super(DeleteSpaceInitial()) {
on<DeleteSpace>(_onDeleteSpace);
}
final DeleteSpaceService _deleteSpaceService;
Future<void> _onDeleteSpace(
DeleteSpace event,
Emitter<DeleteSpaceState> emit,
) async {
emit(DeleteSpaceLoading());
try {
await _deleteSpaceService.delete(event.param);
emit(const DeleteSpaceSuccess('Space deleted successfully'));
} on APIException catch (e) {
emit(DeleteSpaceFailure(e.message));
} catch (e) {
emit(DeleteSpaceFailure(e.toString()));
}
}
}
@@ -0,0 +1,17 @@
part of 'delete_space_bloc.dart';
sealed class DeleteSpaceEvent extends Equatable {
const DeleteSpaceEvent();
@override
List<Object> get props => [];
}
final class DeleteSpace extends DeleteSpaceEvent {
const DeleteSpace(this.param);
final DeleteSpaceParam param;
@override
List<Object> get props => [param];
}
@@ -0,0 +1,30 @@
part of 'delete_space_bloc.dart';
sealed class DeleteSpaceState extends Equatable {
const DeleteSpaceState();
@override
List<Object> get props => [];
}
final class DeleteSpaceInitial extends DeleteSpaceState {}
final class DeleteSpaceLoading extends DeleteSpaceState {}
final class DeleteSpaceSuccess extends DeleteSpaceState {
const DeleteSpaceSuccess(this.successMessage);
final String successMessage;
@override
List<Object> get props => [successMessage];
}
final class DeleteSpaceFailure extends DeleteSpaceState {
const DeleteSpaceFailure(this.errorMessage);
final String errorMessage;
@override
List<Object> get props => [errorMessage];
}
@@ -0,0 +1,73 @@
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/modules/communities/domain/models/space_model.dart';
import 'package:syncrow_web/pages/space_management_v2/modules/delete_space/data/remote_delete_space_service.dart';
import 'package:syncrow_web/pages/space_management_v2/modules/delete_space/presentation/bloc/delete_space_bloc.dart';
import 'package:syncrow_web/pages/space_management_v2/modules/delete_space/presentation/widgets/delete_space_dialog_form.dart';
import 'package:syncrow_web/pages/space_management_v2/modules/delete_space/presentation/widgets/delete_space_loading_widget.dart';
import 'package:syncrow_web/pages/space_management_v2/modules/delete_space/presentation/widgets/delete_space_status_widget.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 DeleteSpaceDialog extends StatelessWidget {
const DeleteSpaceDialog({
required this.space,
required this.community,
required this.onSuccess,
super.key,
});
final SpaceModel space;
final CommunityModel community;
final void Function() onSuccess;
@override
Widget build(BuildContext context) {
return BlocProvider(
create: (context) => DeleteSpaceBloc(
RemoteDeleteSpaceService(HTTPService()),
),
child: Builder(
builder: (context) => Dialog(
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
child: Container(
padding: const EdgeInsetsDirectional.all(32),
constraints: BoxConstraints(
maxWidth: context.screenWidth * 0.2,
),
child: BlocConsumer<DeleteSpaceBloc, DeleteSpaceState>(
listener: (context, state) {
if (state case DeleteSpaceSuccess()) onSuccess();
},
builder: (context, state) => switch (state) {
DeleteSpaceInitial() => DeleteSpaceDialogForm(
space: space,
communityUuid: community.uuid,
),
DeleteSpaceLoading() => const DeleteSpaceLoadingWidget(),
DeleteSpaceSuccess() => DeleteSpaceStatusWidget(
message: state.successMessage,
icon: const Icon(
Icons.check_circle,
size: 92,
color: ColorsManager.goodGreen,
),
),
DeleteSpaceFailure() => DeleteSpaceStatusWidget(
message: state.errorMessage,
icon: const Icon(
Icons.error,
size: 92,
color: ColorsManager.red,
),
),
},
),
),
),
),
);
}
}
@@ -0,0 +1,106 @@
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/communities/domain/models/space_model.dart';
import 'package:syncrow_web/pages/space_management_v2/modules/delete_space/domain/params/delete_space_param.dart';
import 'package:syncrow_web/pages/space_management_v2/modules/delete_space/presentation/bloc/delete_space_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 DeleteSpaceDialogForm extends StatelessWidget {
const DeleteSpaceDialogForm({
required this.space,
required this.communityUuid,
super.key,
});
final SpaceModel space;
final String communityUuid;
@override
Widget build(BuildContext context) {
return Column(
mainAxisSize: MainAxisSize.min,
children: [
SvgPicture.asset(Assets.xDelete, width: 36, height: 36),
const SizedBox(height: 16),
SelectableText(
'Delete Space',
textAlign: TextAlign.center,
style: context.textTheme.titleLarge?.copyWith(
color: ColorsManager.blackColor,
fontWeight: FontWeight.w400,
fontSize: 24,
),
),
const SizedBox(height: 8),
SelectableText(
'Are you sure you want to delete this space? This action is irreversible',
textAlign: TextAlign.center,
style: context.textTheme.bodyLarge?.copyWith(
color: ColorsManager.lightGreyColor,
fontWeight: FontWeight.w400,
fontSize: 14,
),
),
const SizedBox(height: 24),
Row(
children: [
Expanded(
child: FilledButton(
style: _buildButtonStyle(
context,
color: ColorsManager.grey25,
textColor: ColorsManager.blackColor,
),
onPressed: Navigator.of(context).pop,
child: const Text('Cancel'),
),
),
const SizedBox(width: 16),
Expanded(
child: FilledButton(
style: _buildButtonStyle(
context,
color: ColorsManager.semiTransparentRed,
textColor: ColorsManager.whiteColors,
),
onPressed: () {
context.read<DeleteSpaceBloc>().add(
DeleteSpace(
DeleteSpaceParam(
spaceUuid: space.uuid,
communityUuid: communityUuid,
),
),
);
},
child: const Text('Delete'),
),
),
],
),
],
);
}
ButtonStyle _buildButtonStyle(
BuildContext context, {
required Color color,
required Color textColor,
}) {
return FilledButton.styleFrom(
padding: const EdgeInsets.symmetric(vertical: 12),
backgroundColor: color,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8),
),
foregroundColor: textColor,
textStyle: context.textTheme.bodyLarge?.copyWith(
fontWeight: FontWeight.w400,
fontSize: 14,
),
);
}
}
@@ -0,0 +1,13 @@
import 'package:flutter/material.dart';
class DeleteSpaceLoadingWidget extends StatelessWidget {
const DeleteSpaceLoadingWidget({super.key});
@override
Widget build(BuildContext context) {
return const SizedBox.square(
dimension: 32,
child: Center(child: CircularProgressIndicator()),
);
}
}
@@ -0,0 +1,38 @@
import 'package:flutter/material.dart';
import 'package:syncrow_web/utils/color_manager.dart';
import 'package:syncrow_web/utils/extension/build_context_x.dart';
class DeleteSpaceStatusWidget extends StatelessWidget {
const DeleteSpaceStatusWidget({
required this.message,
required this.icon,
super.key,
});
final String message;
final Widget icon;
@override
Widget build(BuildContext context) {
return Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.center,
spacing: 16,
children: [
icon,
SelectableText(
message,
style: context.textTheme.bodyMedium?.copyWith(
color: ColorsManager.blackColor,
fontSize: 22,
),
textAlign: TextAlign.center,
),
FilledButton(
onPressed: Navigator.of(context).pop,
child: const Text('Close'),
),
],
);
}
}
@@ -0,0 +1,58 @@
import 'package:dio/dio.dart';
import 'package:syncrow_web/pages/common/bloc/project_manager.dart';
import 'package:syncrow_web/pages/space_management_v2/modules/reorder_spaces/domain/params/reorder_spaces_param.dart';
import 'package:syncrow_web/pages/space_management_v2/modules/reorder_spaces/domain/services/reorder_spaces_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 RemoteReorderSpacesService implements ReorderSpacesService {
RemoteReorderSpacesService(this._httpClient);
final HTTPService _httpClient;
@override
Future<void> reorderSpaces(ReorderSpacesParam param) async {
try {
await _httpClient.post(
path: await _makeUrl(param),
body: param.toJson(),
expectedResponseModel: (json) => json,
);
} on DioException catch (e) {
final message = e.response?.data as Map<String, dynamic>?;
throw APIException(_getErrorMessageFromBody(message));
} catch (e) {
throw APIException(e.toString());
}
}
String _getErrorMessageFromBody(Map<String, dynamic>? body) {
if (body == null) return 'Failed to delete space';
final error = body['error'] as Map<String, dynamic>?;
final errorMessage = error?['message'] as String? ?? '';
return errorMessage;
}
Future<String> _makeUrl(ReorderSpacesParam param) async {
final projectUuid = await ProjectManager.getProjectUUID();
final communityUuid = param.communityUuid;
if (projectUuid == null || projectUuid.isEmpty) {
throw APIException('Project UUID is not set');
}
if (communityUuid.isEmpty) {
throw APIException('Community UUID is not set');
}
if (param.parentSpaceUuid.isEmpty) {
throw APIException('Parent Space UUID is not set');
}
return ApiEndpoints.reorderSpaces
.replaceAll('{projectUuid}', projectUuid)
.replaceAll('{communityUuid}', communityUuid)
.replaceAll('{parentSpaceUuid}', param.parentSpaceUuid);
}
}
@@ -0,0 +1,21 @@
import 'package:equatable/equatable.dart';
import 'package:syncrow_web/pages/space_management_v2/modules/communities/domain/models/space_model.dart';
class ReorderSpacesParam extends Equatable {
const ReorderSpacesParam({
required this.communityUuid,
required this.parentSpaceUuid,
required this.spaces,
});
final String communityUuid;
final String parentSpaceUuid;
final List<SpaceModel> spaces;
@override
List<Object?> get props => [spaces, communityUuid, parentSpaceUuid];
Map<String, dynamic> toJson() => {
'spacesUuids': spaces.map((space) => space.uuid).toList(),
};
}
@@ -0,0 +1,5 @@
import 'package:syncrow_web/pages/space_management_v2/modules/reorder_spaces/domain/params/reorder_spaces_param.dart';
abstract interface class ReorderSpacesService {
Future<void> reorderSpaces(ReorderSpacesParam param);
}
@@ -0,0 +1,35 @@
import 'package:bloc/bloc.dart';
import 'package:equatable/equatable.dart';
import 'package:syncrow_web/pages/space_management_v2/modules/reorder_spaces/domain/params/reorder_spaces_param.dart';
import 'package:syncrow_web/pages/space_management_v2/modules/reorder_spaces/domain/services/reorder_spaces_service.dart';
import 'package:syncrow_web/services/api/api_exception.dart';
part 'reorder_spaces_event.dart';
part 'reorder_spaces_state.dart';
class ReorderSpacesBloc extends Bloc<ReorderSpacesEvent, ReorderSpacesState> {
ReorderSpacesBloc(
this._reorderSpacesService,
) : super(const ReorderSpacesInitial()) {
on<ReorderSpacesEvent>(_onReorderSpacesEvent);
}
final ReorderSpacesService _reorderSpacesService;
Future<void> _onReorderSpacesEvent(
ReorderSpacesEvent event,
Emitter<ReorderSpacesState> emit,
) async {
emit(const ReorderSpacesLoading());
try {
await _reorderSpacesService.reorderSpaces(event.param);
emit(const ReorderSpacesSuccess());
} on APIException catch (e) {
emit(ReorderSpacesFailure(e.message));
} catch (e) {
emit(ReorderSpacesFailure(e.toString()));
} finally {
emit(const ReorderSpacesInitial());
}
}
}
@@ -0,0 +1,10 @@
part of 'reorder_spaces_bloc.dart';
final class ReorderSpacesEvent extends Equatable {
const ReorderSpacesEvent(this.param);
final ReorderSpacesParam param;
@override
List<Object> get props => [param];
}
@@ -0,0 +1,29 @@
part of 'reorder_spaces_bloc.dart';
sealed class ReorderSpacesState extends Equatable {
const ReorderSpacesState();
@override
List<Object> get props => [];
}
final class ReorderSpacesInitial extends ReorderSpacesState {
const ReorderSpacesInitial();
}
final class ReorderSpacesLoading extends ReorderSpacesState {
const ReorderSpacesLoading();
}
final class ReorderSpacesSuccess extends ReorderSpacesState {
const ReorderSpacesSuccess();
}
final class ReorderSpacesFailure extends ReorderSpacesState {
const ReorderSpacesFailure(this.errorMessage);
final String errorMessage;
@override
List<Object> get props => [errorMessage];
}
@@ -1,24 +1,29 @@
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/models/space_details_model.dart';
import 'package:syncrow_web/pages/space_management_v2/modules/space_details/domain/models/subspace.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/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/pages/space_management_v2/modules/space_details/domain/services/space_details_service.dart';
class UniqueSubspacesDecorator implements SpaceDetailsService { class UniqueSpaceDetailsSpacesDecoratorService implements SpaceDetailsService {
final SpaceDetailsService _decoratee; final SpaceDetailsService _decoratee;
const UniqueSubspacesDecorator(this._decoratee); const UniqueSpaceDetailsSpacesDecoratorService(this._decoratee);
@override @override
Future<SpaceDetailsModel> getSpaceDetails(LoadSpaceDetailsParam param) async { Future<SpaceDetailsModel> getSpaceDetails(LoadSpaceDetailsParam param) async {
final response = await _decoratee.getSpaceDetails(param); final response = await _decoratee.getSpaceDetails(param);
final uniqueSubspaces = <String, Subspace>{}; final uniqueSubspaces = <String, Subspace>{};
final duplicateNames = <String>{};
for (final subspace in response.subspaces) { for (final subspace in response.subspaces) {
final normalizedName = subspace.name.trim().toLowerCase(); final normalizedName = subspace.name.trim().toLowerCase();
if (!uniqueSubspaces.containsKey(normalizedName)) { if (uniqueSubspaces.containsKey(normalizedName)) {
duplicateNames.add(normalizedName);
} else {
uniqueSubspaces[normalizedName] = subspace; uniqueSubspaces[normalizedName] = subspace;
} }
} }
duplicateNames.forEach(uniqueSubspaces.remove);
return response.copyWith( return response.copyWith(
subspaces: uniqueSubspaces.values.toList(), subspaces: uniqueSubspaces.values.toList(),
@@ -0,0 +1,47 @@
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:uuid/uuid.dart';
class ProductAllocation extends Equatable {
final String uuid;
final Product product;
final Tag tag;
const ProductAllocation({
required this.uuid,
required this.product,
required this.tag,
});
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>),
);
}
ProductAllocation copyWith({
String? uuid,
Product? product,
Tag? tag,
}) {
return ProductAllocation(
uuid: uuid ?? this.uuid,
product: product ?? this.product,
tag: tag ?? this.tag,
);
}
Map<String, dynamic> toJson() {
final isNewTag = tag.uuid.isEmpty;
return <String, dynamic>{
if (isNewTag) 'tagName': tag.name else 'tagUuid': tag.uuid,
'productUuid': product.uuid,
};
}
@override
List<Object?> get props => [uuid, product, tag];
}
@@ -1,8 +1,7 @@
import 'package:equatable/equatable.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/space_details/domain/models/product_allocation.dart';
import 'package:syncrow_web/pages/space_management_v2/modules/tags/domain/models/tag.dart'; import 'package:syncrow_web/pages/space_management_v2/modules/space_details/domain/models/subspace.dart';
import 'package:syncrow_web/utils/constants/assets.dart'; import 'package:syncrow_web/utils/constants/assets.dart';
import 'package:uuid/uuid.dart';
class SpaceDetailsModel extends Equatable { class SpaceDetailsModel extends Equatable {
final String uuid; final String uuid;
@@ -26,6 +25,7 @@ class SpaceDetailsModel extends Equatable {
productAllocations: [], productAllocations: [],
subspaces: [], subspaces: [],
); );
factory SpaceDetailsModel.fromJson(Map<String, dynamic> json) { factory SpaceDetailsModel.fromJson(Map<String, dynamic> json) {
return SpaceDetailsModel( return SpaceDetailsModel(
uuid: json['uuid'] as String, uuid: json['uuid'] as String,
@@ -56,78 +56,21 @@ class SpaceDetailsModel extends Equatable {
); );
} }
@override Map<String, dynamic> toJson() {
List<Object?> get props => [uuid, spaceName, icon, productAllocations, subspaces]; return {
} 'spaceName': spaceName,
'icon': icon,
class ProductAllocation extends Equatable { 'subspaces': subspaces.map((e) => e.toJson()).toList(),
final String uuid; 'productAllocations': productAllocations.map((e) => e.toJson()).toList(),
final Product product; };
final Tag tag;
const ProductAllocation({
required this.uuid,
required this.product,
required this.tag,
});
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>),
);
}
ProductAllocation copyWith({
String? uuid,
Product? product,
Tag? tag,
}) {
return ProductAllocation(
uuid: uuid ?? this.uuid,
product: product ?? this.product,
tag: tag ?? this.tag,
);
} }
@override @override
List<Object?> get props => [uuid, product, tag]; List<Object?> get props => [
} uuid,
spaceName,
class Subspace extends Equatable { icon,
final String uuid; productAllocations,
final String name; subspaces,
final List<ProductAllocation> productAllocations; ];
const Subspace({
required this.uuid,
required this.name,
required this.productAllocations,
});
factory Subspace.fromJson(Map<String, dynamic> json) {
return Subspace(
uuid: json['uuid'] as String,
name: json['subspaceName'] as String,
productAllocations: (json['productAllocations'] as List)
.map((e) => ProductAllocation.fromJson(e as Map<String, dynamic>))
.toList(),
);
}
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];
} }
@@ -0,0 +1,48 @@
import 'package:equatable/equatable.dart';
import 'package:syncrow_web/pages/space_management_v2/modules/space_details/domain/models/product_allocation.dart';
class Subspace extends Equatable {
final String uuid;
final String name;
final List<ProductAllocation> productAllocations;
const Subspace({
required this.uuid,
required this.name,
required this.productAllocations,
});
factory Subspace.fromJson(Map<String, dynamic> json) {
return Subspace(
uuid: json['uuid'] as String,
name: json['subspaceName'] as String,
productAllocations: (json['productAllocations'] as List)
.map((e) => ProductAllocation.fromJson(e as Map<String, dynamic>))
.toList(),
);
}
Map<String, dynamic> toJson() {
final isNewSubspace = uuid.endsWith('-NewTag');
return <String, dynamic>{
if (!isNewSubspace) 'uuid': uuid,
'subspaceName': name,
'productAllocations': productAllocations.map((e) => e.toJson()).toList(),
};
}
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];
}
@@ -1,6 +1,9 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:syncrow_web/pages/space_management_v2/modules/communities/domain/models/space_model.dart'; import 'package:syncrow_web/pages/space_management_v2/modules/communities/domain/models/space_model.dart';
import 'package:syncrow_web/pages/space_management_v2/modules/create_space/data/services/remote_create_space_service.dart';
import 'package:syncrow_web/pages/space_management_v2/modules/create_space/domain/params/create_space_param.dart';
import 'package:syncrow_web/pages/space_management_v2/modules/create_space/presentation/bloc/create_space_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/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/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/bloc/space_details_bloc.dart';
@@ -14,6 +17,8 @@ abstract final class SpaceDetailsDialogHelper {
static void showCreate( static void showCreate(
BuildContext context, { BuildContext context, {
required String communityUuid, required String communityUuid,
required void Function(SpaceModel updatedSpaceModel)? onSuccess,
String? parentUuid,
}) { }) {
showDialog<void>( showDialog<void>(
context: context, context: context,
@@ -24,14 +29,41 @@ abstract final class SpaceDetailsDialogHelper {
RemoteSpaceDetailsService(httpService: HTTPService()), RemoteSpaceDetailsService(httpService: HTTPService()),
), ),
), ),
BlocProvider(
create: (context) => CreateSpaceBloc(
RemoteCreateSpaceService(HTTPService()),
),
),
], ],
child: Builder( child: Builder(
builder: (context) => SpaceDetailsDialog( builder: (context) => BlocListener<CreateSpaceBloc, CreateSpaceState>(
listener: (context, state) => switch (state) {
CreateSpaceInitial() => null,
CreateSpaceLoading() => _onLoading(context),
CreateSpaceSuccess() => _onCreateSuccess(
context,
state.space,
onSuccess,
),
CreateSpaceFailure() => _onError(context, state.errorMessage),
},
child: SpaceDetailsDialog(
context: context, context: context,
title: const SelectableText('Create Space'), title: const SelectableText('Create Space'),
spaceModel: SpaceModel.empty(), spaceModel: SpaceModel.empty(),
onSave: (space) {}, onSave: (space) {
context.read<CreateSpaceBloc>().add(
CreateSpace(
CreateSpaceParam(
communityUuid: communityUuid, communityUuid: communityUuid,
space: space,
parentUuid: parentUuid,
),
),
);
},
communityUuid: communityUuid,
),
), ),
), ),
), ),
@@ -135,4 +167,14 @@ abstract final class SpaceDetailsDialogHelper {
), ),
); );
} }
static void _onCreateSuccess(
BuildContext context,
SpaceModel space,
void Function(SpaceModel updatedSpaceModel)? onSuccess,
) {
Navigator.of(context).pop();
Navigator.of(context).pop();
onSuccess?.call(space);
}
} }
@@ -1,7 +1,7 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:syncrow_web/common/edit_chip.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/domain/models/subspace.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/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/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/space_details/presentation/widgets/subspace_name_display_widget.dart';
@@ -1,5 +1,5 @@
import 'package:flutter/material.dart'; 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/domain/models/subspace.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_action_buttons.dart';
import 'package:syncrow_web/pages/space_management_v2/modules/space_details/presentation/widgets/sub_spaces_input.dart'; import 'package:syncrow_web/pages/space_management_v2/modules/space_details/presentation/widgets/sub_spaces_input.dart';
import 'package:uuid/uuid.dart'; import 'package:uuid/uuid.dart';
@@ -1,5 +1,5 @@
import 'package:flutter/material.dart'; 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/domain/models/subspace.dart';
import 'package:syncrow_web/pages/space_management_v2/modules/space_details/presentation/widgets/subspace_chip.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/color_manager.dart';
import 'package:syncrow_web/utils/extension/build_context_x.dart'; import 'package:syncrow_web/utils/extension/build_context_x.dart';
@@ -1,5 +1,5 @@
import 'package:flutter/material.dart'; 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/domain/models/subspace.dart';
import 'package:syncrow_web/utils/color_manager.dart'; import 'package:syncrow_web/utils/color_manager.dart';
import 'package:syncrow_web/utils/extension/build_context_x.dart'; import 'package:syncrow_web/utils/extension/build_context_x.dart';
@@ -1,6 +1,6 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.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/domain/models/subspace.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/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/color_manager.dart';
import 'package:syncrow_web/utils/extension/build_context_x.dart'; import 'package:syncrow_web/utils/extension/build_context_x.dart';
@@ -10,7 +10,12 @@ import 'package:syncrow_web/utils/color_manager.dart';
import 'package:syncrow_web/utils/extension/build_context_x.dart'; import 'package:syncrow_web/utils/extension/build_context_x.dart';
class AddDeviceTypeWidget extends StatefulWidget { class AddDeviceTypeWidget extends StatefulWidget {
const AddDeviceTypeWidget({super.key}); const AddDeviceTypeWidget({
super.key,
this.initialProducts = const [],
});
final List<Product> initialProducts;
@override @override
State<AddDeviceTypeWidget> createState() => _AddDeviceTypeWidgetState(); State<AddDeviceTypeWidget> createState() => _AddDeviceTypeWidgetState();
@@ -18,6 +23,16 @@ class AddDeviceTypeWidget extends StatefulWidget {
class _AddDeviceTypeWidgetState extends State<AddDeviceTypeWidget> { class _AddDeviceTypeWidgetState extends State<AddDeviceTypeWidget> {
final Map<Product, int> _selectedProducts = {}; final Map<Product, int> _selectedProducts = {};
final Map<Product, int> _initialProductCounts = {};
@override
void initState() {
super.initState();
for (final product in widget.initialProducts) {
_initialProductCounts[product] = (_initialProductCounts[product] ?? 0) + 1;
}
_selectedProducts.addAll(_initialProductCounts);
}
void _onIncrement(Product product) { void _onIncrement(Product product) {
setState(() { setState(() {
@@ -27,8 +42,12 @@ class _AddDeviceTypeWidgetState extends State<AddDeviceTypeWidget> {
void _onDecrement(Product product) { void _onDecrement(Product product) {
setState(() { setState(() {
if ((_selectedProducts[product] ?? 0) > 0) { final initialCount = _initialProductCounts[product] ?? 0;
_selectedProducts[product] = _selectedProducts[product]! - 1; final currentCount = _selectedProducts[product] ?? 0;
if (currentCount > initialCount) {
_selectedProducts[product] = currentCount - 1;
} else if (currentCount > 0 && initialCount == 0) {
_selectedProducts[product] = currentCount - 1;
if (_selectedProducts[product] == 0) { if (_selectedProducts[product] == 0) {
_selectedProducts.remove(product); _selectedProducts.remove(product);
} }
@@ -63,7 +82,22 @@ class _AddDeviceTypeWidgetState extends State<AddDeviceTypeWidget> {
actions: [ actions: [
SpaceDetailsActionButtons( SpaceDetailsActionButtons(
onSave: () { onSave: () {
final result = _selectedProducts.entries final resultMap = <Product, int>{};
resultMap.addAll(_selectedProducts);
for (final entry in _initialProductCounts.entries) {
final product = entry.key;
final initialCount = entry.value;
final currentCount = resultMap[product] ?? 0;
if (currentCount > initialCount) {
resultMap[product] = currentCount - initialCount;
} else {
resultMap.remove(product);
}
}
final result = resultMap.entries
.expand((entry) => List.generate(entry.value, (_) => entry.key)) .expand((entry) => List.generate(entry.value, (_) => entry.key))
.toList(); .toList();
Navigator.of(context).pop(result); Navigator.of(context).pop(result);
@@ -1,5 +1,6 @@
import 'package:flutter/material.dart'; 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/products/domain/models/product.dart';
import 'package:syncrow_web/pages/space_management_v2/modules/space_details/domain/models/product_allocation.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/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_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/domain/models/tag.dart';
@@ -205,7 +206,14 @@ class _AssignTagsDialogState extends State<AssignTagsDialog> {
onCancel: () async { onCancel: () async {
final newProducts = await showDialog<List<Product>>( final newProducts = await showDialog<List<Product>>(
context: context, context: context,
builder: (context) => const AddDeviceTypeWidget(), builder: (context) => AddDeviceTypeWidget(
initialProducts: [
..._space.productAllocations.map((e) => e.product),
..._space.subspaces
.expand((s) => s.productAllocations)
.map((e) => e.product),
],
),
); );
if (newProducts == null || newProducts.isEmpty) return; if (newProducts == null || newProducts.isEmpty) return;
@@ -1,7 +1,8 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:syncrow_web/common/dialog_dropdown.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/space_details/domain/models/product_allocation.dart';
import 'package:syncrow_web/pages/space_management_v2/modules/space_details/domain/models/subspace.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/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/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/bloc/tags_bloc.dart';
@@ -34,7 +34,7 @@ class RemoteUpdateSpaceService implements UpdateSpaceService {
} on DioException catch (e) { } on DioException catch (e) {
final message = e.response?.data as Map<String, dynamic>?; final message = e.response?.data as Map<String, dynamic>?;
final error = message?['error'] as Map<String, dynamic>?; final error = message?['error'] as Map<String, dynamic>?;
final errorMessage = error?['error'] as String? ?? ''; final errorMessage = error?['message'] as String? ?? '';
final formattedErrorMessage = [ final formattedErrorMessage = [
_defaultErrorMessage, _defaultErrorMessage,
errorMessage, errorMessage,
@@ -9,34 +9,5 @@ class UpdateSpaceParam {
final SpaceDetailsModel space; final SpaceDetailsModel space;
final String communityUuid; final String communityUuid;
Map<String, dynamic> toJson() { Map<String, dynamic> toJson() => space.toJson();
return {
'spaceName': space.spaceName,
'icon': space.icon,
'subspaces': space.subspaces.map((e) => e._toJson()).toList(),
'productAllocations':
space.productAllocations.map((e) => e._toJson()).toList(),
};
}
}
extension _ProductAllocationToJson on ProductAllocation {
Map<String, dynamic> _toJson() {
final isNewTag = tag.uuid.isEmpty;
return <String, dynamic>{
if (isNewTag) 'tagName': tag.name else 'tagUuid': tag.uuid,
'productUuid': product.uuid,
};
}
}
extension _SubspaceToJson on Subspace {
Map<String, dynamic> _toJson() {
final isNewSubspace = uuid.endsWith('-NewTag');
return <String, dynamic>{
if (!isNewSubspace) 'uuid': uuid,
'subspaceName': name,
'productAllocations': productAllocations.map((e) => e._toJson()).toList(),
};
}
} }
@@ -1,6 +1,8 @@
import 'package:bloc/bloc.dart'; import 'package:bloc/bloc.dart';
import 'package:equatable/equatable.dart'; import 'package:equatable/equatable.dart';
import 'package:syncrow_web/pages/space_management_v2/modules/space_details/domain/models/product_allocation.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/models/space_details_model.dart';
import 'package:syncrow_web/pages/space_management_v2/modules/space_details/domain/models/subspace.dart';
part 'space_details_model_event.dart'; part 'space_details_model_event.dart';
+10 -11
View File
@@ -12,20 +12,20 @@ import 'package:syncrow_web/services/api/http_service.dart';
import 'package:syncrow_web/utils/constants/api_const.dart'; import 'package:syncrow_web/utils/constants/api_const.dart';
class DevicesManagementApi { class DevicesManagementApi {
Future<List<AllDevicesModel>> fetchDevices( Future<List<AllDevicesModel>> fetchDevices(String projectId,
String communityId, String spaceId, String projectId) async { {List<String>? spacesId, List<String>? communities}) async {
try { try {
final response = await HTTPService().get( final response = await HTTPService().get(
path: communityId.isNotEmpty && spaceId.isNotEmpty path: ApiEndpoints.getSpaceDevices.replaceAll('{projectId}', projectId),
? ApiEndpoints.getSpaceDevices queryParameters: {
.replaceAll('{spaceUuid}', spaceId) if (spacesId != null && spacesId.isNotEmpty) 'spaces': spacesId,
.replaceAll('{communityUuid}', communityId) if (communities != null && communities.isNotEmpty)
.replaceAll('{projectId}', projectId) 'communities': communities,
: ApiEndpoints.getAllDevices.replaceAll('{projectId}', projectId), },
showServerMessage: true, showServerMessage: true,
expectedResponseModel: (json) { expectedResponseModel: (json) {
List<dynamic> jsonData = json['data']; final List<dynamic> jsonData = json['data'] as List<dynamic>;
List<AllDevicesModel> devicesList = jsonData.map((jsonItem) { final List<AllDevicesModel> devicesList = jsonData.map((jsonItem) {
return AllDevicesModel.fromJson(jsonItem); return AllDevicesModel.fromJson(jsonItem);
}).toList(); }).toList();
return devicesList; return devicesList;
@@ -416,5 +416,4 @@ class DevicesManagementApi {
); );
return response; return response;
} }
} }
+1 -3
View File
@@ -4,9 +4,7 @@ import 'package:syncrow_web/services/api/http_interceptor.dart';
import 'package:syncrow_web/services/api/http_service.dart'; import 'package:syncrow_web/services/api/http_service.dart';
final GetIt serviceLocator = GetIt.instance; final GetIt serviceLocator = GetIt.instance;
//setupLocator() // to search for dependency injection in flutter void initialSetup() {
initialSetup() {
serviceLocator.registerSingleton<HTTPInterceptor>(HTTPInterceptor()); serviceLocator.registerSingleton<HTTPInterceptor>(HTTPInterceptor());
//Base classes
serviceLocator.registerSingleton<Dio>(HTTPService.setupDioClient()); serviceLocator.registerSingleton<Dio>(HTTPService.setupDioClient());
} }
+19 -11
View File
@@ -34,8 +34,9 @@ class UserPermissionApi {
path: ApiEndpoints.roleTypes, path: ApiEndpoints.roleTypes,
showServerMessage: true, showServerMessage: true,
expectedResponseModel: (json) { expectedResponseModel: (json) {
final List<RoleTypeModel> fetchedRoles = final List<RoleTypeModel> fetchedRoles = (json['data'] as List)
(json['data'] as List).map((item) => RoleTypeModel.fromJson(item)).toList(); .map((item) => RoleTypeModel.fromJson(item))
.toList();
return fetchedRoles; return fetchedRoles;
}, },
); );
@@ -47,7 +48,9 @@ class UserPermissionApi {
path: ApiEndpoints.permission.replaceAll("roleUuid", roleUuid), path: ApiEndpoints.permission.replaceAll("roleUuid", roleUuid),
showServerMessage: true, showServerMessage: true,
expectedResponseModel: (json) { expectedResponseModel: (json) {
return (json as List).map((data) => PermissionOption.fromJson(data)).toList(); return (json as List)
.map((data) => PermissionOption.fromJson(data))
.toList();
}, },
); );
return response ?? []; return response ?? [];
@@ -57,7 +60,7 @@ class UserPermissionApi {
String? firstName, String? firstName,
String? lastName, String? lastName,
String? email, String? email,
String? jobTitle, String? companyName,
String? phoneNumber, String? phoneNumber,
String? roleUuid, String? roleUuid,
List<String>? spaceUuids, List<String>? spaceUuids,
@@ -68,7 +71,7 @@ class UserPermissionApi {
"firstName": firstName, "firstName": firstName,
"lastName": lastName, "lastName": lastName,
"email": email, "email": email,
"jobTitle": jobTitle != '' ? jobTitle : null, "companyName": companyName != '' ? companyName : null,
"phoneNumber": phoneNumber != '' ? phoneNumber : null, "phoneNumber": phoneNumber != '' ? phoneNumber : null,
"roleUuid": roleUuid, "roleUuid": roleUuid,
"projectUuid": projectUuid, "projectUuid": projectUuid,
@@ -140,7 +143,7 @@ class UserPermissionApi {
String? firstName, String? firstName,
String? userId, String? userId,
String? lastName, String? lastName,
String? jobTitle, String? companyName,
String? phoneNumber, String? phoneNumber,
String? roleUuid, String? roleUuid,
List<String>? spaceUuids, List<String>? spaceUuids,
@@ -150,8 +153,8 @@ class UserPermissionApi {
final body = <String, dynamic>{ final body = <String, dynamic>{
"firstName": firstName, "firstName": firstName,
"lastName": lastName, "lastName": lastName,
"jobTitle": jobTitle != '' ? jobTitle : " ", "companyName": companyName != '' ? companyName : ' ',
"phoneNumber": phoneNumber != '' ? phoneNumber : " ", "phoneNumber": phoneNumber != '' ? phoneNumber : ' ',
"roleUuid": roleUuid, "roleUuid": roleUuid,
"projectUuid": projectUuid, "projectUuid": projectUuid,
"spaceUuids": spaceUuids, "spaceUuids": spaceUuids,
@@ -190,12 +193,17 @@ class UserPermissionApi {
} }
} }
Future<bool> changeUserStatusById(userUuid, status, String projectUuid) async { Future<bool> changeUserStatusById(
userUuid, status, String projectUuid) async {
try { try {
Map<String, dynamic> bodya = {"disable": status, "projectUuid": projectUuid}; Map<String, dynamic> bodya = {
"disable": status,
"projectUuid": projectUuid
};
final response = await _httpService.put( final response = await _httpService.put(
path: ApiEndpoints.changeUserStatus.replaceAll("{invitedUserUuid}", userUuid), path: ApiEndpoints.changeUserStatus
.replaceAll("{invitedUserUuid}", userUuid),
body: bodya, body: bodya,
expectedResponseModel: (json) { expectedResponseModel: (json) {
return json['success']; return json['success'];
+53
View File
@@ -0,0 +1,53 @@
import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:syncrow_web/pages/home/bloc/home_bloc.dart';
import 'package:syncrow_web/pages/home/bloc/home_event.dart';
import 'package:syncrow_web/pages/routines/bloc/create_routine_bloc/create_routine_bloc.dart';
import 'package:syncrow_web/pages/routines/bloc/routine_bloc/routine_bloc.dart';
import 'package:syncrow_web/pages/space_tree/bloc/space_tree_bloc.dart';
import 'package:syncrow_web/pages/visitor_password/bloc/visitor_password_bloc.dart';
import 'package:syncrow_web/utils/app_routes.dart';
import 'package:syncrow_web/utils/navigation_service.dart';
import 'package:syncrow_web/utils/theme/theme.dart';
class SyncrowApp extends StatelessWidget {
const SyncrowApp({super.key});
@override
Widget build(BuildContext context) {
return MultiBlocProvider(
providers: [
BlocProvider<CreateRoutineBloc>(
create: (context) => CreateRoutineBloc(),
),
BlocProvider(
create: (context) => HomeBloc()..add(const FetchUserInfo()),
),
BlocProvider<VisitorPasswordBloc>(
create: (context) => VisitorPasswordBloc(),
),
BlocProvider<RoutineBloc>(
create: (context) => RoutineBloc(),
),
BlocProvider<SpaceTreeBloc>(
create: (context) => SpaceTreeBloc(),
),
],
child: MaterialApp.router(
debugShowCheckedModeBanner: false,
scrollBehavior: const MaterialScrollBehavior().copyWith(
dragDevices: {
PointerDeviceKind.mouse,
PointerDeviceKind.touch,
PointerDeviceKind.stylus,
PointerDeviceKind.unknown,
},
),
key: NavigationService.navigatorKey,
theme: myTheme,
routerConfig: AppRoutes.router,
),
);
}
}
+20 -6
View File
@@ -1,17 +1,31 @@
import 'package:go_router/go_router.dart'; import 'package:go_router/go_router.dart';
import 'package:syncrow_web/pages/access_management/view/access_management.dart'; import 'package:syncrow_web/pages/access_management/view/access_management.dart';
import 'package:syncrow_web/pages/analytics/modules/analytics/views/analytics_page.dart'; import 'package:syncrow_web/pages/analytics/modules/analytics/views/analytics_page.dart';
import 'package:syncrow_web/pages/auth/bloc/auth_bloc.dart';
import 'package:syncrow_web/pages/auth/view/login_page.dart'; import 'package:syncrow_web/pages/auth/view/login_page.dart';
import 'package:syncrow_web/pages/device_managment/all_devices/view/device_managment_page.dart'; import 'package:syncrow_web/pages/device_managment/all_devices/view/device_managment_page.dart';
import 'package:syncrow_web/pages/home/view/home_page.dart'; import 'package:syncrow_web/pages/home/view/home_page.dart';
import 'package:syncrow_web/pages/roles_and_permission/view/roles_and_permission_page.dart'; import 'package:syncrow_web/pages/roles_and_permission/view/roles_and_permission_page.dart';
import 'package:syncrow_web/pages/spaces_management/all_spaces/view/spaces_management_page.dart'; import 'package:syncrow_web/pages/space_management_v2/main_module/views/space_management_page.dart';
import 'package:syncrow_web/pages/visitor_password/view/visitor_password_dialog.dart'; import 'package:syncrow_web/pages/visitor_password/view/visitor_password_dialog.dart';
import 'package:syncrow_web/utils/constants/routes_const.dart'; import 'package:syncrow_web/utils/constants/routes_const.dart';
class AppRoutes { abstract final class AppRoutes {
static List<GoRoute> getRoutes() { const AppRoutes._();
return [
static final GoRouter router = GoRouter(
initialLocation: RoutesConst.auth,
redirect: (context, state) async {
final checkToken = await AuthBloc.getTokenAndValidate();
final loggedIn = checkToken == 'Success';
final goingToLogin = state.uri.toString() == RoutesConst.auth;
if (!loggedIn && !goingToLogin) return RoutesConst.auth;
if (loggedIn && goingToLogin) return RoutesConst.home;
return null;
},
routes: [
GoRoute( GoRoute(
path: RoutesConst.auth, path: RoutesConst.auth,
builder: (context, state) => const LoginPage(), builder: (context, state) => const LoginPage(),
@@ -43,6 +57,6 @@ class AppRoutes {
name: 'analytics', name: 'analytics',
builder: (context, state) => const AnalyticsPage(), builder: (context, state) => const AnalyticsPage(),
), ),
]; ],
} );
} }
+2 -1
View File
@@ -69,7 +69,6 @@ abstract class ColorsManager {
static const Color invitedOrange = Color(0xFFFFE193); static const Color invitedOrange = Color(0xFFFFE193);
static const Color invitedOrangeText = Color(0xFFFFBF00); static const Color invitedOrangeText = Color(0xFFFFBF00);
static const Color lightGrayBorderColor = Color(0xB2D5D5D5); static const Color lightGrayBorderColor = Color(0xB2D5D5D5);
//background: #F8F8F8;
static const Color vividBlue = Color(0xFF023DFE); static const Color vividBlue = Color(0xFF023DFE);
static const Color semiTransparentRed = Color(0x99FF0000); static const Color semiTransparentRed = Color(0x99FF0000);
static const Color grey700 = Color(0xFF2D3748); static const Color grey700 = Color(0xFF2D3748);
@@ -85,4 +84,6 @@ abstract class ColorsManager {
static const Color minBlueDot = Color(0xFF023DFE); static const Color minBlueDot = Color(0xFF023DFE);
static const Color grey25 = Color(0xFFF9F9F9); static const Color grey25 = Color(0xFFF9F9F9);
static const Color grey50 = Color(0xFF718096); static const Color grey50 = Color(0xFF718096);
static const Color red100 = Color(0xFFFE0202);
static const Color grey800 = Color(0xffF8F8F8);
} }
+5 -2
View File
@@ -17,8 +17,7 @@ abstract class ApiEndpoints {
////// Devices Management //////////////// ////// Devices Management ////////////////
static const String getAllDevices = '/projects/{projectId}/devices'; static const String getAllDevices = '/projects/{projectId}/devices';
static const String getSpaceDevices = static const String getSpaceDevices = '/projects/{projectId}/devices';
'/projects/{projectId}/communities/{communityUuid}/spaces/{spaceUuid}/devices';
static const String getDeviceStatus = '/devices/{uuid}/functions/status'; static const String getDeviceStatus = '/devices/{uuid}/functions/status';
static const String getBatchStatus = '/devices/batch'; static const String getBatchStatus = '/devices/batch';
@@ -42,6 +41,8 @@ abstract class ApiEndpoints {
'/projects/{projectId}/communities/{communityId}/spaces/{spaceId}'; '/projects/{projectId}/communities/{communityId}/spaces/{spaceId}';
static const String getSpaceHierarchy = static const String getSpaceHierarchy =
'/projects/{projectId}/communities/{communityId}/spaces'; '/projects/{projectId}/communities/{communityId}/spaces';
static const String reorderSpaces =
'/projects/{projectUuid}/communities/{communityUuid}/spaces/{parentSpaceUuid}/spaces/order';
// Community Module // Community Module
static const String createCommunity = '/projects/{projectId}/communities'; static const String createCommunity = '/projects/{projectId}/communities';
@@ -141,4 +142,6 @@ abstract class ApiEndpoints {
static const String saveSchedule = '/schedule/{deviceUuid}'; static const String saveSchedule = '/schedule/{deviceUuid}';
static const String getBookableSpaces = '/bookable-spaces'; static const String getBookableSpaces = '/bookable-spaces';
static const String getBookings =
'/bookings?month={mm}%2F{yyyy}&space={space}';
} }
+1
View File
@@ -517,4 +517,5 @@ class Assets {
static const String emptyRangeOfAqi = 'assets/icons/empty_range_of_aqi.svg'; static const String emptyRangeOfAqi = 'assets/icons/empty_range_of_aqi.svg';
static const String homeIcon = 'assets/icons/home_icon.svg'; static const String homeIcon = 'assets/icons/home_icon.svg';
static const String groupIcon = 'assets/icons/group_icon.svg'; static const String groupIcon = 'assets/icons/group_icon.svg';
static const String xDelete = 'assets/icons/x_delete.svg';
} }