Compare commits

..

67 Commits

Author SHA1 Message Date
6b8827f4d9 fixes after merge 2025-07-15 15:37:08 +03:00
e8c36f5af6 fix after merge 2025-07-15 15:36:20 +03:00
762dade195 Merge branch 'dev' of https://github.com/SyncrowIOT/web into Implement-Spaces-Table-Empty-Filled-Failure-states-bookable-spaces 2025-07-15 15:35:31 +03:00
f5def4b4d7 to open Request 2025-07-15 15:26:37 +03:00
ab326fa820 delete dummy files 2025-07-15 15:18:14 +03:00
e9b4d35f97 add settings and edit feature to bookable spaces 2025-07-15 15:17:43 +03:00
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
fe4063ef8f changed the title from Routine to Workflow Automation 2025-07-15 11:20:13 +03:00
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
428c81efff Adjust table scroll behavior and modify height for improved layout 2025-07-15 10:05:07 +03:00
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
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
08e2ed4b4c Merge branch 'dev' into revert-SP-1589 2025-07-15 08:31:45 +03:00
59e04708cd Merge branch 'main' into revert-SP-1589 2025-07-15 08:30:33 +03:00
338d4f5737 fix typo 2025-07-15 08:19:43 +03:00
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
249cbfc242 Merge branch 'dev' into Build-Schedule-List-View-Support-State-Persistence 2025-07-14 15:20:08 +03:00
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
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
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
c112cde634 Uses Inkwell instead of Gesture Detector for canvas widgets. 2025-07-14 11:04:29 +03:00
65d541d594 Add calendar event management features and UI components and Implement Calendar logic 2025-07-14 10:46:12 +03:00
7331c8440b Refactor SpaceManagementPage to use StatefulWidget and initialize CommunitiesBloc in initState. Update CommunityStructureHeader to handle community updates and improve state management in CommunitiesTreeSelectionBloc with new event for community state updates. 2025-07-14 10:27:22 +03:00
a409e34643 Merge branch 'SP-1717-FE-Draw-Create-Edit-Space-Dialog' of https://github.com/SyncrowIOT/web into SP-1717-FE-Draw-Create-Edit-Space-Dialog 2025-07-14 10:16:31 +03:00
7cc59e43df Setup new firebase project in the web platform. (#343)
<!--
  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

Setup new firebase project in the web platform.

## Type of Change

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

- [ ]  New feature (non-breaking change which adds functionality)
- [ ] 🛠️ Bug fix (non-breaking change which fixes an issue)
- [ ]  Breaking change (fix or feature that would cause existing
functionality to change)
- [ ] 🧹 Code refactor
- [x]  Build configuration change
- [ ] 📝 Documentation
- [ ] 🗑️ Chore
2025-07-13 13:28:32 +03:00
2681c837f5 add company name and replace it with job title 2025-07-11 12:10:53 +03:00
b6664ec1ba fix Redundant API calls on Routines page when selecting a community from the tree 2025-07-11 10:53:04 +03:00
a9895f5462 fix checkbox issue 2025-07-11 10:27:18 +03:00
6e4f0c3c0c edit time picker as in figma 2025-07-10 15:52:52 +03:00
bbf2891804 refactor code 2025-07-10 15:52:38 +03:00
aab2b4a52a insert after update and refactor 2025-07-10 15:52:15 +03:00
d58da9644f add toggling for points 2025-07-10 15:51:44 +03:00
df46a5b905 update spaces remote files 2025-07-10 15:51:17 +03:00
a1fa049a05 no need for dummy data 2025-07-10 15:50:56 +03:00
494a000590 update bookable space logic 2025-07-10 15:50:24 +03:00
b5d72b2a2a refactor code 2025-07-10 15:49:30 +03:00
55a73eee7f we can switch pages in access managment 2025-07-10 15:48:13 +03:00
21f8b2962c Refactor date selection: add SelectDateFromSidebarCalendar event and … (#344)
…update state management for improved clarity

<!--
  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 highlighted selected day

## 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-10 15:05:09 +03:00
645a07287e Refactor date selection: add SelectDateFromSidebarCalendar event and update state management for improved clarity 2025-07-10 14:15:57 +03:00
df29aab111 Setup new firebase project in the web platform. 2025-07-10 12:18:45 +03:00
e55e793081 Implement-Calendar-ui (#342)
<!--
  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 ui

## 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-10 12:13:40 +03:00
83202204b0 Remove BlocProvider for UpdateSpaceBloc in SpaceDetailsDialogHelper to streamline dependency management and improve code clarity. 2025-07-09 15:58:17 +03:00
d87739f1fd Refactor JSON Serialization in UpdateSpaceParam: Adjusted the _toJson method for Subspace to ensure 'subspaceName' is always included and 'uuid' is only added when applicable, enhancing clarity and consistency in data representation. 2025-07-09 15:25:41 +03:00
b128618bfd clean the code for save and next buttons and enhance UI 2025-07-09 15:11:27 +03:00
5cd083a37b Refactor Space and Tag Models: Removed unused JSON serialization methods from SpaceDetailsModel, ProductAllocation, and Subspace. Updated Tag model to eliminate unnecessary fields. Enhanced UpdateSpaceParam to streamline JSON conversion for subspaces and product allocations, improving data handling during updates. 2025-07-09 15:08:49 +03:00
2b8d987c69 Add SpaceReorderDataModel and integrate drag-and-drop functionality in CommunityStructureCanvas for improved space management. 2025-07-08 16:00:57 +03:00
707cb4791f Added CreateSpaceButton for improved user interaction and updated layout calculations to utilize context extensions for better responsiveness. 2025-07-08 13:08:43 +03:00
03c45ed8d0 Refactor SpaceCardWidget: Simplified widget structure by removing unnecessary SizedBox. 2025-07-08 13:07:55 +03:00
9e0ea4ad6f Adjust spacer flex in SpaceManagementCommunityStructure widget for improved layout consistency. 2025-07-08 13:07:39 +03:00
9e6b14737f Refactor CreateSpaceButton: Changed from StatelessWidget to StatefulWidget to manage hover state and added tooltip for improved user experience. Enhanced button styling and interaction feedback for better visual cues during space creation. 2025-07-08 13:07:26 +03:00
7c2aed2d58 Refactor RemoteUpdateSpaceService: Improved error handling in updateSpace method by checking API response success before returning the updated space. This enhances robustness and ensures proper error propagation for failed updates. 2025-07-08 12:20:10 +03:00
bcf62027bc Validate UUIDs in RemoteUpdateSpaceService: Added checks for empty space and community UUIDs before constructing the update URL, improving error handling and robustness in the update space process. 2025-07-08 11:12:12 +03:00
b001713ce4 Enhance Community Structure Widgets: Updated SpaceDetailsDialogHelper to accept community UUID for space creation and editing. Refactored CreateSpaceButton and CommunityStructureHeader to pass community UUID, improving data handling and consistency across the community structure features. 2025-07-08 11:10:22 +03:00
bab3226c73 Merge branch 'dev' of https://github.com/SyncrowIOT/web into SP-1717-FE-Draw-Create-Edit-Space-Dialog 2025-07-08 10:02:12 +03:00
fa1eaa570c Refactor Space Update Logic: Introduced UpdateSpaceParam for better parameter handling in update operations. Enhanced SpaceDetailsDialogHelper to manage loading and error states during space updates. Updated RemoteUpdateSpaceService to construct dynamic URLs for space updates based on community UUID. Improved CommunitiesTreeFailureWidget UI with SelectableText and added spacing for better layout. 2025-07-08 10:01:43 +03:00
42c410d982 use TimeOfDay instead of String 2025-07-07 17:07:38 +03:00
368b1be3c0 fix conditions for start and end time for reservation 2025-07-07 17:07:23 +03:00
c13119a4e8 migrate datatable2 package 2025-07-07 15:41:02 +03:00
35e9b606b2 use param to send Update Api for unbookable to be bookable 2025-07-07 15:40:49 +03:00
387586f6f7 unused commnet 2025-07-07 15:40:20 +03:00
7cf4d0b5a9 add to route and related endpoints and color and assets 2025-07-07 15:40:04 +03:00
e4a27b5651 build services for them 2025-07-07 15:39:19 +03:00
f89660a9ff build modeling and params 2025-07-07 15:38:52 +03:00
1a3dc60bd2 build blocs for bookable and nonBookable spaces 2025-07-07 15:38:18 +03:00
201348a9bf build dialog with steps view 2025-07-07 15:37:38 +03:00
e2d4e48875 build main screens with its widgets for bookableScreen 2025-07-07 15:36:35 +03:00
dcf1df9b4a sp1613 delete condition word 2025-05-21 07:25:34 -05:00
135 changed files with 4858 additions and 1014 deletions

View File

@ -1,2 +1,3 @@
ENV_NAME=development ENV_NAME=development
BASE_URL=https://syncrow-dev.azurewebsites.net BASE_URL=https://syncrow-dev.azurewebsites.net
RTDB_URL=https://syncrow-dev-79446.asia-southeast1.firebasedatabase.app/

View File

@ -1,2 +1,3 @@
ENV_NAME=production ENV_NAME=production
BASE_URL=https://syncrow-staging.azurewebsites.net BASE_URL=https://syncrow-staging.azurewebsites.net
RTDB_URL=https://syncrow-prod-79446.asia-southeast1.firebasedatabase.app/

View File

@ -1,2 +1,3 @@
ENV_NAME=staging ENV_NAME=staging
BASE_URL=https://syncrow-staging.azurewebsites.net BASE_URL=https://syncrow-staging.azurewebsites.net
RTDB_URL=https://syncrow-staging-79446.asia-southeast1.firebasedatabase.app/

112
.vscode/launch.json vendored
View File

@ -1,67 +1,49 @@
{ {
"configurations": [ "configurations": [
{
{ "name": "DEVELOPMENT",
"request": "launch",
"name": "DEVELOPMENT", "type": "dart",
"args": [
"request": "launch", "-d",
"chrome",
"type": "dart", "--web-port",
"3000",
"args": [ "-t",
"-d", "lib/main_dev.dart",
"chrome", "--web-experimental-hot-reload"
"--web-port", ],
"3000", "flutterMode": "debug"
"-t", },
"lib/main_dev.dart", {
"--web-experimental-hot-reload", "name": "STAGING",
], "request": "launch",
"type": "dart",
"flutterMode": "debug" "args": [
"-d",
},{ "chrome",
"--web-port",
"name": "STAGING", "3000",
"-t",
"request": "launch", "lib/main_staging.dart",
"--web-experimental-hot-reload"
"type": "dart", ],
"flutterMode": "debug"
"args": [ },
"-d", {
"chrome", "name": "PRODUCTION",
"--web-port", "request": "launch",
"3000", "type": "dart",
"-t", "args": [
"lib/main_staging.dart", "-d",
"--web-experimental-hot-reload", "chrome",
], "--web-port",
"3000",
"flutterMode": "debug" "-t",
"lib/main.dart",
},{ "--web-experimental-hot-reload"
],
"name": "PRODUCTION", "flutterMode": "debug"
}
"request": "launch", ]
"type": "dart",
"args": [
"-d",
"chrome",
"--web-port",
"3000",
"-t",
"lib/main.dart",
"--web-experimental-hot-reload",
],
"flutterMode": "debug"
},
]
} }

View File

@ -0,0 +1,10 @@
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<g clip-path="url(#clip0_9795_9381)">
<path d="M9.21875 13.5149V10.7805H6.48438C6.05286 10.7805 5.70312 10.4308 5.70312 9.99924C5.70312 9.56787 6.05286 9.21799 6.48438 9.21799H9.21875V6.48361C9.21875 6.05225 9.56848 5.70236 10 5.70236C10.4315 5.70236 10.7812 6.05225 10.7812 6.48361V9.21799H13.5156C13.9471 9.21799 14.2969 9.56787 14.2969 9.99924C14.2969 10.4308 13.9471 10.7805 13.5156 10.7805H10.7812V13.5149C10.7812 13.9464 10.4315 14.2961 10 14.2961C9.56848 14.2961 9.21875 13.9464 9.21875 13.5149ZM17.0711 2.92892C15.1823 1.04019 12.6711 0 10 0C7.32895 0 4.81766 1.04019 2.92892 2.92892C1.04019 4.81766 0 7.32895 0 10C0 12.6711 1.04019 15.1823 2.92892 17.0711C4.81766 18.9598 7.32895 20 10 20C11.8286 20 13.6179 19.5016 15.1743 18.5588C15.5434 18.3353 15.6613 17.8549 15.4378 17.486C15.2142 17.1169 14.7337 16.9989 14.3648 17.2224C13.0525 18.0173 11.5431 18.4375 10 18.4375C5.3476 18.4375 1.5625 14.6524 1.5625 10C1.5625 5.3476 5.3476 1.5625 10 1.5625C14.6524 1.5625 18.4375 5.3476 18.4375 10C18.4375 11.6637 17.9428 13.2829 17.0068 14.6831C16.767 15.0417 16.8634 15.5269 17.2221 15.7668C17.5807 16.0065 18.0659 15.91 18.3058 15.5515C19.4141 13.8936 20 11.9739 20 10C20 7.32895 18.9598 4.81766 17.0711 2.92892Z" fill="#023DFE" fill-opacity="0.7"/>
</g>
<defs>
<clipPath id="clip0_9795_9381">
<rect width="20" height="20" fill="white"/>
</clipPath>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 1.4 KiB

View File

@ -0,0 +1,4 @@
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M10 20C15.514 20 19.9998 15.514 19.9998 9.99995C19.9998 4.48604 15.514 0 10 0C4.48613 0 0.000183105 4.48604 0.000183105 9.99995C0.000183105 15.514 4.48613 20 10 20ZM10 1.36892C14.7591 1.36892 18.6309 5.24077 18.631 9.99995C18.631 14.7591 14.7592 18.631 10 18.6311C5.24095 18.631 1.36919 14.7591 1.36919 9.99986C1.36919 5.24086 5.24095 1.36892 10 1.36892Z" fill="#023DFE" fill-opacity="0.7"/>
<path d="M8.65713 14.2828C8.92444 14.55 9.35784 14.5499 9.62505 14.2828C9.89245 14.0154 9.89245 13.5821 9.62496 13.3147L6.99481 10.6846L14.6112 10.6839C14.9892 10.6838 15.2956 10.3775 15.2956 9.99926C15.2955 9.62126 14.9891 9.31499 14.6111 9.31499L6.99444 9.31572L9.62523 6.68511C9.89254 6.41781 9.89254 5.98432 9.62523 5.7171C9.49154 5.5835 9.3164 5.5166 9.14118 5.5166C8.96605 5.5166 8.79092 5.5835 8.65722 5.71701L4.85811 9.51604C4.7297 9.64435 4.65761 9.81838 4.65761 9.99999C4.6577 10.1816 4.7298 10.3555 4.8582 10.4841L8.65713 14.2828Z" fill="#023DFE" fill-opacity="0.7"/>
</svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

View File

@ -0,0 +1,4 @@
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M9.9999 0C4.48595 0 0 4.48586 0 9.99971C0 15.514 4.48595 20 9.9999 20C15.5138 20 19.9996 15.5139 19.9996 9.99971C19.9996 4.48586 15.5138 0 9.9999 0ZM9.9999 18.5665C5.27638 18.5665 1.43349 14.7234 1.43349 9.99971C1.43349 5.27628 5.27638 1.43349 9.9999 1.43349C14.7233 1.43349 18.5661 5.27628 18.5661 9.99971C18.5661 14.7234 14.7233 18.5665 9.9999 18.5665Z" fill="#D5D5D5"/>
<path d="M15.1416 9.83211H10.4423V4.69526C10.4423 4.29943 10.1215 3.97852 9.72553 3.97852C9.3297 3.97852 9.00879 4.29943 9.00879 4.69526V10.5489C9.00879 10.9447 9.3297 11.2656 9.72553 11.2656H15.1416C15.5376 11.2656 15.8584 10.9447 15.8584 10.5489C15.8584 10.153 15.5375 9.83211 15.1416 9.83211Z" fill="#D5D5D5"/>
</svg>

After

Width:  |  Height:  |  Size: 799 B

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 18 KiB

View File

@ -1 +1 @@
{"flutter":{"platforms":{"android":{"default":{"projectId":"test2-8a3d2","appId":"1:427332280600:android:2bc36fbe82994a3e0c7e6d","fileOutput":"android/app/google-services.json"}},"ios":{"default":{"projectId":"test2-8a3d2","appId":"1:427332280600:ios:14346b200780dc760c7e6d","uploadDebugSymbols":true,"fileOutput":"ios/Runner/GoogleService-Info.plist"}},"macos":{"default":{"projectId":"test2-8a3d2","appId":"1:427332280600:ios:14346b200780dc760c7e6d","uploadDebugSymbols":true,"fileOutput":"macos/Runner/GoogleService-Info.plist"}},"dart":{"lib/firebase_options.dart":{"projectId":"test2-8a3d2","configurations":{"android":"1:427332280600:android:2bc36fbe82994a3e0c7e6d","ios":"1:427332280600:ios:14346b200780dc760c7e6d","macos":"1:427332280600:ios:14346b200780dc760c7e6d","web":"1:427332280600:web:ad50516a87a35a1a0c7e6d","windows":"1:427332280600:web:f7a25537ccd5a7bd0c7e6d"}}}}}} {"flutter":{"platforms":{"android":{"default":{"projectId":"test2-8a3d2","appId":"1:427332280600:android:2bc36fbe82994a3e0c7e6d","fileOutput":"android/app/google-services.json"}},"ios":{"default":{"projectId":"test2-8a3d2","appId":"1:427332280600:ios:14346b200780dc760c7e6d","uploadDebugSymbols":true,"fileOutput":"ios/Runner/GoogleService-Info.plist"}},"macos":{"default":{"projectId":"test2-8a3d2","appId":"1:427332280600:ios:14346b200780dc760c7e6d","uploadDebugSymbols":true,"fileOutput":"macos/Runner/GoogleService-Info.plist"}},"dart":{"lib/firebase_options.dart":{"projectId":"syncrow-prod-79446","configurations":{"web":"1:255001682464:web:a03e2d6214c13101561245"}}}}}}

16
lib/firebase_options.dart Normal file
View File

@ -0,0 +1,16 @@
import 'package:firebase_core/firebase_core.dart' show FirebaseOptions;
final class DefaultFirebaseOptions extends FirebaseOptions {
const DefaultFirebaseOptions({
required String databaseUrl,
}) : super(
apiKey: 'AIzaSyDgq5ywsnFVbbQO-Xz1Z4sR5bBcuiDaS9g',
appId: '1:255001682464:web:a03e2d6214c13101561245',
messagingSenderId: '255001682464',
projectId: 'syncrow-prod-79446',
authDomain: 'syncrow-prod-79446.firebaseapp.com',
storageBucket: 'syncrow-prod-79446.firebasestorage.app',
databaseURL: databaseUrl,
measurementId: 'G-1850Q89RMK',
);
}

View File

@ -1,93 +0,0 @@
// File generated by FlutterFire CLI.
// ignore_for_file: type=lint
import 'package:firebase_core/firebase_core.dart' show FirebaseOptions;
import 'package:flutter/foundation.dart'
show defaultTargetPlatform, kIsWeb, TargetPlatform;
/// Default [FirebaseOptions] for use with your Firebase apps.
///
/// Example:
/// ```dart
/// import 'firebase_options.dart';
/// // ...
/// await Firebase.initializeApp(
/// options: DefaultFirebaseOptions.currentPlatform,
/// );
/// ```
class DefaultFirebaseOptionsDev {
static FirebaseOptions get currentPlatform {
if (kIsWeb) {
return web;
}
switch (defaultTargetPlatform) {
case TargetPlatform.android:
return android;
case TargetPlatform.iOS:
return ios;
case TargetPlatform.macOS:
return macos;
case TargetPlatform.windows:
return windows;
case TargetPlatform.linux:
throw UnsupportedError(
'DefaultFirebaseOptions have not been configured for linux - '
'you can reconfigure this by running the FlutterFire CLI again.',
);
default:
throw UnsupportedError(
'DefaultFirebaseOptions are not supported for this platform.',
);
}
}
static const FirebaseOptions web = FirebaseOptions(
apiKey: 'AIzaSyCVEvKsJYzhWDFM-9Od68FE0nPpP933st0',
appId: '1:427332280600:web:ad50516a87a35a1a0c7e6d',
messagingSenderId: '427332280600',
projectId: 'test2-8a3d2',
authDomain: 'test2-8a3d2.firebaseapp.com',
databaseURL: 'https://test2-8a3d2-default-rtdb.firebaseio.com',
storageBucket: 'test2-8a3d2.firebasestorage.app',
measurementId: 'G-Z1RTTTV5H9',
);
static const FirebaseOptions android = FirebaseOptions(
apiKey: 'AIzaSyA5qOErxdm0zJmoHIB0TixfebYEsNRpwV0',
appId: '1:427332280600:android:2bc36fbe82994a3e0c7e6d',
messagingSenderId: '427332280600',
projectId: 'test2-8a3d2',
databaseURL: 'https://test2-8a3d2-default-rtdb.firebaseio.com',
storageBucket: 'test2-8a3d2.firebasestorage.app',
);
static const FirebaseOptions ios = FirebaseOptions(
apiKey: 'AIzaSyABnpH6yo2RRjtkp4PlvtK84hKwRm2DhBw',
appId: '1:427332280600:ios:14346b200780dc760c7e6d',
messagingSenderId: '427332280600',
projectId: 'test2-8a3d2',
databaseURL: 'https://test2-8a3d2-default-rtdb.firebaseio.com',
storageBucket: 'test2-8a3d2.firebasestorage.app',
iosBundleId: 'com.example.syncrowWeb',
);
static const FirebaseOptions macos = FirebaseOptions(
apiKey: 'AIzaSyABnpH6yo2RRjtkp4PlvtK84hKwRm2DhBw',
appId: '1:427332280600:ios:14346b200780dc760c7e6d',
messagingSenderId: '427332280600',
projectId: 'test2-8a3d2',
databaseURL: 'https://test2-8a3d2-default-rtdb.firebaseio.com',
storageBucket: 'test2-8a3d2.firebasestorage.app',
iosBundleId: 'com.example.syncrowWeb',
);
static const FirebaseOptions windows = FirebaseOptions(
apiKey: 'AIzaSyDizKjPC5rdkEjDxwXjM-RU5unB0Ziq3iw',
appId: '1:427332280600:web:f7a25537ccd5a7bd0c7e6d',
messagingSenderId: '427332280600',
projectId: 'test2-8a3d2',
authDomain: 'test2-8a3d2.firebaseapp.com',
databaseURL: 'https://test2-8a3d2-default-rtdb.firebaseio.com',
storageBucket: 'test2-8a3d2.firebasestorage.app',
measurementId: 'G-4LFVXEXWKY',
);
}

View File

@ -1,77 +0,0 @@
// File generated by FlutterFire CLI.
// ignore_for_file: type=lint
import 'package:firebase_core/firebase_core.dart' show FirebaseOptions;
import 'package:flutter/foundation.dart' show defaultTargetPlatform, kIsWeb, TargetPlatform;
/// Default [FirebaseOptions] for use with your Firebase apps.
///
/// Example:
/// ```dart
/// import 'firebase_options.dart';
/// // ...
/// await Firebase.initializeApp(
/// options: DefaultFirebaseOptions.currentPlatform,
/// );
/// ```
class DefaultFirebaseOptionsStaging {
static FirebaseOptions get currentPlatform {
if (kIsWeb) {
return web;
}
switch (defaultTargetPlatform) {
case TargetPlatform.android:
return android;
case TargetPlatform.iOS:
return ios;
case TargetPlatform.macOS:
throw UnsupportedError(
'DefaultFirebaseOptions have not been configured for macos - '
'you can reconfigure this by running the FlutterFire CLI again.',
);
case TargetPlatform.windows:
throw UnsupportedError(
'DefaultFirebaseOptions have not been configured for windows - '
'you can reconfigure this by running the FlutterFire CLI again.',
);
case TargetPlatform.linux:
throw UnsupportedError(
'DefaultFirebaseOptions have not been configured for linux - '
'you can reconfigure this by running the FlutterFire CLI again.',
);
default:
throw UnsupportedError(
'DefaultFirebaseOptions are not supported for this platform.',
);
}
}
static const FirebaseOptions android = FirebaseOptions(
apiKey: 'AIzaSyDP9GpYfLE8gHTj3kZ1hW8fx_FkJqOqSQk',
appId: '1:786692570726:android:0ef7079c2b978d4417b7a7',
messagingSenderId: '786692570726',
projectId: 'syncrow-staging',
databaseURL: 'https://syncrow-staging-default-rtdb.firebaseio.com',
storageBucket: 'syncrow-staging.appspot.com',
);
static const FirebaseOptions ios = FirebaseOptions(
apiKey: 'AIzaSyAWlRiuJ75FMlf2_UDdri1voWKvkaSHtRg',
appId: '1:786692570726:ios:455a6fcff77e130f17b7a7',
messagingSenderId: '786692570726',
projectId: 'syncrow-staging',
databaseURL: 'https://syncrow-staging-default-rtdb.firebaseio.com',
storageBucket: 'syncrow-staging.appspot.com',
iosBundleId: 'com.example.syncrow.app',
);
static const FirebaseOptions web = FirebaseOptions(
apiKey: 'AIzaSyDyGaQ3sZhb4meaY6sGke-YglhdhJ2is8Q',
appId: '1:786692570726:web:93c931e6701797b317b7a7',
messagingSenderId: '786692570726',
projectId: 'syncrow-staging',
authDomain: 'syncrow-staging.firebaseapp.com',
databaseURL: 'https://syncrow-staging-default-rtdb.firebaseio.com',
storageBucket: 'syncrow-staging.appspot.com',
measurementId: 'G-CZ3J3G6LMQ',
);
}

View File

@ -4,7 +4,7 @@ import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.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:go_router/go_router.dart';
import 'package:syncrow_web/firebase_options_prod.dart'; import 'package:syncrow_web/firebase_options.dart';
import 'package:syncrow_web/pages/auth/bloc/auth_bloc.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_bloc.dart';
import 'package:syncrow_web/pages/home/bloc/home_event.dart'; import 'package:syncrow_web/pages/home/bloc/home_event.dart';
@ -27,7 +27,9 @@ Future<void> main() async {
await dotenv.load(fileName: '.env.$environment'); await dotenv.load(fileName: '.env.$environment');
WidgetsFlutterBinding.ensureInitialized(); WidgetsFlutterBinding.ensureInitialized();
await Firebase.initializeApp( await Firebase.initializeApp(
options: DefaultFirebaseOptionsStaging.currentPlatform, options: DefaultFirebaseOptions(
databaseUrl: dotenv.env['RTDB_URL']!,
),
); );
initialSetup(); initialSetup();
} catch (_) {} } catch (_) {}
@ -59,7 +61,7 @@ class MyApp extends StatelessWidget {
BlocProvider<CreateRoutineBloc>( BlocProvider<CreateRoutineBloc>(
create: (context) => CreateRoutineBloc(), create: (context) => CreateRoutineBloc(),
), ),
BlocProvider(create: (context) => HomeBloc()..add(FetchUserInfo())), BlocProvider(create: (context) => HomeBloc()..add(const FetchUserInfo())),
BlocProvider<VisitorPasswordBloc>( BlocProvider<VisitorPasswordBloc>(
create: (context) => VisitorPasswordBloc(), create: (context) => VisitorPasswordBloc(),
), ),

View File

@ -4,7 +4,7 @@ import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.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:go_router/go_router.dart';
import 'package:syncrow_web/firebase_options_dev.dart'; import 'package:syncrow_web/firebase_options.dart';
import 'package:syncrow_web/pages/auth/bloc/auth_bloc.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_bloc.dart';
import 'package:syncrow_web/pages/home/bloc/home_event.dart'; import 'package:syncrow_web/pages/home/bloc/home_event.dart';
@ -27,7 +27,9 @@ Future<void> main() async {
await dotenv.load(fileName: '.env.$environment'); await dotenv.load(fileName: '.env.$environment');
WidgetsFlutterBinding.ensureInitialized(); WidgetsFlutterBinding.ensureInitialized();
await Firebase.initializeApp( await Firebase.initializeApp(
options: DefaultFirebaseOptionsDev.currentPlatform, options: DefaultFirebaseOptions(
databaseUrl: dotenv.env['RTDB_URL']!,
),
); );
initialSetup(); initialSetup();
} catch (_) {} } catch (_) {}
@ -59,7 +61,7 @@ class MyApp extends StatelessWidget {
BlocProvider<CreateRoutineBloc>( BlocProvider<CreateRoutineBloc>(
create: (context) => CreateRoutineBloc(), create: (context) => CreateRoutineBloc(),
), ),
BlocProvider(create: (context) => HomeBloc()..add(FetchUserInfo())), BlocProvider(create: (context) => HomeBloc()..add(const FetchUserInfo())),
BlocProvider<VisitorPasswordBloc>( BlocProvider<VisitorPasswordBloc>(
create: (context) => VisitorPasswordBloc(), create: (context) => VisitorPasswordBloc(),
), ),

View File

@ -4,7 +4,7 @@ import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.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:go_router/go_router.dart';
import 'package:syncrow_web/firebase_options_prod.dart'; import 'package:syncrow_web/firebase_options.dart';
import 'package:syncrow_web/pages/auth/bloc/auth_bloc.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_bloc.dart';
import 'package:syncrow_web/pages/home/bloc/home_event.dart'; import 'package:syncrow_web/pages/home/bloc/home_event.dart';
@ -24,7 +24,9 @@ Future<void> main() async {
await dotenv.load(fileName: '.env.$environment'); await dotenv.load(fileName: '.env.$environment');
WidgetsFlutterBinding.ensureInitialized(); WidgetsFlutterBinding.ensureInitialized();
await Firebase.initializeApp( await Firebase.initializeApp(
options: DefaultFirebaseOptionsStaging.currentPlatform, options: DefaultFirebaseOptions(
databaseUrl: dotenv.env['RTDB_URL']!,
),
); );
initialSetup(); initialSetup();
} catch (_) {} } catch (_) {}
@ -56,7 +58,7 @@ class MyApp extends StatelessWidget {
BlocProvider<CreateRoutineBloc>( BlocProvider<CreateRoutineBloc>(
create: (context) => CreateRoutineBloc(), create: (context) => CreateRoutineBloc(),
), ),
BlocProvider(create: (context) => HomeBloc()..add(FetchUserInfo())), BlocProvider(create: (context) => HomeBloc()..add(const FetchUserInfo())),
BlocProvider<VisitorPasswordBloc>( BlocProvider<VisitorPasswordBloc>(
create: (context) => VisitorPasswordBloc(), create: (context) => VisitorPasswordBloc(),
), ),

View File

@ -18,7 +18,7 @@ class RemoteBookableSpacesService implements BookableSystemService {
}) async { }) async {
try { try {
final response = await _httpService.get( final response = await _httpService.get(
path: ApiEndpoints.getBookableSpaces, path: ApiEndpoints.bookableSpaces,
queryParameters: { queryParameters: {
'page': param.page, 'page': param.page,
'size': param.size, 'size': param.size,

View File

@ -0,0 +1,170 @@
import 'package:dio/dio.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 String spaceId,
}) async {
try {
final response = await _httpService.get(
path: ApiEndpoints.getCalendarEvents,
queryParameters: {
'spaceId': spaceId,
},
expectedResponseModel: (json) {
return CalendarEventsResponse.fromJson(
json as Map<String, dynamic>,
);
},
);
return CalendarEventsResponse.fromJson(response 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()}');
}
}
}
class FakeRemoteCalendarService implements CalendarSystemService {
const FakeRemoteCalendarService(this._httpService, {this.useDummy = false});
final HTTPService _httpService;
final bool useDummy;
static const _defaultErrorMessage = 'Failed to load Calendar';
@override
Future<CalendarEventsResponse> getCalendarEvents({
required String spaceId,
}) async {
if (useDummy) {
final dummyJson = {
'statusCode': 200,
'message': 'Successfully fetched all bookings',
'data': [
{
'uuid': 'd4553fa6-a0c9-4f42-81c9-99a13a57bf80',
'date': '2025-07-11T10:22:00.626Z',
'startTime': '09:00:00',
'endTime': '12:00:00',
'cost': 10,
'user': {
'uuid': '784394ff-3197-4c39-9f07-48dc44920b1e',
'firstName': 'salsabeel',
'lastName': 'abuzaid',
'email': 'test@test.com',
'companyName': null
},
'space': {
'uuid': '000f4d81-43e4-4ad7-865c-0f8b04b7081e',
'spaceName': '2(1)'
}
},
{
'uuid': 'e9b27af0-b963-4d98-9657-454c4ba78561',
'date': '2025-07-11T10:22:00.626Z',
'startTime': '12:00:00',
'endTime': '13:00:00',
'cost': 10,
'user': {
'uuid': '784394ff-3197-4c39-9f07-48dc44920b1e',
'firstName': 'salsabeel',
'lastName': 'abuzaid',
'email': 'test@test.com',
'companyName': null
},
'space': {
'uuid': '000f4d81-43e4-4ad7-865c-0f8b04b7081e',
'spaceName': '2(1)'
}
},
{
'uuid': 'e9b27af0-b963-4d98-9657-454c4ba78561',
'date': '2025-07-13T10:22:00.626Z',
'startTime': '15:30:00',
'endTime': '19:00:00',
'cost': 20,
'user': {
'uuid': '784394ff-3197-4c39-9f07-48dc44920b1e',
'firstName': 'salsabeel',
'lastName': 'abuzaid',
'email': 'test@test.com',
'companyName': null
},
'space': {
'uuid': '000f4d81-43e4-4ad7-865c-0f8b04b7081e',
'spaceName': '2(1)'
}
}
],
'success': true
};
final response = CalendarEventsResponse.fromJson(dummyJson);
// Filter events by spaceId
final filteredData = response.data.where((event) {
return event.space.uuid == spaceId;
}).toList();
print('Filtering events for spaceId: $spaceId');
print('Found ${filteredData.length} matching events');
return filteredData.isNotEmpty
? CalendarEventsResponse(
statusCode: response.statusCode,
message: response.message,
data: filteredData,
success: response.success,
)
: CalendarEventsResponse(
statusCode: 404,
message: 'No events found for spaceId: $spaceId',
data: [],
success: false,
);
}
try {
final response = await _httpService.get(
path: ApiEndpoints.getCalendarEvents,
queryParameters: {
'spaceId': spaceId,
},
expectedResponseModel: (json) {
return CalendarEventsResponse.fromJson(
json as Map<String, dynamic>,
);
},
);
return CalendarEventsResponse.fromJson(response 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()}');
}
}
}

View File

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

View File

@ -0,0 +1,7 @@
import 'package:syncrow_web/pages/access_management/booking_system/domain/models/calendar_event_booking.dart';
abstract class CalendarSystemService {
Future<CalendarEventsResponse> getCalendarEvents({
required String spaceId,
});
}

View File

@ -2,13 +2,17 @@ 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/models/calendar_event_booking.dart';
import 'package:syncrow_web/pages/access_management/booking_system/domain/services/calendar_system_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<StartTimer>(_onStartTimer);
@ -22,53 +26,24 @@ class CalendarEventsBloc extends Bloc<CalendarEventsEvent, CalendarEventState> {
) async { ) async {
emit(EventsLoading()); emit(EventsLoading());
try { try {
final events = _generateDummyEventsForWeek(event.weekStart); final response = await calendarService.getCalendarEvents(
spaceId: event.spaceId,
);
final events =
response.data.map<CalendarEventData>(_toCalendarEventData).toList();
eventController.addAll(events); eventController.addAll(events);
emit(EventsLoaded( emit(EventsLoaded(events: events));
events: events,
initialDate: event.weekStart,
weekDays: _getWeekDays(event.weekStart),
));
} 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,
weekDays: loaded.weekDays,
)); ));
} }
} }
@ -86,47 +61,44 @@ 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,
weekDays: newWeekDays,
)); ));
} }
} }
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(
color: Colors.blue, localDate.year,
), localDate.month,
CalendarEventData( localDate.day,
date: now, startParts[0],
startTime: now.copyWith(hour: 9, minute: 00, second: 0), startParts[1],
endTime: now.copyWith(hour: 10, minute: 30, second: 0), );
title: 'Team Meeting',
description: 'Weekly team sync', final endTime = DateTime(
color: Colors.blue, localDate.year,
), localDate.month,
CalendarEventData( localDate.day,
date: now.add(const Duration(days: 1)), endParts[0],
startTime: now.copyWith(hour: 14, day: now.day + 1), endParts[1],
endTime: now.copyWith(hour: 15, day: now.day + 1), );
title: 'Client Call',
description: 'Project discussion', return CalendarEventData(
color: Colors.green, date: startTime,
), startTime: startTime,
CalendarEventData( endTime: endTime,
date: now.add(const Duration(days: 2)), title:
startTime: now.copyWith(hour: 11, day: now.day + 2), '${booking.space.spaceName} - ${booking.user.firstName} ${booking.user.lastName}',
endTime: now.copyWith(hour: 12, day: now.day + 2), description: 'Cost: ${booking.cost}',
title: 'Lunch with Team', color: Colors.blue,
color: Colors.orange, event: booking,
), );
];
} }
List<DateTime> _getWeekDays(DateTime date) { List<DateTime> _getWeekDays(DateTime date) {

View File

@ -6,13 +6,20 @@ abstract class CalendarEventsEvent {
} }
class LoadEvents extends CalendarEventsEvent { class LoadEvents extends CalendarEventsEvent {
final String spaceId;
final DateTime weekStart; final DateTime weekStart;
const LoadEvents({required this.weekStart}); final DateTime weekEnd;
const LoadEvents({
required this.spaceId,
required this.weekStart,
required this.weekEnd,
});
} }
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 +30,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);
}

View File

@ -9,13 +9,9 @@ class EventsLoading extends CalendarEventState {}
class EventsLoaded extends CalendarEventState { class EventsLoaded extends CalendarEventState {
final List<CalendarEventData> events; final List<CalendarEventData> events;
final DateTime initialDate;
final List<DateTime> weekDays;
EventsLoaded({ EventsLoaded({
required this.events, required this.events,
required this.initialDate,
required this.weekDays,
}); });
} }

View File

@ -2,11 +2,12 @@ import 'package:bloc/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 'date_selection_state.dart'; import 'date_selection_state.dart';
class DateSelectionBloc extends Bloc<DateSelectionEvent, DateSelectionState> { class DateSelectionBloc extends Bloc<DateSelectionEvent, DateSelectionState> {
DateSelectionBloc() : super(DateSelectionState.initial()) { DateSelectionBloc() : super(DateSelectionState.initial()) {
on<SelectDate>((event, emit) { on<SelectDate>((event, emit) {
final newWeekStart = _getStartOfWeek(event.selectedDate); final newWeekStart = _getStartOfWeek(event.selectedDate);
emit(DateSelectionState( emit(state.copyWith(
selectedDate: event.selectedDate, selectedDate: event.selectedDate,
weekStart: newWeekStart, weekStart: newWeekStart,
)); ));
@ -14,19 +15,21 @@ class DateSelectionBloc extends Bloc<DateSelectionEvent, DateSelectionState> {
on<NextWeek>((event, emit) { on<NextWeek>((event, emit) {
final newWeekStart = state.weekStart.add(const Duration(days: 7)); final newWeekStart = state.weekStart.add(const Duration(days: 7));
final inNewWeek = state.selectedDate emit(state.copyWith(
.isAfter(newWeekStart.subtract(const Duration(days: 1))) &&
state.selectedDate
.isBefore(newWeekStart.add(const Duration(days: 7)));
emit(DateSelectionState(
selectedDate: state.selectedDate,
weekStart: newWeekStart, weekStart: newWeekStart,
)); ));
}); });
on<PreviousWeek>((event, emit) { on<PreviousWeek>((event, emit) {
emit(DateSelectionState( final newWeekStart = state.weekStart.subtract(const Duration(days: 7));
selectedDate: state.selectedDate!.subtract(const Duration(days: 7)), emit(state.copyWith(
weekStart: state.weekStart.subtract(const Duration(days: 7)), weekStart: newWeekStart,
));
});
on<SelectDateFromSidebarCalendar>((event, emit) {
emit(state.copyWith(
selectedDateFromSideBarCalender: event.selectedDate,
)); ));
}); });
} }

View File

@ -10,4 +10,9 @@ class SelectDate extends DateSelectionEvent {
class NextWeek extends DateSelectionEvent {} class NextWeek extends DateSelectionEvent {}
class PreviousWeek extends DateSelectionEvent {} class PreviousWeek extends DateSelectionEvent {}
class SelectDateFromSidebarCalendar extends DateSelectionEvent {
final DateTime selectedDate;
SelectDateFromSidebarCalendar(this.selectedDate);
}

View File

@ -1,21 +1,34 @@
class DateSelectionState { class DateSelectionState {
final DateTime selectedDate; final DateTime selectedDate;
final DateTime weekStart; final DateTime weekStart;
final DateTime? selectedDateFromSideBarCalender;
const DateSelectionState({ DateSelectionState({
required this.selectedDate, required this.selectedDate,
required this.weekStart, required this.weekStart,
this.selectedDateFromSideBarCalender,
}); });
factory DateSelectionState.initial() { factory DateSelectionState.initial() {
final now = DateTime.now(); final now = DateTime.now();
final weekStart = now.subtract(Duration(days: now.weekday - 1));
return DateSelectionState( return DateSelectionState(
selectedDate: now, selectedDate: now,
weekStart: _getStartOfWeek(now), weekStart: weekStart,
selectedDateFromSideBarCalender: null,
); );
} }
static DateTime _getStartOfWeek(DateTime date) { DateSelectionState copyWith({
return date.subtract(Duration(days: date.weekday - 1)); DateTime? selectedDate,
DateTime? weekStart,
DateTime? selectedDateFromSideBarCalender,
}) {
return DateSelectionState(
selectedDate: selectedDate ?? this.selectedDate,
weekStart: weekStart ?? this.weekStart,
selectedDateFromSideBarCalender: selectedDateFromSideBarCalender ?? this.selectedDateFromSideBarCalender,
);
} }
} }

View File

@ -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: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/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';
@ -9,7 +10,9 @@ import 'package:syncrow_web/pages/access_management/booking_system/presentation/
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 +38,20 @@ 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(
spaceId: selectedRoom.uuid,
weekStart: dateState.weekStart,
weekEnd: dateState.weekStart.add(const Duration(days: 6)),
),
);
}
} }
@override @override
@ -70,190 +60,181 @@ class _BookingPageState extends State<BookingPage> {
providers: [ providers: [
BlocProvider(create: (_) => SelectedBookableSpaceBloc()), BlocProvider(create: (_) => SelectedBookableSpaceBloc()),
BlocProvider(create: (_) => DateSelectionBloc()), BlocProvider(create: (_) => DateSelectionBloc()),
BlocProvider(
create: (_) => CalendarEventsBloc(
calendarService:
FakeRemoteCalendarService(HTTPService(), useDummy: true),
),
),
], ],
child: BlocListener<DateSelectionBloc, DateSelectionState>( child: Builder(
listenWhen: (previous, current) => builder: (context) =>
previous.weekStart != current.weekStart, BlocListener<CalendarEventsBloc, CalendarEventState>(
listener: (context, state) { listenWhen: (prev, curr) => curr is EventsLoaded,
_loadEventsForWeek(state.weekStart); listener: (context, state) {
}, if (state is EventsLoaded) {
child: Row( _eventController.removeWhere((_) => true);
crossAxisAlignment: CrossAxisAlignment.start, _eventController.addAll(state.events);
children: [ }
Expanded( },
child: Container( child: BlocListener<SelectedBookableSpaceBloc,
decoration: BoxDecoration( SelectedBookableSpaceState>(
color: ColorsManager.whiteColors, listener: (context, state) => _dispatchLoadEvents(context),
boxShadow: [ child: BlocListener<DateSelectionBloc, DateSelectionState>(
BoxShadow( listener: (context, state) => _dispatchLoadEvents(context),
color: ColorsManager.blackColor.withOpacity(0.1), child: Row(
offset: const Offset(3, 0), crossAxisAlignment: CrossAxisAlignment.start,
blurRadius: 6, children: [
spreadRadius: 0, Expanded(
), child: Container(
], decoration: BoxDecoration(
), color: ColorsManager.whiteColors,
child: Column( boxShadow: [
children: [ BoxShadow(
Expanded( color: ColorsManager.blackColor.withOpacity(0.1),
flex: 2, offset: const Offset(3, 0),
child: BlocBuilder<SelectedBookableSpaceBloc, blurRadius: 6,
SelectedBookableSpaceState>( spreadRadius: 0,
builder: (context, state) { ),
return BookingSidebar( ],
onRoomSelected: (selectedRoom) { ),
context child: Column(
.read<SelectedBookableSpaceBloc>() children: [
.add(SelectBookableSpace(selectedRoom)); Expanded(
}, flex: 2,
); child: BlocBuilder<SelectedBookableSpaceBloc,
}, SelectedBookableSpaceState>(
builder: (context, state) {
return BookingSidebar(
onRoomSelected: (selectedRoom) {
context
.read<SelectedBookableSpaceBloc>()
.add(SelectBookableSpace(selectedRoom));
},
);
},
),
),
Expanded(
child: BlocBuilder<DateSelectionBloc,
DateSelectionState>(
builder: (context, dateState) {
return CustomCalendarPage(
selectedDate: dateState.selectedDate,
onDateChanged: (day, month, year) {
final newDate = DateTime(year, month, day);
context
.read<DateSelectionBloc>()
.add(SelectDate(newDate));
context.read<DateSelectionBloc>().add(
SelectDateFromSidebarCalendar(newDate));
},
);
},
),
),
],
), ),
), ),
Expanded( ),
child: BlocBuilder<DateSelectionBloc, DateSelectionState>( Expanded(
builder: (context, dateState) { flex: 4,
return CustomCalendarPage( child: Padding(
selectedDate: dateState.selectedDate, padding: const EdgeInsets.all(20.0),
onDateChanged: (day, month, year) { child: Column(
final newDate = DateTime(year, month, day); crossAxisAlignment: CrossAxisAlignment.start,
context children: [
.read<DateSelectionBloc>() Row(
.add(SelectDate(newDate)); mainAxisAlignment: MainAxisAlignment.spaceBetween,
}, children: [
); Row(
}, children: [
), SvgTextButton(
), svgAsset: Assets.homeIcon,
], label: 'Manage Bookable Spaces',
), onPressed: () {},
), ),
), const SizedBox(width: 20),
Expanded( SvgTextButton(
flex: 4, svgAsset: Assets.groupIcon,
child: Padding( label: 'Manage Users',
padding: const EdgeInsets.all(20.0), onPressed: () {},
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Row(
children: [
SvgTextButton(
svgAsset: Assets.homeIcon,
label: 'Manage Bookable Spaces',
onPressed: () {},
),
const SizedBox(width: 20),
SvgTextButton(
svgAsset: Assets.groupIcon,
label: 'Manage Users',
onPressed: () {},
),
],
),
BlocBuilder<DateSelectionBloc, DateSelectionState>(
builder: (context, state) {
final weekStart = state.weekStart;
final weekEnd =
weekStart.add(const Duration(days: 6));
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( BlocBuilder<DateSelectionBloc,
children: [ DateSelectionState>(
IconButton( builder: (context, state) {
iconSize: 15, final weekStart = state.weekStart;
icon: const Icon(Icons.arrow_back_ios, final weekEnd =
color: ColorsManager.lightGrayColor), weekStart.add(const Duration(days: 6));
onPressed: () { return WeekNavigation(
weekStart: weekStart,
weekEnd: weekEnd,
onPreviousWeek: () {
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(
], child: BlocBuilder<SelectedBookableSpaceBloc,
), SelectedBookableSpaceState>(
Expanded( builder: (context, roomState) {
child: BlocBuilder<SelectedBookableSpaceBloc, final selectedRoom =
SelectedBookableSpaceState>( roomState.selectedBookableSpace;
builder: (context, roomState) { return BlocBuilder<DateSelectionBloc,
final selectedRoom = roomState.selectedBookableSpace; DateSelectionState>(
return BlocBuilder<DateSelectionBloc, builder: (context, dateState) {
DateSelectionState>( return BlocListener<CalendarEventsBloc,
builder: (context, dateState) { CalendarEventState>(
return WeeklyCalendarPage( listenWhen: (prev, curr) =>
startTime: curr is EventsLoaded,
selectedRoom?.bookableConfig.startTime, listener: (context, state) {
endTime: selectedRoom?.bookableConfig.endTime, if (state is EventsLoaded) {
weekStart: dateState.weekStart, _eventController
selectedDate: dateState.selectedDate, .removeWhere((_) => true);
eventController: _eventController, _eventController.addAll(state.events);
); }
}, },
); child: WeeklyCalendarPage(
}, startTime: selectedRoom
?.bookableConfig.startTime,
endTime: selectedRoom
?.bookableConfig.endTime,
weekStart: dateState.weekStart,
selectedDate: dateState.selectedDate,
eventController: _eventController,
selectedDateFromSideBarCalender: context
.watch<DateSelectionBloc>()
.state
.selectedDateFromSideBarCalender,
),
);
},
);
},
),
),
],
), ),
), ),
], ),
), ],
), ),
), ),
], ),
), ),
), ),
); );
} }
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';
}
}
} }

View File

@ -0,0 +1,60 @@
import 'package:calendar_view/calendar_view.dart';
import 'package:flutter/material.dart';
import 'package:intl/intl.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 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(),
),
);
}
}

View File

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

View File

@ -12,10 +12,13 @@ class SvgTextButton extends StatelessWidget {
final double borderRadius; final double borderRadius;
final List<BoxShadow> boxShadow; final List<BoxShadow> boxShadow;
final double svgSize; final double svgSize;
final double? fontSize;
final FontWeight? fontWeight;
const SvgTextButton({ const SvgTextButton({
super.key, super.key,
required this.svgAsset, required this.svgAsset,
this.fontSize,
this.fontWeight,
required this.label, required this.label,
required this.onPressed, required this.onPressed,
this.backgroundColor = ColorsManager.circleRolesBackground, this.backgroundColor = ColorsManager.circleRolesBackground,

View File

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

View File

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

View File

@ -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';
}
}
}

View File

@ -1,6 +1,9 @@
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/hatched_column_background.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 {
@ -9,6 +12,7 @@ class WeeklyCalendarPage extends StatelessWidget {
final EventController eventController; final EventController eventController;
final String? startTime; final String? startTime;
final String? endTime; final String? endTime;
final DateTime? selectedDateFromSideBarCalender;
const WeeklyCalendarPage({ const WeeklyCalendarPage({
super.key, super.key,
@ -17,37 +21,81 @@ class WeeklyCalendarPage extends StatelessWidget {
required this.eventController, required this.eventController,
this.startTime, this.startTime,
this.endTime, this.endTime,
this.selectedDateFromSideBarCalender,
}); });
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final startHour = _parseHour(startTime, defaultValue: 0); final startHour = _parseHour(startTime, defaultValue: 0);
final endHour = _parseHour(endTime, defaultValue: 24); final endHour = _parseHour(endTime, defaultValue: 24);
if (endTime == null || endTime!.isEmpty) { if (endTime == null || endTime!.isEmpty) {
return const Center( return const Center(
child: Text( child: Column(
'Please select a bookable space to view the calendar.', mainAxisAlignment: MainAxisAlignment.center,
style: TextStyle(fontSize: 16, fontWeight: FontWeight.w500), children: [
Icon(
Icons.calendar_today,
color: ColorsManager.lightGrayColor,
size: 80,
),
SizedBox(height: 20),
Text(
'Please select a bookable space to view the calendar.',
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.w500,
color: ColorsManager.lightGrayColor),
),
],
), ),
); );
} }
final weekDays = _getWeekDays(weekStart); 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 int totalDays = 7;
final DateTime highlightStart = DateTime(2025, 7, 10);
final DateTime highlightEnd = DateTime(2025, 7, 19);
return LayoutBuilder( return LayoutBuilder(
builder: (context, constraints) { builder: (context, constraints) {
final double calendarWidth = constraints.maxWidth; final double calendarWidth = constraints.maxWidth;
const double timeLineWidth = 80;
const int totalDays = 7;
final double dayColumnWidth = final double dayColumnWidth =
(calendarWidth - timeLineWidth) / totalDays - 0.1; (calendarWidth - timeLineWidth) / totalDays - 0.1;
final selectedDayIndex = bool isInRange(DateTime date, DateTime start, DateTime end) {
weekDays.indexWhere((d) => isSameDay(d, selectedDate)); return !date.isBefore(start) && !date.isAfter(end);
}
return Padding( return Padding(
padding: const EdgeInsets.only(left: 25.0, right: 25.0, top: 25), padding: const EdgeInsets.only(left: 25.0, right: 25.0, top: 25),
child: Stack( child: Stack(
children: [ children: [
WeekView( WeekView(
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,
@ -64,84 +112,19 @@ class WeeklyCalendarPage extends StatelessWidget {
height: 0, height: 0,
), ),
weekDayBuilder: (date) { weekDayBuilder: (date) {
final weekDays = _getWeekDays(weekStart); return WeekDayHeader(
final selectedDayIndex = date: date,
weekDays.indexWhere((d) => isSameDay(d, selectedDate)); isSelectedDay: isSameDay(date, selectedDate),
final index = weekDays.indexWhere((d) => isSameDay(d, date));
final isSelectedDay = index == selectedDayIndex;
final isToday = isSameDay(date, DateTime.now());
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,
),
),
],
); );
}, },
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(),
weekTitleHeight: 60, weekTitleHeight: 60,
weekNumberBuilder: (firstDayOfWeek) => Padding( weekNumberBuilder: (firstDayOfWeek) => Padding(
padding: const EdgeInsets.only( padding: const EdgeInsets.only(right: 15, bottom: 10),
right: 15,
bottom: 10,
),
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.end, crossAxisAlignment: CrossAxisAlignment.end,
mainAxisAlignment: MainAxisAlignment.end, mainAxisAlignment: MainAxisAlignment.end,
@ -158,49 +141,8 @@ 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(),
),
); );
}, },
), ),
@ -219,6 +161,22 @@ class WeeklyCalendarPage extends StatelessWidget {
), ),
), ),
), ),
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,
top: 50, top: 50,

View File

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

View File

@ -0,0 +1,47 @@
import 'package:dio/dio.dart';
import 'package:syncrow_web/pages/access_management/manage_bookable_spaces/domain/models/bookable_space_model.dart';
import 'package:syncrow_web/pages/access_management/manage_bookable_spaces/domain/params/bookable_spaces_params.dart';
import 'package:syncrow_web/pages/access_management/manage_bookable_spaces/domain/service/bookable_spaces_service.dart';
import 'package:syncrow_web/pages/space_management_v2/main_module/shared/models/paginated_data_model.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 RemoteBookableSpacesService implements BookableSpacesService {
final HTTPService _httpService;
RemoteBookableSpacesService(this._httpService);
static const _defaultErrorMessage = 'Failed to load Bookable Spaces';
@override
Future<PaginatedDataModel<BookableSpacemodel>> load(
BookableSpacesParams param) async {
try {
final response = await _httpService.get(
path: ApiEndpoints.bookableSpaces,
queryParameters: {
'configured': true,
'page': param.currentPage,
},
expectedResponseModel: (json) {
final result = json as Map<String, dynamic>;
return PaginatedDataModel.fromJson(
result,
BookableSpacemodel.fromJsonList,
);
},
);
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);
}
}
}

View File

@ -0,0 +1,74 @@
import 'package:dio/dio.dart';
import 'package:syncrow_web/pages/access_management/manage_bookable_spaces/domain/models/bookable_space_model.dart';
import 'package:syncrow_web/pages/access_management/manage_bookable_spaces/domain/params/non_bookable_spaces_params.dart';
import 'package:syncrow_web/pages/access_management/manage_bookable_spaces/domain/service/non_bookable_spaces_service.dart';
import 'package:syncrow_web/pages/access_management/manage_bookable_spaces/domain/params/send_bookable_spaces_to_api_params.dart';
import 'package:syncrow_web/pages/space_management_v2/main_module/shared/models/paginated_data_model.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 RemoteNonBookableSpaces implements NonBookableSpacesService {
final HTTPService _httpService;
RemoteNonBookableSpaces(this._httpService);
static const _defaultErrorMessage = 'Failed to load Spaces';
@override
Future<PaginatedDataModel<BookableSpacemodel>> load(
NonBookableSpacesParams params) async {
try {
final response = await _httpService.get(
path: ApiEndpoints.bookableSpaces,
queryParameters: {
'configured': false,
'page': params.currentPage,
'search': params.searchedWords,
},
expectedResponseModel: (json) {
final result = json as Map<String, dynamic>;
return PaginatedDataModel.fromJson(
result,
BookableSpacemodel.fromJsonList,
);
},
);
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);
}
}
@override
Future<void> sendBookableSpacesToApi(
SendBookableSpacesToApiParams params) async {
try {
await _httpService.post(
path: ApiEndpoints.bookableSpaces,
body: params.toJson(),
expectedResponseModel: (p0) {},
);
} 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);
}
}
}

View File

@ -0,0 +1,40 @@
import 'package:dio/dio.dart';
import 'package:syncrow_web/pages/access_management/manage_bookable_spaces/domain/models/bookable_space_config.dart';
import 'package:syncrow_web/pages/access_management/manage_bookable_spaces/domain/params/update_bookable_space_param.dart';
import 'package:syncrow_web/pages/access_management/manage_bookable_spaces/domain/service/update_bookable_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';
class RemoteUpdateBookableSpaceService implements UpdateBookableSpaceService {
final HTTPService _httpService;
RemoteUpdateBookableSpaceService(this._httpService);
static const _defaultErrorMessage = 'Failed to load Bookable Spaces';
@override
Future<BookableSpaceConfig> update(
UpdateBookableSpaceParam updateParam) async {
try {
final response = await _httpService.put(
path: '${ApiEndpoints.bookableSpaces}/${updateParam.spaceUuid}',
body: updateParam.toJson(),
expectedResponseModel: (json) {
return BookableSpaceConfig.fromJson(
json['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);
}
}
}

View File

@ -0,0 +1,63 @@
import 'package:flutter/material.dart';
class BookableSpaceConfig {
String configUuid;
List<String> bookableDays;
TimeOfDay? bookingStartTime;
TimeOfDay? bookingEndTime;
int cost;
bool availability;
BookableSpaceConfig({
required this.configUuid,
required this.availability,
required this.bookableDays,
this.bookingEndTime,
this.bookingStartTime,
required this.cost,
});
factory BookableSpaceConfig.zero() => BookableSpaceConfig(
configUuid: '',
bookableDays: [],
availability: false,
cost: -1,
);
factory BookableSpaceConfig.fromJson(Map<String, dynamic> json) =>
BookableSpaceConfig(
configUuid: json['uuid'] as String,
bookableDays: (json['daysAvailable'] as List).cast<String>(),
availability: (json['active'] as bool?) ?? false,
bookingStartTime: parseTimeOfDay(json['startTime'] as String),
bookingEndTime: parseTimeOfDay(json['endTime'] as String),
cost: json['points'] as int,
);
static TimeOfDay parseTimeOfDay(String timeString) {
final parts = timeString.split(':');
final hour = int.parse(parts[0]);
final minute = int.parse(parts[1]);
return TimeOfDay(hour: hour, minute: minute);
}
bool get isValid =>
bookableDays.isNotEmpty &&
cost >= 0 &&
bookingStartTime != null &&
bookingEndTime != null;
BookableSpaceConfig copyWith({
List<String>? bookableDays,
TimeOfDay? bookingStartTime,
TimeOfDay? bookingEndTime,
int? cost,
bool? availability,
}) {
return BookableSpaceConfig(
configUuid: configUuid,
availability: availability ?? this.availability,
bookableDays: bookableDays ?? this.bookableDays,
cost: cost ?? this.cost,
bookingEndTime: bookingEndTime ?? this.bookingEndTime,
bookingStartTime: bookingStartTime ?? this.bookingStartTime,
);
}
}

View File

@ -0,0 +1,58 @@
import 'package:syncrow_web/pages/access_management/manage_bookable_spaces/domain/models/bookable_space_config.dart';
class BookableSpacemodel {
String spaceUuid;
String spaceName;
BookableSpaceConfig? spaceConfig;
String spaceVirtualAddress;
BookableSpacemodel({
required this.spaceUuid,
required this.spaceName,
this.spaceConfig,
required this.spaceVirtualAddress,
});
factory BookableSpacemodel.zero() => BookableSpacemodel(
spaceUuid: '',
spaceName: '',
spaceVirtualAddress: '',
);
factory BookableSpacemodel.fromJson(Map<String, dynamic> json) =>
BookableSpacemodel(
spaceUuid: json['uuid'] as String,
spaceName: json['spaceName'] as String,
spaceConfig: json['bookableConfig'] == null
? BookableSpaceConfig.zero()
: BookableSpaceConfig.fromJson(
json['bookableConfig'] as Map<String, dynamic>),
spaceVirtualAddress: json['virtualLocation'] as String,
);
static List<BookableSpacemodel> fromJsonList(List<dynamic> jsonList) =>
jsonList
.map(
(e) => BookableSpacemodel.fromJson(e as Map<String, dynamic>),
)
.toList();
bool get isValid =>
spaceUuid.isNotEmpty &&
spaceName.isNotEmpty &&
spaceVirtualAddress.isNotEmpty &&
spaceConfig != null &&
spaceConfig!.isValid;
BookableSpacemodel copyWith({
String? spaceUuid,
String? spaceName,
BookableSpaceConfig? spaceConfig,
String? spaceVirtualAddress,
}) {
return BookableSpacemodel(
spaceUuid: spaceUuid ?? this.spaceUuid,
spaceName: spaceName ?? this.spaceName,
spaceConfig: spaceConfig ?? this.spaceConfig,
spaceVirtualAddress: spaceVirtualAddress ?? this.spaceVirtualAddress,
);
}
}

View File

@ -0,0 +1,10 @@
class BookableSpacesParams {
int currentPage;
BookableSpacesParams({
required this.currentPage,
});
Map<String, dynamic> toJson() => {
'page': currentPage,
};
}

View File

@ -0,0 +1,12 @@
class NonBookableSpacesParams {
int currentPage;
String? searchedWords;
NonBookableSpacesParams({
required this.currentPage,
this.searchedWords,
});
Map<String, dynamic> toJson() => {
'page': currentPage,
};
}

View File

@ -0,0 +1,41 @@
import 'package:syncrow_web/pages/access_management/manage_bookable_spaces/domain/models/bookable_space_model.dart';
import 'package:syncrow_web/utils/string_utils.dart';
class SendBookableSpacesToApiParams {
List<String> spaceUuids;
List<String> daysAvailable;
String startTime;
String endTime;
int points;
SendBookableSpacesToApiParams({
required this.spaceUuids,
required this.daysAvailable,
required this.startTime,
required this.endTime,
required this.points,
});
static SendBookableSpacesToApiParams fromBookableSpacesModel(
List<BookableSpacemodel> bookableSpaces) {
return SendBookableSpacesToApiParams(
spaceUuids: bookableSpaces.map((space) => space.spaceUuid).toList(),
daysAvailable: bookableSpaces
.expand((space) => space.spaceConfig!.bookableDays)
.toSet()
.toList(),
startTime: formatTimeOfDayTo24HourString(
bookableSpaces.first.spaceConfig!.bookingStartTime!),
endTime: formatTimeOfDayTo24HourString(
bookableSpaces.first.spaceConfig!.bookingEndTime!),
points: bookableSpaces.first.spaceConfig!.cost,
);
}
Map<String, dynamic> toJson() => {
'spaceUuids': spaceUuids,
'daysAvailable': daysAvailable,
'startTime': startTime,
'endTime': endTime,
'points': points
};
}

View File

@ -0,0 +1,42 @@
import 'package:flutter/foundation.dart';
import 'package:syncrow_web/pages/access_management/manage_bookable_spaces/domain/models/bookable_space_model.dart';
import 'package:syncrow_web/utils/string_utils.dart';
class UpdateBookableSpaceParam {
String spaceUuid;
List<String>? bookableDays;
String? bookingStartTime;
String? bookingEndTime;
int? cost;
bool? availability;
UpdateBookableSpaceParam({
required this.spaceUuid,
this.bookingStartTime,
this.bookingEndTime,
this.bookableDays,
this.availability,
this.cost,
});
factory UpdateBookableSpaceParam.fromBookableModel(
BookableSpacemodel bookableSpace) {
return UpdateBookableSpaceParam(
spaceUuid: bookableSpace.spaceUuid,
availability: bookableSpace.spaceConfig!.availability,
bookableDays: bookableSpace.spaceConfig!.bookableDays,
cost: bookableSpace.spaceConfig!.cost,
bookingStartTime: formatTimeOfDayTo24HourString(
bookableSpace.spaceConfig!.bookingStartTime!),
bookingEndTime: formatTimeOfDayTo24HourString(
bookableSpace.spaceConfig!.bookingEndTime!),
);
}
Map<String, dynamic> toJson() => {
if (bookableDays != null) 'daysAvailable': bookableDays,
if (bookingStartTime != null) 'startTime': bookingStartTime,
if (bookingEndTime != null) 'endTime': bookingEndTime,
if (cost != null) 'points': cost,
if (availability != null) 'active': availability,
};
}

View File

@ -0,0 +1,8 @@
import 'package:syncrow_web/pages/access_management/manage_bookable_spaces/domain/models/bookable_space_model.dart';
import 'package:syncrow_web/pages/access_management/manage_bookable_spaces/domain/params/bookable_spaces_params.dart';
import 'package:syncrow_web/pages/space_management_v2/main_module/shared/models/paginated_data_model.dart';
abstract class BookableSpacesService {
Future<PaginatedDataModel<BookableSpacemodel>> load(
BookableSpacesParams param);
}

View File

@ -0,0 +1,10 @@
import 'package:syncrow_web/pages/access_management/manage_bookable_spaces/domain/models/bookable_space_model.dart';
import 'package:syncrow_web/pages/access_management/manage_bookable_spaces/domain/params/non_bookable_spaces_params.dart';
import 'package:syncrow_web/pages/access_management/manage_bookable_spaces/domain/params/send_bookable_spaces_to_api_params.dart';
import 'package:syncrow_web/pages/space_management_v2/main_module/shared/models/paginated_data_model.dart';
abstract class NonBookableSpacesService {
Future<PaginatedDataModel<BookableSpacemodel>> load(
NonBookableSpacesParams params);
Future<void> sendBookableSpacesToApi(SendBookableSpacesToApiParams params);
}

View File

@ -0,0 +1,6 @@
import 'package:syncrow_web/pages/access_management/manage_bookable_spaces/domain/models/bookable_space_config.dart';
import 'package:syncrow_web/pages/access_management/manage_bookable_spaces/domain/params/update_bookable_space_param.dart';
abstract class UpdateBookableSpaceService {
Future<BookableSpaceConfig> update(UpdateBookableSpaceParam updateParam);
}

View File

@ -0,0 +1,63 @@
import 'package:bloc/bloc.dart';
import 'package:equatable/equatable.dart';
import 'package:syncrow_web/pages/access_management/manage_bookable_spaces/domain/models/bookable_space_config.dart';
import 'package:syncrow_web/pages/access_management/manage_bookable_spaces/domain/models/bookable_space_model.dart';
import 'package:syncrow_web/pages/access_management/manage_bookable_spaces/domain/params/bookable_spaces_params.dart';
import 'package:syncrow_web/pages/access_management/manage_bookable_spaces/domain/service/bookable_spaces_service.dart';
import 'package:syncrow_web/pages/space_management_v2/main_module/shared/models/paginated_data_model.dart';
import 'package:syncrow_web/services/api/api_exception.dart';
part 'bookable_spaces_event.dart';
part 'bookable_spaces_state.dart';
class BookableSpacesBloc
extends Bloc<BookableSpacesEvent, BookableSpacesState> {
final BookableSpacesService bookableSpacesService;
BookableSpacesBloc(this.bookableSpacesService)
: super(BookableSpacesInitial()) {
on<LoadBookableSpacesEvent>(_onLoadBookableSpaces);
on<InsertUpdatedSpaceEvent>(_onInsertUpdatedSpaceEven);
}
Future<void> _onLoadBookableSpaces(
LoadBookableSpacesEvent event, Emitter<BookableSpacesState> emit) async {
emit(BookableSpacesLoading());
try {
final bookableSpaces = await bookableSpacesService.load(event.params);
emit(BookableSpacesLoaded(bookableSpacesList: bookableSpaces));
} on APIException catch (e) {
emit(BookableSpacesError(error: e.message));
} catch (e) {
emit(
BookableSpacesError(error: e.toString()),
);
}
}
void _onInsertUpdatedSpaceEven(
InsertUpdatedSpaceEvent event, Emitter<BookableSpacesState> emit) {
emit(InsertingUpdatedSpaceState());
if (event.bookableSpace.spaceConfig!.configUuid ==
event.updatedBookableSpaceConfig.configUuid) {
final editedBookableSpace = event.bookableSpaces.data.firstWhere(
(element) => element.spaceUuid == event.bookableSpace.spaceUuid,
);
final config = editedBookableSpace.spaceConfig!.copyWith(
availability: event.updatedBookableSpaceConfig.availability,
bookableDays: event.updatedBookableSpaceConfig.bookableDays,
bookingEndTime: event.updatedBookableSpaceConfig.bookingEndTime,
bookingStartTime: event.updatedBookableSpaceConfig.bookingStartTime,
cost: event.updatedBookableSpaceConfig.cost,
);
editedBookableSpace.spaceConfig = config;
final index = event.bookableSpaces.data.indexWhere(
(element) => element.spaceUuid == event.bookableSpace.spaceUuid,
);
event.bookableSpaces.data.removeAt(index);
event.bookableSpaces.data.insert(index, editedBookableSpace);
}
emit(BookableSpacesLoaded(bookableSpacesList: event.bookableSpaces));
}
}

View File

@ -0,0 +1,24 @@
part of 'bookable_spaces_bloc.dart';
sealed class BookableSpacesEvent extends Equatable {
const BookableSpacesEvent();
@override
List<Object> get props => [];
}
class LoadBookableSpacesEvent extends BookableSpacesEvent {
final BookableSpacesParams params;
const LoadBookableSpacesEvent(this.params);
}
class InsertUpdatedSpaceEvent extends BookableSpacesEvent {
final PaginatedDataModel<BookableSpacemodel> bookableSpaces;
final BookableSpacemodel bookableSpace;
final BookableSpaceConfig updatedBookableSpaceConfig;
const InsertUpdatedSpaceEvent({
required this.bookableSpaces,
required this.bookableSpace,
required this.updatedBookableSpaceConfig,
});
}

View File

@ -0,0 +1,28 @@
part of 'bookable_spaces_bloc.dart';
sealed class BookableSpacesState extends Equatable {
const BookableSpacesState();
@override
List<Object> get props => [];
}
final class BookableSpacesInitial extends BookableSpacesState {}
final class BookableSpacesLoading extends BookableSpacesState {}
final class BookableSpacesLoaded extends BookableSpacesState {
final PaginatedDataModel<BookableSpacemodel> bookableSpacesList;
const BookableSpacesLoaded({
required this.bookableSpacesList,
});
}
final class BookableSpacesError extends BookableSpacesState {
final String error;
const BookableSpacesError({
required this.error,
});
}
class InsertingUpdatedSpaceState extends BookableSpacesState {}

View File

@ -0,0 +1,18 @@
import 'package:bloc/bloc.dart';
import 'package:equatable/equatable.dart';
part 'toggle_points_switch_state.dart';
class TogglePointsSwitchCubit extends Cubit<TogglePointsSwitchState> {
TogglePointsSwitchCubit() : super(TogglePointsSwitchInitial());
bool switchValue = true;
void activateSwitch() {
switchValue = true;
emit(ActivatePointsSwitch());
}
void unActivateSwitch() {
switchValue = false;
emit(UnActivatePointsSwitch());
}
}

View File

@ -0,0 +1,14 @@
part of 'toggle_points_switch_cubit.dart';
sealed class TogglePointsSwitchState extends Equatable {
const TogglePointsSwitchState();
@override
List<Object> get props => [];
}
final class TogglePointsSwitchInitial extends TogglePointsSwitchState {}
class ActivatePointsSwitch extends TogglePointsSwitchState {}
class UnActivatePointsSwitch extends TogglePointsSwitchState {}

View File

@ -0,0 +1,150 @@
import 'package:bloc/bloc.dart';
import 'package:equatable/equatable.dart';
import 'package:flutter/material.dart';
import 'package:syncrow_web/pages/access_management/manage_bookable_spaces/domain/models/bookable_space_model.dart';
import 'package:syncrow_web/pages/access_management/manage_bookable_spaces/domain/params/non_bookable_spaces_params.dart';
import 'package:syncrow_web/pages/access_management/manage_bookable_spaces/domain/params/send_bookable_spaces_to_api_params.dart';
import 'package:syncrow_web/pages/access_management/manage_bookable_spaces/domain/service/non_bookable_spaces_service.dart';
import 'package:syncrow_web/pages/space_management_v2/main_module/shared/models/paginated_data_model.dart';
part 'non_bookaable_spaces_event.dart';
part 'non_bookaable_spaces_state.dart';
class NonBookableSpacesBloc
extends Bloc<NonBookableSpacesEvent, NonBookableSpacesState> {
NonBookableSpacesService nonBookableSpacesService;
List<BookableSpacemodel> selectedBookableSpaces = [];
NonBookableSpacesBloc(this.nonBookableSpacesService)
: super(NonBookableSpacesInitial()) {
on<CallInitStateEvent>(_onCallInitStateEvent);
on<LoadUnBookableSpacesEvent>(_onLoadUnBookableSpacesEvent);
on<AddToBookableSpaceEvent>(_onAddToBookableSpaceEvent);
on<RemoveFromBookableSpaceEvent>(_onRemoveFromBookableSpaceEvent);
on<SendBookableSpacesToApi>(_onSendBookableSpacesToApi);
on<CheckConfigurValidityEvent>(_onCheckConfigurValidityEvent);
on<EditModeSelected>(_onEditModeSelected);
}
TimeOfDay? get endTime =>
selectedBookableSpaces.first.spaceConfig!.bookingEndTime;
TimeOfDay? get startTime =>
selectedBookableSpaces.first.spaceConfig!.bookingStartTime;
void _onCallInitStateEvent(
CallInitStateEvent event, Emitter<NonBookableSpacesState> emit) {
emit(NonBookableSpacesInitial());
}
Future<void> _onLoadUnBookableSpacesEvent(LoadUnBookableSpacesEvent event,
Emitter<NonBookableSpacesState> emit) async {
if (state is NonBookableSpacesLoaded) {
final currState = state as NonBookableSpacesLoaded;
try {
emit(NonBookableSpacesLoading(
lastNonBookableSpaces: currState.nonBookableSpaces));
final nonBookableSpacesList = await nonBookableSpacesService.load(
event.nonBookableSpacesParams,
);
nonBookableSpacesList.data.addAll(currState.nonBookableSpaces.data);
emit(
NonBookableSpacesLoaded(
nonBookableSpaces: nonBookableSpacesList,
),
);
} catch (e) {
emit(
NonBookableSpacesError(e.toString()),
);
}
} else {
try {
emit(const NonBookableSpacesLoading());
final nonBookableSpacesList = await nonBookableSpacesService.load(
event.nonBookableSpacesParams,
);
emit(
NonBookableSpacesLoaded(nonBookableSpaces: nonBookableSpacesList),
);
} catch (e) {
emit(
NonBookableSpacesError(e.toString()),
);
}
}
}
void _onAddToBookableSpaceEvent(
AddToBookableSpaceEvent event,
Emitter<NonBookableSpacesState> emit,
) {
if (state is NonBookableSpacesLoaded) {
final currentState = state as NonBookableSpacesLoaded;
emit(AddNonBookableSpaceIntoBookableState());
final updatedSelectedSpaces =
List<BookableSpacemodel>.from(currentState.selectedBookableSpaces)
..add(event.nonBookableSpace);
selectedBookableSpaces.add(event.nonBookableSpace);
emit(
NonBookableSpacesLoaded(
nonBookableSpaces: currentState.nonBookableSpaces,
selectedBookableSpaces: updatedSelectedSpaces,
),
);
}
}
void _onRemoveFromBookableSpaceEvent(RemoveFromBookableSpaceEvent event,
Emitter<NonBookableSpacesState> emit) {
if (state is NonBookableSpacesLoaded) {
final currentState = state as NonBookableSpacesLoaded;
emit(RemoveBookableSpaceIntoNonBookableState());
if (currentState.selectedBookableSpaces.isNotEmpty) {
currentState.selectedBookableSpaces.remove(event.bookableSpace);
}
selectedBookableSpaces.remove(event.bookableSpace);
emit(
NonBookableSpacesLoaded(
nonBookableSpaces: currentState.nonBookableSpaces,
selectedBookableSpaces: currentState.selectedBookableSpaces,
),
);
}
}
Future<void> _onSendBookableSpacesToApi(SendBookableSpacesToApi event,
Emitter<NonBookableSpacesState> emit) async {
emit(const NonBookableSpacesLoading());
try {
await nonBookableSpacesService.sendBookableSpacesToApi(
SendBookableSpacesToApiParams.fromBookableSpacesModel(
selectedBookableSpaces,
),
);
add(CallInitStateEvent());
} catch (e) {
emit(
NonBookableSpacesError(e.toString()),
);
}
}
void _onCheckConfigurValidityEvent(
CheckConfigurValidityEvent event, Emitter<NonBookableSpacesState> emit) {
if (selectedBookableSpaces.first.spaceConfig!.isValid) {
emit(ValidSaveButtonState());
} else {
emit(UnValidSaveButtonState());
}
}
void _onEditModeSelected(
EditModeSelected event, Emitter<NonBookableSpacesState> emit) {
selectedBookableSpaces.clear();
selectedBookableSpaces.add(event.editingBookableSpace);
}
}

View File

@ -0,0 +1,42 @@
part of 'non_bookaable_spaces_bloc.dart';
sealed class NonBookableSpacesEvent extends Equatable {
const NonBookableSpacesEvent();
@override
List<Object> get props => [];
}
class CallInitStateEvent extends NonBookableSpacesEvent {}
class LoadUnBookableSpacesEvent extends NonBookableSpacesEvent {
final NonBookableSpacesParams nonBookableSpacesParams;
const LoadUnBookableSpacesEvent({
required this.nonBookableSpacesParams,
});
}
class AddToBookableSpaceEvent extends NonBookableSpacesEvent {
final BookableSpacemodel nonBookableSpace;
const AddToBookableSpaceEvent({
required this.nonBookableSpace,
});
}
class RemoveFromBookableSpaceEvent extends NonBookableSpacesEvent {
final BookableSpacemodel bookableSpace;
const RemoveFromBookableSpaceEvent({
required this.bookableSpace,
});
}
class SendBookableSpacesToApi extends NonBookableSpacesEvent {}
class CheckConfigurValidityEvent extends NonBookableSpacesEvent {}
class EditModeSelected extends NonBookableSpacesEvent {
final BookableSpacemodel editingBookableSpace;
const EditModeSelected({
required this.editingBookableSpace,
});
}

View File

@ -0,0 +1,39 @@
part of 'non_bookaable_spaces_bloc.dart';
sealed class NonBookableSpacesState extends Equatable {
const NonBookableSpacesState();
@override
List<Object> get props => [];
}
final class NonBookableSpacesInitial extends NonBookableSpacesState {}
class NonBookableSpacesLoading extends NonBookableSpacesState {
final PaginatedDataModel<BookableSpacemodel>? lastNonBookableSpaces;
const NonBookableSpacesLoading({
this.lastNonBookableSpaces,
});
}
class NonBookableSpacesLoaded extends NonBookableSpacesState {
final PaginatedDataModel<BookableSpacemodel> nonBookableSpaces;
final List<BookableSpacemodel> selectedBookableSpaces;
const NonBookableSpacesLoaded({
required this.nonBookableSpaces,
this.selectedBookableSpaces = const [],
});
}
class NonBookableSpacesError extends NonBookableSpacesState {
final String error;
const NonBookableSpacesError(this.error);
}
class AddNonBookableSpaceIntoBookableState extends NonBookableSpacesState {}
class RemoveBookableSpaceIntoNonBookableState extends NonBookableSpacesState {}
class ValidSaveButtonState extends NonBookableSpacesState {}
class UnValidSaveButtonState extends NonBookableSpacesState {}

View File

@ -0,0 +1,22 @@
import 'package:bloc/bloc.dart';
import 'package:equatable/equatable.dart';
part 'steps_state.dart';
class StepsCubit extends Cubit<StepsState> {
StepsCubit() : super(StepsInitial());
void initDialogValue() {
emit(StepOneState());
}
void editValueInit() {
emit(StepTwoState());
}
void goToNextStep() {
if (state is StepOneState) {
emit(StepTwoState());
}
}
}

View File

@ -0,0 +1,15 @@
part of 'steps_cubit.dart';
sealed class StepsState extends Equatable {
const StepsState();
@override
List<Object> get props => [];
}
final class StepsInitial extends StepsState {}
final class StepOneState extends StepsState {}
final class StepTwoState extends StepsState {}

View File

@ -0,0 +1,36 @@
import 'package:bloc/bloc.dart';
import 'package:equatable/equatable.dart';
import 'package:syncrow_web/pages/access_management/manage_bookable_spaces/domain/models/bookable_space_config.dart';
import 'package:syncrow_web/pages/access_management/manage_bookable_spaces/domain/params/update_bookable_space_param.dart';
import 'package:syncrow_web/pages/access_management/manage_bookable_spaces/domain/service/update_bookable_space_service.dart';
import 'package:syncrow_web/services/api/api_exception.dart';
part 'update_bookable_spaces_event.dart';
part 'update_bookable_spaces_state.dart';
class UpdateBookableSpacesBloc
extends Bloc<UpdateBookableSpaceEvent, UpdateBookableSpacesState> {
final UpdateBookableSpaceService updateBookableSpaceService;
UpdateBookableSpacesBloc(this.updateBookableSpaceService)
: super(UpdateBookableSpacesInitial()) {
on<UpdateBookableSpace>(_onUpdateBookableSpace);
}
Future<void> _onUpdateBookableSpace(UpdateBookableSpace event,
Emitter<UpdateBookableSpacesState> emit) async {
emit(UpdateBookableSpaceLoading(event.updatedParams.spaceUuid));
try {
final updatedSpace =
await updateBookableSpaceService.update(event.updatedParams);
emit(UpdateBookableSpaceSuccess(bookableSpaceConfig: updatedSpace));
event.onSuccess?.call();
} on APIException catch (e) {
emit(UpdateBookableSpaceFailure(error: e.message));
} catch (e) {
emit(
UpdateBookableSpaceFailure(error: e.toString()),
);
}
}
}

View File

@ -0,0 +1,17 @@
part of 'update_bookable_spaces_bloc.dart';
sealed class UpdateBookableSpaceEvent extends Equatable {
const UpdateBookableSpaceEvent();
@override
List<Object> get props => [];
}
class UpdateBookableSpace extends UpdateBookableSpaceEvent {
final void Function()? onSuccess;
final UpdateBookableSpaceParam updatedParams;
const UpdateBookableSpace({
required this.updatedParams,
this.onSuccess,
});
}

View File

@ -0,0 +1,30 @@
part of 'update_bookable_spaces_bloc.dart';
sealed class UpdateBookableSpacesState extends Equatable {
const UpdateBookableSpacesState();
@override
List<Object> get props => [];
}
final class UpdateBookableSpacesInitial extends UpdateBookableSpacesState {}
final class UpdateBookableSpaceLoading extends UpdateBookableSpacesState {
final String updatingSpaceUuid;
const UpdateBookableSpaceLoading(this.updatingSpaceUuid);
}
final class UpdateBookableSpaceSuccess extends UpdateBookableSpacesState {
final BookableSpaceConfig bookableSpaceConfig;
const UpdateBookableSpaceSuccess({
required this.bookableSpaceConfig,
});
}
final class UpdateBookableSpaceFailure extends UpdateBookableSpacesState {
final String error;
const UpdateBookableSpaceFailure({
required this.error,
});
}

View File

@ -0,0 +1,87 @@
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:syncrow_web/pages/access_management/manage_bookable_spaces/data/remote_bookable_spaces_service.dart';
import 'package:syncrow_web/pages/access_management/manage_bookable_spaces/data/remote_update_bookable_space_service.dart';
import 'package:syncrow_web/pages/access_management/manage_bookable_spaces/domain/params/bookable_spaces_params.dart';
import 'package:syncrow_web/pages/access_management/manage_bookable_spaces/presentation/blocs/bookable_spaces_bloc/bookable_spaces_bloc.dart';
import 'package:syncrow_web/pages/access_management/manage_bookable_spaces/presentation/blocs/update_bookable_spaces/update_bookable_spaces_bloc.dart';
import 'package:syncrow_web/pages/access_management/manage_bookable_spaces/presentation/widgets/main_manage_bookable_widgets/bottom_pagination_part_widget.dart';
import 'package:syncrow_web/pages/access_management/manage_bookable_spaces/presentation/widgets/main_manage_bookable_widgets/table_part_widget.dart';
import 'package:syncrow_web/pages/access_management/manage_bookable_spaces/presentation/widgets/main_manage_bookable_widgets/top_part_widget.dart';
import 'package:syncrow_web/services/api/http_service.dart';
class ManageBookableSpacesPage extends StatefulWidget {
final PageController pageController;
const ManageBookableSpacesPage({
super.key,
required this.pageController,
});
@override
State<ManageBookableSpacesPage> createState() =>
_ManageBookableSpacesPageState();
}
class _ManageBookableSpacesPageState extends State<ManageBookableSpacesPage> {
@override
Widget build(BuildContext context) {
return MultiBlocProvider(
providers: [
BlocProvider(
create: (context) => BookableSpacesBloc(
RemoteBookableSpacesService(HTTPService()),
)..add(
LoadBookableSpacesEvent(
BookableSpacesParams(currentPage: 1),
),
),
),
BlocProvider(
create: (context) => UpdateBookableSpacesBloc(
RemoteUpdateBookableSpaceService(HTTPService()),
),
)
],
child: ManageBookableSpacesWidget(
pageController: widget.pageController,
),
);
}
}
class ManageBookableSpacesWidget extends StatelessWidget {
final PageController pageController;
const ManageBookableSpacesWidget({
super.key,
required this.pageController,
});
@override
Widget build(BuildContext context) {
return Container(
padding: const EdgeInsets.all(20),
child: Column(
children: [
Expanded(
flex: 10, child: TopPartWidget(pageController: pageController)),
const SizedBox(
height: 10,
),
const Expanded(
flex: 85,
child: TablePartWidget(),
),
const SizedBox(
height: 5,
),
const Expanded(
flex: 5,
child: BottomPaginationPartWidget(),
),
],
),
);
}
}

View File

@ -0,0 +1,106 @@
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:syncrow_web/pages/access_management/manage_bookable_spaces/data/remote_non_bookable_spaces.dart';
import 'package:syncrow_web/pages/access_management/manage_bookable_spaces/domain/models/bookable_space_model.dart';
import 'package:syncrow_web/pages/access_management/manage_bookable_spaces/domain/params/non_bookable_spaces_params.dart';
import 'package:syncrow_web/pages/access_management/manage_bookable_spaces/presentation/blocs/non_bookable_spaces_bloc/non_bookaable_spaces_bloc.dart';
import 'package:syncrow_web/pages/access_management/manage_bookable_spaces/presentation/blocs/steps_cubit/cubit/steps_cubit.dart';
import 'package:syncrow_web/pages/access_management/manage_bookable_spaces/presentation/widgets/details_steps_widget.dart';
import 'package:syncrow_web/pages/access_management/manage_bookable_spaces/presentation/widgets/next_first_step_button.dart';
import 'package:syncrow_web/pages/access_management/manage_bookable_spaces/presentation/widgets/save_second_step_button.dart';
import 'package:syncrow_web/pages/access_management/manage_bookable_spaces/presentation/widgets/stepper_part_widget.dart';
import 'package:syncrow_web/services/api/http_service.dart';
import 'package:syncrow_web/utils/color_manager.dart';
class SetupBookableSpacesDialog extends StatelessWidget {
final TextEditingController pointsController = TextEditingController();
final BookableSpacemodel? editingBookableSpace;
SetupBookableSpacesDialog({
super.key,
this.editingBookableSpace,
});
@override
Widget build(BuildContext context) {
return MultiBlocProvider(
providers: [
BlocProvider<StepsCubit>(
create: editingBookableSpace == null
? (context) => StepsCubit()..initDialogValue()
: (context) => StepsCubit()..editValueInit(),
),
BlocProvider<NonBookableSpacesBloc>(
create: editingBookableSpace == null
? (context) => NonBookableSpacesBloc(
RemoteNonBookableSpaces(HTTPService()),
)..add(
LoadUnBookableSpacesEvent(
nonBookableSpacesParams:
NonBookableSpacesParams(currentPage: 1),
),
)
: (context) => NonBookableSpacesBloc(
RemoteNonBookableSpaces(HTTPService()),
)..add(
EditModeSelected(
editingBookableSpace: editingBookableSpace!),
),
),
],
child: AlertDialog(
backgroundColor: ColorsManager.whiteColors,
contentPadding: EdgeInsets.zero,
title: Center(
child: Text(
'Set Up a Bookable Spaces',
style: TextStyle(
fontWeight: FontWeight.w700,
color: ColorsManager.dialogBlueTitle,
fontSize: 15,
),
),
),
content: Column(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
const Divider(),
Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Expanded(
flex: 3,
child: StepperPartWidget(),
),
const SizedBox(
height: 588,
child: VerticalDivider(
thickness: 0.5,
width: 1,
),
),
Expanded(
flex: 7,
child: DetailsStepsWidget(
pointsController: pointsController,
editingBookableSpace: editingBookableSpace,
),
)
],
),
Builder(builder: (context) {
final stepsState = context.watch<StepsCubit>().state;
final nonBookableBloc = context.watch<NonBookableSpacesBloc>();
final selectedSpaces = nonBookableBloc.selectedBookableSpaces;
return stepsState is StepOneState
? NextFirstStepButton(selectedSpaces: selectedSpaces)
: SaveSecondStepButton(
selectedSpaces: selectedSpaces,
pointsController: pointsController,
isEditingMode: editingBookableSpace != null,
);
}),
],
),
),
);
}
}

View File

@ -0,0 +1,127 @@
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_svg/svg.dart';
import 'package:syncrow_web/pages/access_management/manage_bookable_spaces/domain/models/bookable_space_model.dart';
import 'package:syncrow_web/pages/access_management/manage_bookable_spaces/presentation/blocs/non_bookable_spaces_bloc/non_bookaable_spaces_bloc.dart';
import 'package:syncrow_web/pages/access_management/manage_bookable_spaces/presentation/widgets/time_picker_widget.dart';
import 'package:syncrow_web/utils/color_manager.dart';
import 'package:syncrow_web/utils/constants/assets.dart';
import 'package:syncrow_web/utils/string_utils.dart';
class BookingPeriodWidget extends StatelessWidget {
final BookableSpacemodel? editingBookableSpace;
const BookingPeriodWidget({
super.key,
this.editingBookableSpace,
});
@override
Widget build(BuildContext context) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Text(
'* ',
style: Theme.of(context)
.textTheme
.bodyMedium!
.copyWith(color: Colors.red),
),
const Text('Booking Period'),
],
),
Container(
width: 300,
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(12),
color: ColorsManager.graysColor,
),
child: Row(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
TimePickerWidget(
title: editingBookableSpace == null
? 'Start Time'
: editingBookableSpace!.spaceConfig!.bookingStartTime!
.format(context),
onTimePicked: (timePicked) {
if (timePicked == null) {
return;
}
final nonBookableBloc = context.read<NonBookableSpacesBloc>();
if (nonBookableBloc.endTime != null &&
isEndTimeAfterStartTime(
timePicked, nonBookableBloc.endTime!)) {
ScaffoldMessenger.of(context).clearSnackBars();
ScaffoldMessenger.of(context).showSnackBar(const SnackBar(
content:
Text("You can't choose start Time Before End time"),
duration: Duration(seconds: 2),
backgroundColor: ColorsManager.red,
));
throw Exception();
} else {
nonBookableBloc.selectedBookableSpaces.forEach(
(e) => e.spaceConfig!.bookingStartTime = timePicked,
);
}
},
),
const Icon(
Icons.arrow_right_alt,
color: ColorsManager.grayColor,
),
TimePickerWidget(
title: editingBookableSpace == null
? 'End Time'
: editingBookableSpace!.spaceConfig!.bookingEndTime!
.format(context),
onTimePicked: (timePicked) {
if (timePicked == null) {
return;
}
final nonBookableBloc = context.read<NonBookableSpacesBloc>();
if (nonBookableBloc.startTime != null &&
isEndTimeAfterStartTime(
nonBookableBloc.startTime!, timePicked)) {
ScaffoldMessenger.of(context).clearSnackBars();
ScaffoldMessenger.of(context).showSnackBar(const SnackBar(
content:
Text("You can't choose End Time After Start time"),
duration: Duration(seconds: 2),
backgroundColor: ColorsManager.red,
));
throw Exception();
} else {
nonBookableBloc.selectedBookableSpaces.forEach(
(e) => e.spaceConfig!.bookingEndTime = timePicked,
);
}
},
),
Container(
width: 50,
height: 32,
decoration: const BoxDecoration(
borderRadius: BorderRadius.only(
topLeft: Radius.circular(10),
bottomLeft: Radius.circular(10),
),
),
alignment: Alignment.center,
child: SvgPicture.asset(
Assets.clockIcon,
height: 15,
color: ColorsManager.blackColor.withValues(alpha: 0.4),
),
)
],
),
),
],
);
}
}

View File

@ -0,0 +1,106 @@
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:go_router/go_router.dart';
import 'package:syncrow_web/pages/access_management/manage_bookable_spaces/domain/params/bookable_spaces_params.dart';
import 'package:syncrow_web/pages/access_management/manage_bookable_spaces/presentation/blocs/bookable_spaces_bloc/bookable_spaces_bloc.dart';
import 'package:syncrow_web/pages/access_management/manage_bookable_spaces/presentation/blocs/non_bookable_spaces_bloc/non_bookaable_spaces_bloc.dart';
import 'package:syncrow_web/utils/color_manager.dart';
class ButtonsDividerBottomDialogWidget extends StatelessWidget {
final String title;
final void Function()? onNextPressed;
final void Function() onCancelPressed;
const ButtonsDividerBottomDialogWidget({
super.key,
required this.title,
required this.onNextPressed,
required this.onCancelPressed,
});
@override
Widget build(BuildContext context) {
return Column(
children: [
const Divider(
thickness: 0.5,
height: 1,
),
Row(
children: [
Expanded(
child: InkWell(
borderRadius: const BorderRadius.only(
bottomLeft: Radius.circular(26),
),
onTap: onCancelPressed,
child: Container(
height: 40,
alignment: Alignment.center,
decoration: const BoxDecoration(
border: Border(
right: BorderSide(
color: ColorsManager.grayBorder,
),
),
borderRadius: BorderRadius.only(
bottomLeft: Radius.circular(26),
),
),
child: const Text(
'Cancel',
style: TextStyle(color: ColorsManager.blackColor),
),
),
),
),
Expanded(
child:
BlocConsumer<NonBookableSpacesBloc, NonBookableSpacesState>(
listener: (context, nonBookableState) {
if (nonBookableState is NonBookableSpacesInitial) {
context.pop();
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text(
'Operation Done Successfully',
style: TextStyle(color: ColorsManager.activeGreen),
),
duration: Duration(seconds: 2),
behavior: SnackBarBehavior.floating,
),
);
context.read<BookableSpacesBloc>().add(
LoadBookableSpacesEvent(
BookableSpacesParams(currentPage: 1),
),
);
} else if (nonBookableState is NonBookableSpacesError) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(
nonBookableState.error,
style:
const TextStyle(color: ColorsManager.activeGreen),
),
duration: const Duration(seconds: 2),
behavior: SnackBarBehavior.floating,
),
);
}
},
builder: (context, nonBookableState) {
return TextButton(
onPressed: onNextPressed,
child: Text(
title,
),
);
},
),
)
],
)
],
);
}
}

View File

@ -0,0 +1,48 @@
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:syncrow_web/pages/access_management/manage_bookable_spaces/domain/models/bookable_space_model.dart';
import 'package:syncrow_web/pages/access_management/manage_bookable_spaces/presentation/blocs/non_bookable_spaces_bloc/non_bookaable_spaces_bloc.dart';
class CheckBoxSpaceWidget extends StatelessWidget {
final BookableSpacemodel nonBookableSpace;
final List<BookableSpacemodel> selectedSpaces;
const CheckBoxSpaceWidget({
super.key,
required this.nonBookableSpace,
required this.selectedSpaces,
});
@override
Widget build(BuildContext context) {
final isChecked = selectedSpaces.any(
(element) => element.spaceUuid == nonBookableSpace.spaceUuid,
);
return Row(
children: [
Checkbox(
value: isChecked,
onChanged: (value) {
final bloc = context.read<NonBookableSpacesBloc>();
if (value ?? false) {
bloc.add(
AddToBookableSpaceEvent(
nonBookableSpace: nonBookableSpace,
),
);
} else {
bloc.add(
RemoveFromBookableSpaceEvent(
bookableSpace: nonBookableSpace,
),
);
}
},
),
const SizedBox(width: 5),
Expanded(child: Text(nonBookableSpace.spaceName)),
],
);
}
}

View File

@ -0,0 +1,40 @@
import 'package:flutter/material.dart';
import 'package:syncrow_web/utils/color_manager.dart';
class ColumnTitleWidget extends StatelessWidget {
final bool isFirst;
final bool isLast;
final String title;
const ColumnTitleWidget({
super.key,
required this.title,
required this.isFirst,
required this.isLast,
});
@override
Widget build(BuildContext context) {
return Container(
alignment: Alignment.centerLeft,
padding: const EdgeInsets.only(left: 10),
decoration: BoxDecoration(
color: ColorsManager.graysColor,
borderRadius: isFirst
? const BorderRadius.only(
topLeft: Radius.circular(12),
)
: isLast
? const BorderRadius.only(
topRight: Radius.circular(12),
)
: null,
),
child: Text(
title,
style: const TextStyle(
color: ColorsManager.grayColor,
fontSize: 12,
),
));
}
}

View File

@ -0,0 +1,55 @@
import 'package:data_table_2/data_table_2.dart';
import 'package:flutter/material.dart';
import 'package:flutter_svg/flutter_svg.dart';
import 'package:syncrow_web/pages/access_management/manage_bookable_spaces/presentation/widgets/column_title_widget.dart';
import 'package:syncrow_web/utils/color_manager.dart';
import 'package:syncrow_web/utils/constants/assets.dart';
class CustomDataTable<T> extends StatelessWidget {
final List<String> columnsTitles;
final List<DataCell> Function(T item) cellsWidgets;
final List<T> items;
const CustomDataTable({
super.key,
required this.items,
required this.cellsWidgets,
required this.columnsTitles,
});
@override
Widget build(BuildContext context) {
return DataTable2(
dividerThickness: 0.5,
columnSpacing: 2,
horizontalMargin: 0,
empty: SvgPicture.asset(Assets.emptyDataTable),
decoration: BoxDecoration(
color: ColorsManager.circleRolesBackground,
borderRadius: BorderRadius.circular(12),
boxShadow: const [
BoxShadow(
color: ColorsManager.textGray,
blurRadius: 12,
offset: Offset(0, 4),
),
],
),
columns: columnsTitles.asMap().entries.map((entry) {
final index = entry.key;
final title = entry.value;
return DataColumn(
label: ColumnTitleWidget(
title: title,
isFirst: index == 0,
isLast: index == columnsTitles.length - 1,
),
);
}).toList(),
rows: items.map((item) {
return DataRow(cells: cellsWidgets(item));
}).toList(),
);
}
}

View File

@ -0,0 +1,37 @@
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:syncrow_web/pages/access_management/manage_bookable_spaces/domain/models/bookable_space_model.dart';
import 'package:syncrow_web/pages/access_management/manage_bookable_spaces/presentation/blocs/steps_cubit/cubit/steps_cubit.dart';
import 'package:syncrow_web/pages/access_management/manage_bookable_spaces/presentation/widgets/space_step_part_widget.dart';
import 'package:syncrow_web/pages/access_management/manage_bookable_spaces/presentation/widgets/step_two_details_widget.dart';
class DetailsStepsWidget extends StatelessWidget {
final TextEditingController pointsController;
final BookableSpacemodel? editingBookableSpace;
const DetailsStepsWidget({
super.key,
required this.pointsController,
this.editingBookableSpace,
});
@override
Widget build(BuildContext context) {
return Container(
padding: const EdgeInsets.symmetric(horizontal: 30, vertical: 20),
child: BlocBuilder<StepsCubit, StepsState>(
builder: (context, state) {
if (state is StepOneState) {
return const SpacesStepDetailsWidget();
} else if (state is StepTwoState) {
return StepTwoDetailsWidget(
pointsController: pointsController,
editingBookableSpace:editingBookableSpace
);
} else {
return const SizedBox();
}
},
),
);
}
}

View File

@ -0,0 +1,158 @@
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:syncrow_web/pages/access_management/manage_bookable_spaces/domain/params/bookable_spaces_params.dart';
import 'package:syncrow_web/pages/access_management/manage_bookable_spaces/presentation/blocs/bookable_spaces_bloc/bookable_spaces_bloc.dart';
import 'package:syncrow_web/utils/color_manager.dart';
class BottomPaginationPartWidget extends StatelessWidget {
const BottomPaginationPartWidget({super.key});
@override
Widget build(BuildContext context) {
return BlocBuilder<BookableSpacesBloc, BookableSpacesState>(
builder: (context, state) {
if (state is BookableSpacesLoaded) {
final totalPages = state.bookableSpacesList.totalPages;
final currentPage = state.bookableSpacesList.page;
List<Widget> paginationItems = [];
// « Two pages back
if (currentPage > 2) {
paginationItems.add(
_buildArrowButton(
label: '«',
onTap: () {
context.read<BookableSpacesBloc>().add(
LoadBookableSpacesEvent(
BookableSpacesParams(currentPage: currentPage - 2),
),
);
},
),
);
}
// < One page back
if (currentPage > 1) {
paginationItems.add(
_buildArrowButton(
label: '<',
onTap: () {
context.read<BookableSpacesBloc>().add(
LoadBookableSpacesEvent(
BookableSpacesParams(currentPage: currentPage - 1),
),
);
},
),
);
}
// Page numbers
for (int i = 1; i <= totalPages; i++) {
paginationItems.add(
Padding(
padding: const EdgeInsets.symmetric(horizontal: 4),
child: GestureDetector(
onTap: () {
if (i != currentPage) {
context.read<BookableSpacesBloc>().add(
LoadBookableSpacesEvent(
BookableSpacesParams(currentPage: i),
),
);
}
},
child: Container(
width: 30,
height: 30,
alignment: Alignment.center,
decoration: BoxDecoration(
color: i == currentPage
? ColorsManager.dialogBlueTitle
: Colors.grey[300],
borderRadius: BorderRadius.circular(8),
),
child: Text(
'$i',
style: TextStyle(
color: i == currentPage ? Colors.white : Colors.black,
fontWeight: i == currentPage
? FontWeight.bold
: FontWeight.normal,
),
),
),
),
),
);
}
// > One page forward
if (currentPage < totalPages) {
paginationItems.add(
_buildArrowButton(
label: '>',
onTap: () {
context.read<BookableSpacesBloc>().add(
LoadBookableSpacesEvent(
BookableSpacesParams(currentPage: currentPage + 1),
),
);
},
),
);
}
// » Two pages forward
if (currentPage + 1 < totalPages) {
paginationItems.add(
_buildArrowButton(
label: '»',
onTap: () {
context.read<BookableSpacesBloc>().add(
LoadBookableSpacesEvent(
BookableSpacesParams(currentPage: currentPage + 2),
),
);
},
),
);
}
return Row(
mainAxisAlignment: MainAxisAlignment.end,
children: paginationItems,
);
} else {
return const SizedBox.shrink();
}
},
);
}
Widget _buildArrowButton(
{required String label, required VoidCallback onTap}) {
return Padding(
padding: const EdgeInsets.symmetric(horizontal: 4),
child: GestureDetector(
onTap: onTap,
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
decoration: BoxDecoration(
color: Colors.grey[300],
borderRadius: BorderRadius.circular(8),
),
child: Text(
label,
style: const TextStyle(
color: Colors.black,
fontWeight: FontWeight.bold,
),
),
),
),
);
}
}

View File

@ -0,0 +1,201 @@
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_svg/svg.dart';
import 'package:syncrow_web/pages/access_management/manage_bookable_spaces/data/remote_update_bookable_space_service.dart';
import 'package:syncrow_web/pages/access_management/manage_bookable_spaces/domain/models/bookable_space_model.dart';
import 'package:syncrow_web/pages/access_management/manage_bookable_spaces/domain/params/bookable_spaces_params.dart';
import 'package:syncrow_web/pages/access_management/manage_bookable_spaces/domain/params/update_bookable_space_param.dart';
import 'package:syncrow_web/pages/access_management/manage_bookable_spaces/presentation/blocs/bookable_spaces_bloc/bookable_spaces_bloc.dart';
import 'package:syncrow_web/pages/access_management/manage_bookable_spaces/presentation/blocs/update_bookable_spaces/update_bookable_spaces_bloc.dart';
import 'package:syncrow_web/pages/access_management/manage_bookable_spaces/presentation/screens/setup_bookable_spaces_dialog.dart';
import 'package:syncrow_web/pages/access_management/manage_bookable_spaces/presentation/widgets/custom_data_table.dart';
import 'package:syncrow_web/services/api/http_service.dart';
import 'package:syncrow_web/utils/color_manager.dart';
import 'package:syncrow_web/utils/constants/assets.dart';
class TablePartWidget extends StatelessWidget {
const TablePartWidget({
super.key,
});
@override
Widget build(BuildContext context) {
return BlocBuilder<BookableSpacesBloc, BookableSpacesState>(
builder: (context, state) {
if (state is BookableSpacesLoading) {
return const Center(child: CircularProgressIndicator());
} else if (state is BookableSpacesError) {
return Column(mainAxisAlignment: MainAxisAlignment.center, children: [
Text(state.error),
const SizedBox(
height: 5,
),
ElevatedButton(
onPressed: () => context
.read<BookableSpacesBloc>()
.add(LoadBookableSpacesEvent(
BookableSpacesParams(currentPage: 1),
)),
child: const Text('try Again'))
]);
} else if (state is BookableSpacesLoaded) {
return CustomDataTable<BookableSpacemodel>(
items: state.bookableSpacesList.data,
cellsWidgets: (space) => [
DataCell(
Padding(
padding: const EdgeInsetsGeometry.only(left: 10),
child: Text(
space.spaceName,
style: const TextStyle(fontSize: 11),
)),
),
DataCell(Padding(
padding: const EdgeInsetsGeometry.only(left: 10),
child: Text(
space.spaceVirtualAddress,
style: const TextStyle(fontSize: 11),
))),
DataCell(Container(
padding: const EdgeInsetsGeometry.only(left: 10),
width: 200,
child: Wrap(
spacing: 4,
children: space.spaceConfig!.bookableDays
.map((day) => Text(
day,
style: const TextStyle(fontSize: 11),
))
.toList(),
),
)),
DataCell(
Padding(
padding: const EdgeInsetsGeometry.only(left: 10),
child: Text(
space.spaceConfig!.bookingStartTime!.format(context),
style: const TextStyle(fontSize: 11),
),
),
),
DataCell(
Padding(
padding: const EdgeInsetsGeometry.only(left: 10),
child: Text(
space.spaceConfig!.bookingEndTime!.format(context),
style: const TextStyle(fontSize: 11),
),
),
),
DataCell(Padding(
padding: const EdgeInsetsGeometry.only(left: 10),
child: Text(
'${space.spaceConfig!.cost} Points',
style: const TextStyle(fontSize: 11),
))),
DataCell(Center(
child: Transform.scale(
scale: 0.7,
child: BlocConsumer<UpdateBookableSpacesBloc,
UpdateBookableSpacesState>(
listener: (context, updateState) {
if (updateState is UpdateBookableSpaceSuccess) {
context.read<BookableSpacesBloc>().add(
InsertUpdatedSpaceEvent(
bookableSpaces: state.bookableSpacesList,
bookableSpace: space,
updatedBookableSpaceConfig:
updateState.bookableSpaceConfig,
),
);
}
},
builder: (context, updateState) {
final isLoading =
updateState is UpdateBookableSpaceLoading &&
updateState.updatingSpaceUuid == space.spaceUuid;
if (isLoading) {
return const Center(child: CircularProgressIndicator());
}
return Switch(
trackOutlineColor:
WidgetStateProperty.resolveWith<Color>(
(Set<WidgetState> states) {
return ColorsManager.whiteColors;
}),
value: space.spaceConfig!.availability,
activeTrackColor: ColorsManager.blueColor,
inactiveTrackColor: ColorsManager.grayBorder,
thumbColor: WidgetStateProperty.resolveWith<Color>(
(Set<WidgetState> states) {
return ColorsManager.whiteColors;
}),
onChanged: (value) {
context.read<UpdateBookableSpacesBloc>().add(
UpdateBookableSpace(
updatedParams: UpdateBookableSpaceParam(
spaceUuid: space.spaceUuid,
availability: value,
)),
);
},
);
},
),
),
)),
DataCell(Center(
child: ElevatedButton(
onPressed: () {
final bookableBloc = context.read<BookableSpacesBloc>();
showDialog(
context: context,
builder: (context) => MultiBlocProvider(
providers: [
BlocProvider.value(
value: bookableBloc,
),
BlocProvider(
create: (context) => UpdateBookableSpacesBloc(
RemoteUpdateBookableSpaceService(HTTPService()),
),
),
],
child: SetupBookableSpacesDialog(
editingBookableSpace: space,
),
),
);
},
style: ElevatedButton.styleFrom(
padding: EdgeInsets.zero,
fixedSize: const Size(50, 30),
elevation: 1,
),
child: SvgPicture.asset(
Assets.settings,
height: 15,
color: ColorsManager.blue1,
),
),
)),
],
columnsTitles: const [
'Space',
'Space Virtual Address',
'Bookable Days',
'Booking Start Time',
'Booking End Time',
'Cost',
'Availability',
'Settings',
],
);
} else {
return const SizedBox();
}
},
);
}
}

View File

@ -0,0 +1,75 @@
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_svg/svg.dart';
import 'package:syncrow_web/pages/access_management/booking_system/presentation/view/widgets/icon_text_button.dart';
import 'package:syncrow_web/pages/access_management/manage_bookable_spaces/presentation/blocs/bookable_spaces_bloc/bookable_spaces_bloc.dart';
import 'package:syncrow_web/pages/access_management/manage_bookable_spaces/presentation/screens/setup_bookable_spaces_dialog.dart';
import 'package:syncrow_web/utils/color_manager.dart';
import 'package:syncrow_web/utils/constants/assets.dart';
class TopPartWidget extends StatelessWidget {
const TopPartWidget({
super.key,
required this.pageController,
});
final PageController pageController;
@override
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsetsGeometry.symmetric(vertical: 5),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Row(
children: [
ElevatedButton(
style: ElevatedButton.styleFrom(
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
),
),
child: SvgPicture.asset(
Assets.backButtonIcon,
height: 15,
),
onPressed: () {
pageController.jumpToPage(1);
}),
const SizedBox(
width: 10,
),
Text(
'Manage Bookable Spaces',
style: TextStyle(
fontSize: 18,
color: ColorsManager.vividBlue.withValues(
alpha: 0.7,
),
fontWeight: FontWeight.w700),
)
],
),
SvgTextButton(
svgSize: 15,
fontSize: 10,
fontWeight: FontWeight.bold,
svgAsset: Assets.addButtonIcon,
label: 'Set Up a Bookable Spaces',
onPressed: () {
final bloc = context.read<BookableSpacesBloc>();
showDialog(
context: context,
builder: (context) => BlocProvider.value(
value: bloc,
child: SetupBookableSpacesDialog(),
),
);
},
)
],
),
);
}
}

View File

@ -0,0 +1,32 @@
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:go_router/go_router.dart';
import 'package:syncrow_web/pages/access_management/manage_bookable_spaces/domain/models/bookable_space_model.dart';
import 'package:syncrow_web/pages/access_management/manage_bookable_spaces/presentation/blocs/non_bookable_spaces_bloc/non_bookaable_spaces_bloc.dart';
import 'package:syncrow_web/pages/access_management/manage_bookable_spaces/presentation/blocs/steps_cubit/cubit/steps_cubit.dart';
import 'package:syncrow_web/pages/access_management/manage_bookable_spaces/presentation/widgets/buttons_divider_bottom_dialog_widget.dart';
class NextFirstStepButton extends StatelessWidget {
final List<BookableSpacemodel> selectedSpaces;
const NextFirstStepButton({
super.key,
required this.selectedSpaces,
});
@override
Widget build(BuildContext context) {
return ButtonsDividerBottomDialogWidget(
title: 'Next',
onNextPressed: selectedSpaces.isEmpty
? null
: () {
context.read<StepsCubit>().goToNextStep();
context
.read<NonBookableSpacesBloc>()
.add(CheckConfigurValidityEvent());
},
onCancelPressed: () => context.pop(),
);
}
}

View File

@ -0,0 +1,142 @@
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:syncrow_web/pages/access_management/manage_bookable_spaces/domain/models/bookable_space_model.dart';
import 'package:syncrow_web/pages/access_management/manage_bookable_spaces/presentation/blocs/cubit/toggle_points_switch_cubit.dart';
import 'package:syncrow_web/pages/access_management/manage_bookable_spaces/presentation/blocs/non_bookable_spaces_bloc/non_bookaable_spaces_bloc.dart';
import 'package:syncrow_web/pages/access_management/manage_bookable_spaces/presentation/widgets/search_unbookable_spaces_widget.dart';
import 'package:syncrow_web/utils/color_manager.dart';
class PointsPartWidget extends StatefulWidget {
final BookableSpacemodel? editingBookableSpace;
const PointsPartWidget({
super.key,
required this.pointsController,
this.editingBookableSpace,
});
final TextEditingController pointsController;
@override
State<PointsPartWidget> createState() => _PointsPartWidgetState();
}
class _PointsPartWidgetState extends State<PointsPartWidget> {
@override
void initState() {
if (widget.editingBookableSpace != null) {
widget.pointsController.text =
widget.editingBookableSpace!.spaceConfig!.cost.toString();
}
super.initState();
}
@override
Widget build(BuildContext context) {
return BlocBuilder<TogglePointsSwitchCubit, TogglePointsSwitchState>(
builder: (context, state) {
return Column(
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.start,
children: [
if (state is ActivatePointsSwitch)
Text(
'* ',
style: Theme.of(context)
.textTheme
.bodyMedium!
.copyWith(color: Colors.red),
)
else
const SizedBox(
width: 11,
),
const Text('Points/hrs'),
],
),
Transform.scale(
scale: 0.7,
child: Switch(
trackOutlineColor: WidgetStateProperty.resolveWith<Color>(
(Set<WidgetState> states) {
return ColorsManager.whiteColors;
}),
activeTrackColor: ColorsManager.blueColor,
inactiveTrackColor: ColorsManager.grayBorder,
thumbColor: WidgetStateProperty.resolveWith<Color>(
(Set<WidgetState> states) {
return ColorsManager.whiteColors;
}),
value: context.watch<TogglePointsSwitchCubit>().switchValue,
onChanged: (value) {
if (value) {
context
.read<TogglePointsSwitchCubit>()
.activateSwitch();
context
.read<NonBookableSpacesBloc>()
.selectedBookableSpaces
.forEach(
(e) => e.spaceConfig!.cost = -1,
);
context
.read<NonBookableSpacesBloc>()
.add(CheckConfigurValidityEvent());
} else {
context
.read<TogglePointsSwitchCubit>()
.unActivateSwitch();
widget.pointsController.clear();
context
.read<NonBookableSpacesBloc>()
.selectedBookableSpaces
.forEach(
(e) => e.spaceConfig!.cost = 0,
);
context
.read<NonBookableSpacesBloc>()
.add(CheckConfigurValidityEvent());
}
},
),
)
],
),
const SizedBox(
height: 5,
),
if (state is ActivatePointsSwitch)
SearchUnbookableSpacesWidget(
title: 'Ex: 0',
height: 40,
onChanged: (p0) {
context
.read<NonBookableSpacesBloc>()
.selectedBookableSpaces
.forEach(
(e) => e.spaceConfig!.cost = int.parse(
widget.pointsController.text.isEmpty
? '0'
: widget.pointsController.text,
),
);
context
.read<NonBookableSpacesBloc>()
.add(CheckConfigurValidityEvent());
},
controller: widget.pointsController,
inputFormatters: [FilteringTextInputFormatter.digitsOnly],
suffix: const SizedBox(),
)
else
const SizedBox(),
],
);
},
);
}
}

View File

@ -0,0 +1,61 @@
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:go_router/go_router.dart';
import 'package:syncrow_web/pages/access_management/manage_bookable_spaces/domain/models/bookable_space_model.dart';
import 'package:syncrow_web/pages/access_management/manage_bookable_spaces/domain/params/update_bookable_space_param.dart';
import 'package:syncrow_web/pages/access_management/manage_bookable_spaces/presentation/blocs/non_bookable_spaces_bloc/non_bookaable_spaces_bloc.dart';
import 'package:syncrow_web/pages/access_management/manage_bookable_spaces/presentation/blocs/update_bookable_spaces/update_bookable_spaces_bloc.dart';
import 'package:syncrow_web/pages/access_management/manage_bookable_spaces/presentation/widgets/buttons_divider_bottom_dialog_widget.dart';
class SaveSecondStepButton extends StatelessWidget {
final List<BookableSpacemodel> selectedSpaces;
final TextEditingController pointsController;
final bool isEditingMode;
const SaveSecondStepButton({
super.key,
required this.selectedSpaces,
required this.pointsController,
required this.isEditingMode,
});
@override
Widget build(BuildContext context) {
return BlocBuilder<NonBookableSpacesBloc, NonBookableSpacesState>(
builder: (context, state) {
return ButtonsDividerBottomDialogWidget(
title: 'Save',
onNextPressed: state is UnValidSaveButtonState
? null
: () {
if (selectedSpaces.any(
(element) => element.isValid,
)) {
isEditingMode
? callEditLogic(context)
: context.read<NonBookableSpacesBloc>().add(
SendBookableSpacesToApi(),
);
}
},
onCancelPressed: () => context.pop(),
);
},
);
}
void callEditLogic(BuildContext context) {
context.read<UpdateBookableSpacesBloc>().add(
UpdateBookableSpace(
onSuccess: () =>
context.read<NonBookableSpacesBloc>().add(CallInitStateEvent()),
updatedParams: UpdateBookableSpaceParam.fromBookableModel(
context
.read<NonBookableSpacesBloc>()
.selectedBookableSpaces
.first,
),
),
);
}
}

View File

@ -0,0 +1,60 @@
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
class SearchUnbookableSpacesWidget extends StatelessWidget {
final String title;
final Widget? suffix;
final double? height;
final double? width;
final TextEditingController? controller;
final List<TextInputFormatter>? inputFormatters;
final void Function(String)? onChanged;
const SearchUnbookableSpacesWidget({
required this.title,
this.controller,
this.onChanged,
this.suffix,
this.height,
this.width,
this.inputFormatters,
super.key,
});
@override
Widget build(BuildContext context) {
return Container(
width: width ?? 480,
height: height ?? 30,
padding: const EdgeInsets.only(top: 4),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(12),
boxShadow: const [
BoxShadow(
color: Color(0x26000000),
offset: Offset(0, 4),
blurRadius: 5,
),
],
),
child: TextField(
controller: controller,
inputFormatters: inputFormatters,
onChanged: onChanged,
decoration: InputDecoration(
contentPadding:
const EdgeInsets.symmetric(vertical: 5, horizontal: 15),
hintText: title,
hintStyle: const TextStyle(color: Colors.grey),
border: InputBorder.none,
suffixIcon:
suffix ?? const Icon(Icons.search, size: 20, color: Colors.grey),
),
style: const TextStyle(
fontSize: 14,
color: Colors.grey,
),
),
);
}
}

View File

@ -0,0 +1,181 @@
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:syncrow_web/pages/access_management/manage_bookable_spaces/domain/params/non_bookable_spaces_params.dart';
import 'package:syncrow_web/pages/access_management/manage_bookable_spaces/presentation/blocs/non_bookable_spaces_bloc/non_bookaable_spaces_bloc.dart';
import 'package:syncrow_web/pages/access_management/manage_bookable_spaces/presentation/widgets/search_unbookable_spaces_widget.dart';
import 'package:syncrow_web/pages/access_management/manage_bookable_spaces/presentation/widgets/unbookable_list_widget.dart';
import 'package:syncrow_web/utils/color_manager.dart';
class SpacesStepDetailsWidget extends StatefulWidget {
const SpacesStepDetailsWidget({
super.key,
});
@override
State<SpacesStepDetailsWidget> createState() =>
_SpacesStepDetailsWidgetState();
}
class _SpacesStepDetailsWidgetState extends State<SpacesStepDetailsWidget> {
Timer? _debounce;
ScrollController scrollController = ScrollController();
int currentPage = 1;
String? currentSearchTerm;
bool isLoadingMore = false;
@override
void initState() {
super.initState();
scrollController.addListener(() {
if (scrollController.position.pixels >=
scrollController.position.maxScrollExtent - 100) {
final state = context.read<NonBookableSpacesBloc>().state;
if (state is NonBookableSpacesLoaded &&
state.nonBookableSpaces.hasNext &&
!isLoadingMore) {
isLoadingMore = true;
currentPage++;
context.read<NonBookableSpacesBloc>().add(
LoadUnBookableSpacesEvent(
nonBookableSpacesParams: NonBookableSpacesParams(
currentPage: currentPage,
searchedWords: currentSearchTerm,
),
),
);
}
}
});
}
@override
void dispose() {
scrollController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text(
'Select Space',
style: TextStyle(
fontWeight: FontWeight.w700,
color: ColorsManager.blackColor,
),
),
const SizedBox(
height: 20,
),
Container(
width: 450,
height: 480,
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(20),
boxShadow: const [
BoxShadow(
color: Color(0x40000000),
offset: Offset.zero,
blurRadius: 5,
),
],
),
child: Column(
children: [
Container(
width: 520,
height: 70,
padding:
const EdgeInsets.symmetric(vertical: 15, horizontal: 20),
decoration: const BoxDecoration(
color: Color(0xFFF8F8F8),
borderRadius: BorderRadius.vertical(
top: Radius.circular(20),
),
),
child: SearchUnbookableSpacesWidget(
title: 'Search',
onChanged: (p0) {
if (_debounce?.isActive ?? false) _debounce!.cancel();
_debounce = Timer(const Duration(milliseconds: 500), () {
currentSearchTerm = p0;
currentPage = 1;
context.read<NonBookableSpacesBloc>().add(
LoadUnBookableSpacesEvent(
nonBookableSpacesParams: NonBookableSpacesParams(
currentPage: currentPage,
searchedWords: currentSearchTerm,
),
),
);
});
},
),
),
Expanded(
child:
BlocConsumer<NonBookableSpacesBloc, NonBookableSpacesState>(
listener: (context, state) {
if (state is NonBookableSpacesLoaded) {
isLoadingMore = false;
}
},
builder: (context, state) {
if (state is NonBookableSpacesError) {
return Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(state.error),
const SizedBox(
height: 5,
),
ElevatedButton(
onPressed: () {
context.read<NonBookableSpacesBloc>().add(
LoadUnBookableSpacesEvent(
nonBookableSpacesParams:
NonBookableSpacesParams(
currentPage: currentPage,
searchedWords: currentSearchTerm,
),
),
);
},
child: const Text('Try Again'))
],
);
} else if (state is NonBookableSpacesLoading) {
if (state.lastNonBookableSpaces == null) {
return const Center(
child: CircularProgressIndicator(),
);
} else {
return UnbookableListWidget(
scrollController: scrollController,
nonBookableSpaces: state.lastNonBookableSpaces!,
);
}
} else if (state is NonBookableSpacesLoaded) {
return UnbookableListWidget(
scrollController: scrollController,
nonBookableSpaces: state.nonBookableSpaces,
);
} else {
return const SizedBox();
}
},
),
)
],
),
)
],
);
}
}

View File

@ -0,0 +1,51 @@
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:syncrow_web/pages/access_management/manage_bookable_spaces/domain/models/bookable_space_model.dart';
import 'package:syncrow_web/pages/access_management/manage_bookable_spaces/presentation/blocs/cubit/toggle_points_switch_cubit.dart';
import 'package:syncrow_web/pages/access_management/manage_bookable_spaces/presentation/widgets/booking_period_widget.dart';
import 'package:syncrow_web/pages/access_management/manage_bookable_spaces/presentation/widgets/points_part_widget.dart';
import 'package:syncrow_web/pages/access_management/manage_bookable_spaces/presentation/widgets/week_checkbox_title_widget.dart';
class StepTwoDetailsWidget extends StatelessWidget {
final TextEditingController pointsController;
final BookableSpacemodel? editingBookableSpace;
const StepTwoDetailsWidget({
super.key,
required this.pointsController,
this.editingBookableSpace,
});
@override
Widget build(BuildContext context) {
return SizedBox(
width: 450,
height: 480,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
WeekDaysCheckboxRow(
editingBookableSpace: editingBookableSpace,
),
const SizedBox(
height: 20,
),
BookingPeriodWidget(
editingBookableSpace: editingBookableSpace,
),
const SizedBox(
height: 20,
),
BlocProvider(
create: editingBookableSpace == null
? (context) => TogglePointsSwitchCubit()..activateSwitch()
: editingBookableSpace!.spaceConfig!.cost == 0
? (context) => TogglePointsSwitchCubit()..unActivateSwitch()
: (context) => TogglePointsSwitchCubit()..activateSwitch(),
child: PointsPartWidget(
pointsController: pointsController,
editingBookableSpace: editingBookableSpace),
)
],
),
);
}
}

View File

@ -0,0 +1,123 @@
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:syncrow_web/pages/access_management/manage_bookable_spaces/presentation/blocs/steps_cubit/cubit/steps_cubit.dart';
import 'package:syncrow_web/utils/color_manager.dart';
class StepperPartWidget extends StatelessWidget {
const StepperPartWidget({super.key});
@override
Widget build(BuildContext context) {
return Container(
margin: const EdgeInsets.all(20),
padding: const EdgeInsetsGeometry.only(left: 20),
child: BlocBuilder<StepsCubit, StepsState>(
builder: (context, state) {
if (state is StepOneState) {
return Column(
mainAxisAlignment: MainAxisAlignment.start,
children: [
const SizedBox(
height: 10,
),
const CircleTitleStepperWidget(
title: 'Space',
),
Container(
padding: const EdgeInsets.only(left: 3),
alignment: Alignment.centerLeft,
height: 50,
child: const VerticalDivider(
width: 8,
)),
const CircleTitleStepperWidget(
title: 'Settings',
titleColor: ColorsManager.softGray,
circleColor: ColorsManager.whiteColors,
borderColor: ColorsManager.textGray,
)
],
);
} else if (state is StepTwoState) {
return Column(
children: [
const SizedBox(
height: 10,
),
const CircleTitleStepperWidget(
title: 'Space',
titleColor: ColorsManager.softGray,
cicleIcon: Icon(
Icons.check,
color: ColorsManager.whiteColors,
size: 12,
),
circleColor: ColorsManager.trueIconGreen,
radius: 15,
borderColor: ColorsManager.trueIconGreen,
),
Container(
padding: const EdgeInsets.only(left: 3),
alignment: Alignment.centerLeft,
height: 50,
child: const VerticalDivider(
width: 8,
)),
const CircleTitleStepperWidget(
title: 'Settings',
)
],
);
} else {
return const SizedBox();
}
},
),
);
}
}
class CircleTitleStepperWidget extends StatelessWidget {
final double? radius;
final Widget? cicleIcon;
final Color? circleColor;
final Color? borderColor;
final Color? titleColor;
final String title;
const CircleTitleStepperWidget({
super.key,
required this.title,
this.circleColor,
this.borderColor,
this.cicleIcon,
this.titleColor,
this.radius,
});
@override
Widget build(BuildContext context) {
return Row(
children: [
Container(
width: radius ?? 15,
height: radius ?? 15,
decoration: BoxDecoration(
shape: BoxShape.circle,
color: circleColor ?? ColorsManager.blue1,
border: Border.all(color: borderColor ?? ColorsManager.blue1)),
child: cicleIcon,
),
const SizedBox(
width: 10,
),
Text(
title,
style: TextStyle(
fontWeight: FontWeight.w700,
color: titleColor ?? ColorsManager.blackColor,
),
),
],
);
}
}

View File

@ -0,0 +1,69 @@
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:syncrow_web/pages/access_management/manage_bookable_spaces/presentation/blocs/non_bookable_spaces_bloc/non_bookaable_spaces_bloc.dart';
import 'package:syncrow_web/utils/color_manager.dart';
class TimePickerWidget extends StatefulWidget {
final String title;
const TimePickerWidget({
super.key,
required this.onTimePicked,
required this.title,
});
final void Function(TimeOfDay? timePicked) onTimePicked;
@override
State<TimePickerWidget> createState() => _TimePickerWidgetState();
}
class _TimePickerWidgetState extends State<TimePickerWidget> {
TimeOfDay? timePicked;
@override
Widget build(BuildContext context) {
return InkWell(
borderRadius: BorderRadius.circular(10),
onTap: () async {
final tempTime = await showTimePicker(
context: context,
initialTime: TimeOfDay.now(),
builder: (context, child) {
return Theme(
data: ThemeData.light().copyWith(
colorScheme: const ColorScheme.light(
primary: ColorsManager.primaryColor,
onSurface: Colors.black,
),
),
child: child!,
);
},
);
widget.onTimePicked(tempTime);
timePicked = tempTime;
context.read<NonBookableSpacesBloc>().add(CheckConfigurValidityEvent());
setState(() {});
},
child: Container(
width: 100,
height: 32,
decoration: const BoxDecoration(
borderRadius: BorderRadius.only(
topRight: Radius.circular(10),
bottomRight: Radius.circular(10),
),
),
alignment: Alignment.centerLeft,
padding: const EdgeInsets.symmetric(horizontal: 8),
child: Text(
timePicked == null ? widget.title : timePicked!.format(context),
style: TextStyle(
color: ColorsManager.blackColor.withValues(alpha: 0.4),
fontSize: 12,
),
),
),
);
}
}

View File

@ -0,0 +1,52 @@
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:syncrow_web/pages/access_management/manage_bookable_spaces/domain/models/bookable_space_model.dart';
import 'package:syncrow_web/pages/access_management/manage_bookable_spaces/presentation/blocs/non_bookable_spaces_bloc/non_bookaable_spaces_bloc.dart';
import 'package:syncrow_web/pages/access_management/manage_bookable_spaces/presentation/widgets/check_box_space_widget.dart';
import 'package:syncrow_web/pages/space_management_v2/main_module/shared/models/paginated_data_model.dart';
class UnbookableListWidget extends StatelessWidget {
final PaginatedDataModel<BookableSpacemodel> nonBookableSpaces;
const UnbookableListWidget({
super.key,
required this.scrollController,
required this.nonBookableSpaces,
});
final ScrollController scrollController;
@override
Widget build(BuildContext context) {
return Container(
width: 490,
decoration: const BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.vertical(
bottom: Radius.circular(20),
),
),
padding: const EdgeInsets.only(top: 10, left: 20, bottom: 5),
child: ListView.separated(
separatorBuilder: (context, index) => const SizedBox(
height: 5,
),
controller: scrollController,
itemCount: nonBookableSpaces.data.length,
itemBuilder: (context, index) {
if (index < nonBookableSpaces.data.length) {
return CheckBoxSpaceWidget(
nonBookableSpace: nonBookableSpaces.data[index],
selectedSpaces:
context.read<NonBookableSpacesBloc>().selectedBookableSpaces,
);
} else {
return const Padding(
padding: EdgeInsets.symmetric(vertical: 10),
child: Center(child: CircularProgressIndicator()),
);
}
},
),
);
}
}

View File

@ -0,0 +1,84 @@
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:syncrow_web/pages/access_management/manage_bookable_spaces/domain/models/bookable_space_model.dart';
import 'package:syncrow_web/pages/access_management/manage_bookable_spaces/presentation/blocs/non_bookable_spaces_bloc/non_bookaable_spaces_bloc.dart';
class WeekDaysCheckboxRow extends StatefulWidget {
final BookableSpacemodel? editingBookableSpace;
const WeekDaysCheckboxRow({
super.key,
this.editingBookableSpace,
});
@override
State<WeekDaysCheckboxRow> createState() => _WeekDaysCheckboxRowState();
}
class _WeekDaysCheckboxRowState extends State<WeekDaysCheckboxRow> {
final Map<String, bool> _daysChecked = {
'Mon': false,
'Tue': false,
'Wed': false,
'Thu': false,
'Fri': false,
'Sat': false,
'Sun': false,
};
@override
void initState() {
super.initState();
final existingDays =
widget.editingBookableSpace?.spaceConfig?.bookableDays ?? [];
for (var day in _daysChecked.keys) {
if (existingDays.contains(day)) {
_daysChecked[day] = true;
}
}
}
@override
Widget build(BuildContext context) {
return Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: _daysChecked.entries.map((entry) {
return Expanded(
child: Row(
children: [
Expanded(
child: Checkbox(
value: entry.value,
onChanged: (newValue) {
setState(() {
_daysChecked[entry.key] = newValue ?? false;
final selectedDays = _daysChecked.entries
.where((e) => e.value)
.map((e) => e.key)
.toList();
for (var space in context
.read<NonBookableSpacesBloc>()
.selectedBookableSpaces) {
space.spaceConfig!.bookableDays = selectedDays;
}
});
context
.read<NonBookableSpacesBloc>()
.add(CheckConfigurValidityEvent());
},
),
),
Expanded(
child: Text(
entry.key,
style: const TextStyle(fontSize: 10),
)),
],
),
);
}).toList(),
);
}
}

View File

@ -2,7 +2,9 @@ import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:syncrow_web/pages/access_management/bloc/access_bloc.dart'; import 'package:syncrow_web/pages/access_management/bloc/access_bloc.dart';
import 'package:syncrow_web/pages/access_management/bloc/access_event.dart'; import 'package:syncrow_web/pages/access_management/bloc/access_event.dart';
import 'package:syncrow_web/pages/access_management/booking_system/presentation/view/booking_page.dart'; import 'package:syncrow_web/pages/access_management/booking_system/view/booking_page.dart';
import 'package:syncrow_web/pages/access_management/manage_bookable_spaces/presentation/screens/manage_bookable_spaces_screen.dart';
import 'package:syncrow_web/pages/access_management/booking_system/presentation/view/booking_page.dart' hide BookingPage;
import 'package:syncrow_web/pages/access_management/view/access_overview_content.dart'; import 'package:syncrow_web/pages/access_management/view/access_overview_content.dart';
import 'package:syncrow_web/pages/device_managment/shared/navigate_home_grid_view.dart'; import 'package:syncrow_web/pages/device_managment/shared/navigate_home_grid_view.dart';
import 'package:syncrow_web/utils/extension/build_context_x.dart'; import 'package:syncrow_web/utils/extension/build_context_x.dart';
@ -71,9 +73,14 @@ class _AccessManagementPageState extends State<AccessManagementPage>
scaffoldBody: PageView( scaffoldBody: PageView(
controller: _pageController, controller: _pageController,
physics: const NeverScrollableScrollPhysics(), physics: const NeverScrollableScrollPhysics(),
children: const [ children: [
AccessOverviewContent(), const AccessOverviewContent(),
BookingPage(), BookingPage(
pageController: _pageController,
),
ManageBookableSpacesPage(
pageController: _pageController,
),
], ],
), ),
), ),

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

View File

@ -46,15 +46,15 @@ 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()
devices.addAll(await DevicesManagementApi() .fetchDevices(projectUuid, spacesId: spacesList));
.fetchDevices(community, space, projectUuid));
}
} }
} }

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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,39 +113,27 @@ class ScheduleDialogHelper {
], ],
), ),
actions: [ actions: [
SizedBox( ScheduleModeButtons(
width: 100, onSave: () {
child: OutlinedButton( dynamic temp;
onPressed: () { if (deviceType == 'CUR_2') {
Navigator.pop(ctx, null); temp = functionOn! ? 'open' : 'close';
}, } else {
child: const Text('Cancel'), temp = functionOn;
), }
final entry = ScheduleEntry(
category: schedule?.category ?? 'switch_1',
time: _formatTimeOfDayToISO(selectedTime),
function: Status(
code: code ?? 'switch_1',
value: temp,
),
days: _convertSelectedDaysToStrings(selectedDays),
scheduleId: schedule.scheduleId,
);
Navigator.pop(ctx, entry);
},
), ),
SizedBox(
width: 100,
child: ElevatedButton(
onPressed: () {
dynamic temp;
if (deviceType == 'CUR_2') {
temp = functionOn! ? 'open' : 'close';
} else {
temp = functionOn;
}
final entry = ScheduleEntry(
category: schedule?.category ?? 'switch_1',
time: _formatTimeOfDayToISO(selectedTime),
function: Status(
code: code ?? 'switch_1',
value: temp,
),
days: _convertSelectedDaysToStrings(selectedDays),
scheduleId: schedule.scheduleId,
);
Navigator.pop(ctx, entry);
},
child: const Text('Save'),
)),
], ],
); );
}, },

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -170,45 +170,45 @@ class RoutineBloc extends Bloc<RoutineEvent, RoutineState> {
} }
} }
Future<void> _onLoadScenes( Future<void> _onLoadScenes(
LoadScenes event, Emitter<RoutineState> emit) async { LoadScenes event, Emitter<RoutineState> emit) async {
emit(state.copyWith(isLoading: true, errorMessage: null)); emit(state.copyWith(isLoading: true, errorMessage: null));
List<ScenesModel> scenes = []; List<ScenesModel> scenes = [];
try { try {
BuildContext context = NavigationService.navigatorKey.currentContext!; BuildContext context = NavigationService.navigatorKey.currentContext!;
var createRoutineBloc = context.read<CreateRoutineBloc>(); var createRoutineBloc = context.read<CreateRoutineBloc>();
final projectUuid = await ProjectManager.getProjectUUID() ?? ''; final projectUuid = await ProjectManager.getProjectUUID() ?? '';
if (createRoutineBloc.selectedSpaceId == '' && if (createRoutineBloc.selectedSpaceId == '' &&
createRoutineBloc.selectedCommunityId == '') { createRoutineBloc.selectedCommunityId == '') {
var spaceBloc = context.read<SpaceTreeBloc>(); var spaceBloc = context.read<SpaceTreeBloc>();
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) { for (var spaceId in spacesList) {
scenes.addAll( scenes.addAll(
await SceneApi.getScenes(spaceId, communityId, projectUuid)); await SceneApi.getScenes(spaceId, communityId, projectUuid));
}
} }
} else {
scenes.addAll(await SceneApi.getScenes(
createRoutineBloc.selectedSpaceId,
createRoutineBloc.selectedCommunityId,
projectUuid));
} }
} else {
scenes.addAll(await SceneApi.getScenes(
createRoutineBloc.selectedSpaceId,
createRoutineBloc.selectedCommunityId,
projectUuid));
}
emit(state.copyWith( emit(state.copyWith(
scenes: scenes, scenes: scenes,
isLoading: false,
));
} catch (e) {
emit(state.copyWith(
isLoading: false, isLoading: false,
loadScenesErrorMessage: 'Failed to load scenes', ));
errorMessage: '', } catch (e) {
loadAutomationErrorMessage: '', emit(state.copyWith(
scenes: scenes)); isLoading: false,
loadScenesErrorMessage: 'Failed to load scenes',
errorMessage: '',
loadAutomationErrorMessage: '',
scenes: scenes));
}
} }
}
Future<void> _onLoadAutomation( Future<void> _onLoadAutomation(
LoadAutomation event, Emitter<RoutineState> emit) async { LoadAutomation event, Emitter<RoutineState> emit) async {
@ -936,16 +936,15 @@ 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(communityId, spaceId, projectUuid)); .fetchDevices(projectUuid, spacesId: spacesList));
}
} }
} else { } else {
devices.addAll(await DevicesManagementApi().fetchDevices( devices.addAll(await DevicesManagementApi().fetchDevices(
createRoutineBloc.selectedCommunityId, projectUuid,
createRoutineBloc.selectedSpaceId, spacesId: [createRoutineBloc.selectedSpaceId],
projectUuid)); ));
} }
emit(state.copyWith(isLoading: false, devices: devices)); emit(state.copyWith(isLoading: false, devices: devices));

View File

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

View File

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

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