Compare commits

..

70 Commits

Author SHA1 Message Date
2681c837f5 add company name and replace it with job title 2025-07-11 12:10:53 +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
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
885ef61114 Refactor booking system: replace DebouncedBookingSystemService with DebouncedBookableSpacesService and streamline search handling 2025-07-10 12:10:28 +03:00
bfd6b5c3a0 Refactor booking system: replace BookingSystemService with BookableSystemService and update parameter handling for improved clarity 2025-07-10 11:25:35 +03:00
2b638940ae Refactor booking system: enhance parameter handling for improved clarity and maintainability 2025-07-10 11:14:30 +03:00
3e95bf4473 Refactor booking system: replace individual parameters with LoadBookableSpacesParam for improved clarity and maintainability 2025-07-10 10:56:10 +03:00
2d16bda61d Refactor SidebarBloc: streamline room data handling by using paginatedSpaces.data directly 2025-07-09 16:41:31 +03:00
5c90d5f6b9 Refactor SidebarBloc: simplify room data handling by directly using paginatedSpaces.data 2025-07-09 16:40:57 +03:00
d6a48850a7 Remove debug print statement from BookableSpacesService response handling 2025-07-09 16:25:21 +03:00
6cac94a1c4 Clean up booking system code: remove commented-out code and unnecessary variables for improved readability 2025-07-09 16:23:39 +03:00
9f28e1ccef Refactor booking system: remove unused classes, update dependencies, and implement date selection logic 2025-07-09 16:18:10 +03:00
6534bfae5b Implement-Calendar-ui 2025-07-09 09:31:55 +03:00
4cfb984d2c Sp 1720 fe draw assign tags to space dialog (#341)
<!--
  Thanks for contributing!

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

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

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

## Description

Implemented products and assign tags functionality.

## Type of Change

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

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


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

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

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

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

## Description

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

## Type of Change

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

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


[SP-1716]:
https://syncrow.atlassian.net/browse/SP-1716?atlOrigin=eyJpIjoiNWRkNTljNzYxNjVmNDY3MDlhMDU5Y2ZhYzA5YTRkZjUiLCJwIjoiZ2l0aHViLWNvbS1KU1cifQ
2025-07-07 10:20:45 +03:00
df87e41d61 Refactor AddDeviceTypeWidget and ProductTypeCard Components:
- Replaced ProductTypeCard with ProductsGrid in AddDeviceTypeWidget for improved layout and maintainability.
- Converted ProductTypeCardCounter from StatefulWidget to StatelessWidget, simplifying its implementation.
- Updated ProductTypeCard to accept count and increment/decrement callbacks, enhancing its reusability and interaction.
- Introduced ProductsGrid to manage product display in a grid format, improving UI organization and responsiveness.
2025-07-07 10:15:33 +03:00
f0bfe085a4 Enhance Product Model with Icon Mapping:
- Added icon mapping functionality to the Product model, allowing dynamic icon retrieval based on product type.
- Updated ProductTypeCard to utilize the new icon property, improving UI representation and maintainability.
2025-07-07 09:34:11 +03:00
bb846f797f Implement Tag Assignment and Device Addition Features:
- Introduced AssignTagsDialog for assigning tags to devices, enhancing user interaction and organization.
- Added AddDeviceTypeWidget for adding new device types, improving the flexibility of device management.
- Created ProductTypeCard and ProductTypeCardCounter for better representation and interaction with device types.
- Enhanced AssignTagsTable for displaying and managing product allocations, improving maintainability and user experience.
2025-07-06 16:54:15 +03:00
e234c9f3b2 Enhance SpaceDetailsActionButtons: Introduced customizable button labels for save and cancel actions, improving flexibility and user experience. Updated button implementations to utilize these new labels, enhancing maintainability and adherence to Clean Architecture principles. 2025-07-06 16:44:40 +03:00
bcd0ae4a2a Refactor Products Module:
- Introduced ProductsBloc and updated ProductsService to remove LoadProductsParam, simplifying the product loading process.
- Updated RemoteProductsService to utilize a new API endpoint for fetching products.
- Adjusted ProductsEvent and ProductsState to reflect changes in the loading mechanism, enhancing maintainability and clarity in the products management flow.
2025-07-06 16:44:26 +03:00
cebce2ce7f Update SpaceDetailsModel: Change default icon from villa to location for improved representation of space details. 2025-07-06 14:50:24 +03:00
97e3fb68bf Enhance Product Model and SpaceDetailsDevicesBox:
- Added 'productType' field to Product model for improved data representation.
- Updated JSON parsing in Product model to handle 'prodType'.
- Refactored SpaceDetailsDevicesBox to utilize productType for dynamic device icon rendering, enhancing UI clarity and maintainability.
2025-07-06 14:49:10 +03:00
46a7add90d made subspaces unique and removed duplication from BE side. 2025-07-06 13:23:50 +03:00
73de1e6ff9 Enhance EditCommunityDialog: Refactor to accept parent context and streamline community update handling. Introduced a new method _onUpdateCommunitySuccess for improved readability and maintainability. Updated SpaceManagementCommunityDialogHelper to pass the parent context for better state management in the community update flow. 2025-07-06 12:52:35 +03:00
826dea8054 Implement community update endpoint: Refactor RemoteUpdateCommunityService to dynamically construct the update URL using the project UUID. This change enhances the service's flexibility and error handling by ensuring the correct endpoint is used for community updates. 2025-07-06 12:39:54 +03:00
fdea4b1cd0 Refactor CommunityDialog: Extract error message display into a separate method _buildErrorMessage for improved readability and maintainability. This change enhances the structure of the dialog while ensuring consistent error handling. 2025-07-06 12:32:18 +03:00
823d86fd80 Add loading and success snackbar helpers: Introduced showLoadingDialog and showSuccessSnackBar methods in SpaceManagementCommunityDialogHelper for consistent loading indicators and success messages across community dialogs. Updated CreateCommunityDialog and EditCommunityDialog to utilize these new helpers, enhancing user experience and maintainability. 2025-07-06 11:17:28 +03:00
dd735032ea Update SpaceSubSpacesDialog: Replace Text with SelectableText for title and error message, and add spacing in the content column to enhance user experience and maintainability. 2025-07-06 11:01:29 +03:00
6dcc851d97 Made CommunityDialog reusable for edit and create features. 2025-07-06 10:46:20 +03:00
15b36fd052 Enhance SpaceDetailsDialog: Adjust loading indicator layout for improved responsiveness. The CircularProgressIndicator is now wrapped in a SizedBox to ensure proper sizing based on screen dimensions, enhancing user experience and maintainability. 2025-07-06 09:33:27 +03:00
a4024067c7 Sp 1708 fe implement create edit space (#339)
<!--
  Thanks for contributing!

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

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

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

## Description

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

## Type of Change

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

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


[SP-1708]:
https://syncrow.atlassian.net/browse/SP-1708?atlOrigin=eyJpIjoiNWRkNTljNzYxNjVmNDY3MDlhMDU5Y2ZhYzA5YTRkZjUiLCJwIjoiZ2l0aHViLWNvbS1KU1cifQ
2025-07-06 09:24:45 +03:00
95cded4bf5 Enhance SubSpacesInput: Introduce FocusNode for improved text field focus management. This change allows the input field to regain focus after adding a subspace, enhancing user experience and maintaining clean state management practices. 2025-07-06 09:04:13 +03:00
757a96ed9f Refactor SpaceDetailsActionButtons and SpaceIconPicker: Adjust layout properties for improved responsiveness and user experience. Set mainAxisSize to min in SpaceDetailsActionButtons and simplify the layout in SpaceIconPicker for better alignment and interaction. This enhances maintainability and adheres to Clean Architecture principles. 2025-07-03 16:15:31 +03:00
b857736e10 Refactor SpaceNameTextField: Update text styling and border handling to utilize context-based theming. This improves consistency with the app's theme and enhances maintainability by centralizing border styling logic. 2025-07-03 15:37:06 +03:00
1fccd51440 Refactor SubspaceNameDisplayWidget: Update Chip border radius for improved aesthetics and add delete functionality to remove subspaces. This enhances user interaction and aligns with maintainability principles. 2025-07-03 15:33:34 +03:00
c07ddb0ccd Refactor SpaceDetailsDevicesBox: Improve readability by extracting variables for product allocations and subspaces. This change enhances code clarity and maintainability in line with Clean Architecture principles. 2025-07-03 15:27:06 +03:00
58e99f95b2 removed comments. 2025-07-03 15:25:04 +03:00
227df6fe3d Refactor SpaceDetailsWidgets: Simplify layout and improve responsiveness in SpaceDetailsDevicesBox and SpaceSubSpacesBox. Update SpaceDetailsForm to enhance dialog width for better user experience. This refactor enhances maintainability and aligns with Clean Architecture principles. 2025-07-03 15:23:00 +03:00
9451ec0cc4 Update SpaceDetailsDialog to utilize SelectableText for error messages and ensure proper Bloc context usage. This enhances user experience by allowing text selection for easier copying of error information. 2025-07-03 13:19:42 +03:00
fc797c2646 Refactor SpaceDetailsModel and ProductAllocation: Update JSON parsing for clarity and remove unused location field. Change subspace name mapping for consistency with API response. 2025-07-03 13:19:34 +03:00
318e1d9af7 Implement Space Management Header and Action Buttons; integrate SpaceDetailsBloc for improved space management functionality. Add CommunityStructureHeader, CommunityStructureHeaderActionButtons, and CommunityStructureHeaderButton widgets to enhance UI and user interactions. Update SpaceManagementCommunityStructure to include the new header and refactor space details service for better endpoint handling. 2025-07-03 13:09:43 +03:00
d47dc349bc Enhance SubspaceNameDisplayWidget to handle duplicate names during editing. Introduced logic to check for existing subspace names and provide user feedback. Refactored state management for editing and submission processes, improving overall user experience and code clarity. 2025-07-03 12:04:03 +03:00
c221c8499f Add factory method empty to SpaceModel for default instance creation. Refactor SpaceDetailsDialog and related widgets to utilize SpaceModel, enhancing parameter handling and state management in space creation and editing flows. 2025-07-02 17:05:56 +03:00
71cf4b9feb Update LoadSpaceDetailsParam to require spaceUuid and refactor SpaceDetailsDialog to enhance clarity in parameter handling. 2025-07-02 16:30:23 +03:00
c43cf9347f Remove unnecessary deactivate method from SpaceDetailsDialog to streamline state management and improve code clarity. 2025-07-02 16:29:02 +03:00
9990b1805e Fix typo in HomeBloc: change 'Devices Management' to 'Device Management' for consistency in naming. 2025-07-02 16:27:03 +03:00
009b7c0316 Refactor SpaceDetails feature to replace LoadSpacesParam with LoadSpaceDetailsParam, enhancing clarity in parameter handling. Introduce ClearSpaceDetails event in SpaceDetailsBloc for better state management. Update SpaceDetailsDialog and SpaceDetailsForm to utilize new parameter and improve dialog functionality. 2025-07-02 15:53:54 +03:00
779c0fe916 Refactor SpaceDetailsDialog to improve code readability and structure by simplifying widget hierarchy and enhancing the use of Bloc for state management. 2025-07-02 15:42:19 +03:00
e448eabda6 Refactor SpaceSubSpacesDialog to use SubSpacesInput for managing subspaces, enhancing state management and UI structure. Update SpaceDetailsActionButtons to handle optional save callback. 2025-07-02 15:41:13 +03:00
9dfb3ed369 Refactor SpaceDetailsDialog and SpaceIconPicker to integrate Bloc for state management, enhancing icon selection and dialog functionality. 2025-07-02 15:20:52 +03:00
63353af38b Add SpaceDetails dialog and related widgets for creating and editing spaces, including SpaceDetailsDevicesBox and SpaceSubSpacesBox for managing devices and subspaces. 2025-07-02 15:03:23 +03:00
68b6c9b18c Refactor SpaceDetailsBloc to move SpaceDetailsService declaration for improved clarity 2025-07-02 15:03:07 +03:00
fa6ee9a0af Add factory method empty to SpaceDetailsModel for creating default instances 2025-07-02 15:03:00 +03:00
3601b02bc3 Add SpaceDetailsModelBloc and events for managing space details state 2025-07-02 15:02:55 +03:00
fdd0526c78 added copyWith to SpaceDetailsModel and its property models. 2025-07-02 14:17:27 +03:00
bdeec7d325 Add SpaceIconPicker widget for selecting and displaying space icons with a dialog option. 2025-07-02 11:27:36 +03:00
50ff17a0c1 Add SpaceIconSelectionDialog widget for selecting space icons in a dialog. 2025-07-02 11:27:26 +03:00
87c2e3261d Add SpaceDetailsActionButtons widget for improved action handling in space details. 2025-07-02 11:27:13 +03:00
62a6f9c993 Add ButtonContentWidget for customizable button UI in space details. 2025-07-02 11:27:03 +03:00
f7e4d6ff07 added default dialog background color to be white. 2025-07-02 09:33:45 +03:00
161 changed files with 4694 additions and 3124 deletions

View File

@ -1,10 +0,0 @@
<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>

Before

Width:  |  Height:  |  Size: 1.4 KiB

View File

@ -1,4 +0,0 @@
<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>

Before

Width:  |  Height:  |  Size: 1.1 KiB

View File

@ -1,4 +0,0 @@
<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>

Before

Width:  |  Height:  |  Size: 799 B

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 18 KiB

View File

@ -2,7 +2,7 @@ import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:syncrow_web/pages/access_management/bloc/access_event.dart';
import 'package:syncrow_web/pages/access_management/bloc/access_state.dart';
import 'package:syncrow_web/pages/access_management/model/password_model.dart';
import 'package:syncrow_web/pages/access_management/booking_system/presentation/model/password_model.dart';
import 'package:syncrow_web/pages/common/bloc/project_manager.dart';
import 'package:syncrow_web/pages/common/hour_picker_dialog.dart';
import 'package:syncrow_web/services/access_mang_api.dart';

View File

@ -1,5 +1,5 @@
import 'package:equatable/equatable.dart';
import 'package:syncrow_web/pages/access_management/model/password_model.dart';
import 'package:syncrow_web/pages/access_management/booking_system/presentation/model/password_model.dart';
abstract class AccessState extends Equatable {
const AccessState();

View File

@ -0,0 +1,52 @@
import 'package:dio/dio.dart';
import 'package:syncrow_web/pages/access_management/booking_system/domain/load_bookable_spaces_param.dart';
import 'package:syncrow_web/pages/access_management/booking_system/domain/models/paginated_bookable_spaces.dart';
import 'package:syncrow_web/pages/access_management/booking_system/domain/services/bookable_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 RemoteBookableSpacesService implements BookableSystemService {
const RemoteBookableSpacesService(this._httpService);
final HTTPService _httpService;
static const _defaultErrorMessage = 'Failed to load bookable spaces';
@override
Future<PaginatedBookableSpaces> getBookableSpaces({
required LoadBookableSpacesParam param,
}) async {
try {
final response = await _httpService.get(
path: ApiEndpoints.getBookableSpaces,
queryParameters: {
'page': param.page,
'size': param.size,
'active': true,
'configured': true,
if (param.search != null &&
param.search.isNotEmpty &&
param.search != 'null')
'search': param.search,
},
expectedResponseModel: (json) {
return PaginatedBookableSpaces.fromJson(
json as Map<String, dynamic>,
);
},
);
return response;
} 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,36 @@
import 'package:equatable/equatable.dart';
class LoadBookableSpacesParam extends Equatable {
const LoadBookableSpacesParam({
this.page = 1,
this.size = 25,
this.search = '',
this.active = true,
this.configured = true,
});
final int page;
final int size;
final String search;
final bool active;
final bool configured;
LoadBookableSpacesParam copyWith({
int? page,
int? size,
String? search,
bool? active,
bool? configured,
}) {
return LoadBookableSpacesParam(
page: page ?? this.page,
size: size ?? this.size,
search: search ?? this.search,
active: active ?? this.active,
configured: configured ?? this.configured,
);
}
@override
List<Object?> get props => [page, size, search, active, configured];
}

View File

@ -0,0 +1,52 @@
class BookableSpaceModel {
final String uuid;
final String spaceName;
final String virtualLocation;
final BookableConfig bookableConfig;
BookableSpaceModel({
required this.uuid,
required this.spaceName,
required this.virtualLocation,
required this.bookableConfig,
});
factory BookableSpaceModel.fromJson(Map<String, dynamic> json) {
return BookableSpaceModel(
uuid: json['uuid'] as String,
spaceName: json['spaceName'] as String,
virtualLocation: json['virtualLocation'] as String,
bookableConfig: BookableConfig.fromJson(
json['bookableConfig'] as Map<String, dynamic>),
);
}
}
class BookableConfig {
final String uuid;
final List<String> daysAvailable;
final String startTime;
final String endTime;
final bool active;
final int points;
BookableConfig({
required this.uuid,
required this.daysAvailable,
required this.startTime,
required this.endTime,
required this.active,
required this.points,
});
factory BookableConfig.fromJson(Map<String, dynamic> json) {
return BookableConfig(
uuid: json['uuid'] as String,
daysAvailable: (json['daysAvailable'] as List).cast<String>(),
startTime: json['startTime'] as String,
endTime: json['endTime'] as String,
active: json['active'] as bool,
points: json['points'] as int,
);
}
}

View File

@ -0,0 +1,40 @@
import 'package:syncrow_web/pages/access_management/booking_system/domain/models/bookable_room.dart';
class PaginatedBookableSpaces {
final List<BookableSpaceModel> data;
final String message;
final int page;
final int size;
final int totalItem;
final int totalPage;
final bool hasNext;
final bool hasPrevious;
PaginatedBookableSpaces({
required this.data,
required this.message,
required this.page,
required this.size,
required this.totalItem,
required this.totalPage,
required this.hasNext,
required this.hasPrevious,
});
factory PaginatedBookableSpaces.fromJson(Map<String, dynamic> json) {
return PaginatedBookableSpaces(
data: (json['data'] as List)
.map((item) => BookableSpaceModel.fromJson(item))
.toList(),
message: json['message'] as String,
page: json['page'] as int,
size: json['size'] as int,
totalItem: json['totalItem'] as int,
totalPage: json['totalPage'] as int,
hasNext: json['hasNext'] as bool,
hasPrevious: json['hasPrevious'] as bool,
);
}
}

View File

@ -0,0 +1,8 @@
import 'package:syncrow_web/pages/access_management/booking_system/domain/load_bookable_spaces_param.dart';
import 'package:syncrow_web/pages/access_management/booking_system/domain/models/paginated_bookable_spaces.dart';
abstract class BookableSystemService {
Future<PaginatedBookableSpaces> getBookableSpaces({
required LoadBookableSpacesParam param,
});
}

View File

@ -0,0 +1,45 @@
import 'dart:async';
import 'package:syncrow_web/pages/access_management/booking_system/domain/load_bookable_spaces_param.dart';
import 'package:syncrow_web/pages/access_management/booking_system/domain/models/paginated_bookable_spaces.dart';
import 'package:syncrow_web/pages/access_management/booking_system/domain/services/bookable_system_service.dart';
class DebouncedBookableSpacesService implements BookableSystemService {
final BookableSystemService _inner;
final Duration debounceDuration;
Timer? _debounceTimer;
Completer<PaginatedBookableSpaces>? _lastCompleter;
DebouncedBookableSpacesService(
this._inner, {
this.debounceDuration = const Duration(milliseconds: 500),
});
@override
Future<PaginatedBookableSpaces> getBookableSpaces({
required LoadBookableSpacesParam param,
}) {
_debounceTimer?.cancel();
if (_lastCompleter != null && !_lastCompleter!.isCompleted) {
_lastCompleter!.completeError(StateError("Cancelled by new search"));
}
final completer = Completer<PaginatedBookableSpaces>();
_lastCompleter = completer;
_debounceTimer = Timer(debounceDuration, () async {
try {
final result = await _inner.getBookableSpaces(param: param);
if (!completer.isCompleted) {
completer.complete(result);
}
} catch (e, st) {
if (!completer.isCompleted) {
completer.completeError(e, st);
}
}
});
return completer.future;
}
}

View File

@ -0,0 +1,143 @@
import 'dart:async';
import 'package:bloc/bloc.dart';
import 'package:calendar_view/calendar_view.dart';
import 'package:flutter/material.dart';
part 'events_event.dart';
part 'events_state.dart';
class CalendarEventsBloc extends Bloc<CalendarEventsEvent, CalendarEventState> {
final EventController eventController = EventController();
CalendarEventsBloc() : super(EventsInitial()) {
on<LoadEvents>(_onLoadEvents);
on<AddEvent>(_onAddEvent);
on<StartTimer>(_onStartTimer);
on<DisposeResources>(_onDisposeResources);
on<GoToWeek>(_onGoToWeek);
}
Future<void> _onLoadEvents(
LoadEvents event,
Emitter<CalendarEventState> emit,
) async {
emit(EventsLoading());
try {
final events = _generateDummyEventsForWeek(event.weekStart);
eventController.addAll(events);
emit(EventsLoaded(
events: events,
initialDate: event.weekStart,
weekDays: _getWeekDays(event.weekStart),
));
} catch (e) {
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) {
eventController.add(event.event);
if (state is EventsLoaded) {
final loaded = state as EventsLoaded;
emit(EventsLoaded(
events: [...eventController.events],
initialDate: loaded.initialDate,
weekDays: loaded.weekDays,
));
}
}
void _onStartTimer(StartTimer event, Emitter<CalendarEventState> emit) {}
void _onDisposeResources(
DisposeResources event, Emitter<CalendarEventState> emit) {
eventController.dispose();
}
void _onGoToWeek(GoToWeek event, Emitter<CalendarEventState> emit) {
if (state is EventsLoaded) {
final loaded = state as EventsLoaded;
final newWeekDays = _getWeekDays(event.weekDate);
emit(EventsLoaded(
events: loaded.events,
initialDate: event.weekDate,
weekDays: newWeekDays,
));
}
}
List<CalendarEventData> _generateDummyEvents() {
final now = DateTime.now();
return [
CalendarEventData(
date: now,
startTime: now.copyWith(hour: 8, minute: 00, second: 0),
endTime: now.copyWith(hour: 9, minute: 00, second: 0),
title: 'Team Meeting',
description: 'Weekly team sync',
color: Colors.blue,
),
CalendarEventData(
date: now,
startTime: now.copyWith(hour: 9, minute: 00, second: 0),
endTime: now.copyWith(hour: 10, minute: 30, second: 0),
title: 'Team Meeting',
description: 'Weekly team sync',
color: Colors.blue,
),
CalendarEventData(
date: now.add(const Duration(days: 1)),
startTime: now.copyWith(hour: 14, day: now.day + 1),
endTime: now.copyWith(hour: 15, day: now.day + 1),
title: 'Client Call',
description: 'Project discussion',
color: Colors.green,
),
CalendarEventData(
date: now.add(const Duration(days: 2)),
startTime: now.copyWith(hour: 11, day: now.day + 2),
endTime: now.copyWith(hour: 12, day: now.day + 2),
title: 'Lunch with Team',
color: Colors.orange,
),
];
}
List<DateTime> _getWeekDays(DateTime date) {
final int weekday = date.weekday;
final DateTime monday = date.subtract(Duration(days: weekday - 1));
return List.generate(7, (i) => monday.add(Duration(days: i)));
}
@override
Future<void> close() {
eventController.dispose();
return super.close();
}
}

View File

@ -0,0 +1,25 @@
part of 'events_bloc.dart';
@immutable
abstract class CalendarEventsEvent {
const CalendarEventsEvent();
}
class LoadEvents extends CalendarEventsEvent {
final DateTime weekStart;
const LoadEvents({required this.weekStart});
}
class AddEvent extends CalendarEventsEvent {
final CalendarEventData event;
AddEvent(this.event);
}
class StartTimer extends CalendarEventsEvent {}
class DisposeResources extends CalendarEventsEvent {}
class GoToWeek extends CalendarEventsEvent {
final DateTime weekDate;
GoToWeek(this.weekDate);
}

View File

@ -0,0 +1,25 @@
part of 'events_bloc.dart';
@immutable
abstract class CalendarEventState {}
class EventsInitial extends CalendarEventState {}
class EventsLoading extends CalendarEventState {}
class EventsLoaded extends CalendarEventState {
final List<CalendarEventData> events;
final DateTime initialDate;
final List<DateTime> weekDays;
EventsLoaded({
required this.events,
required this.initialDate,
required this.weekDays,
});
}
class EventsError extends CalendarEventState {
final String message;
EventsError(this.message);
}

View File

@ -0,0 +1,40 @@
import 'package:bloc/bloc.dart';
import 'package:syncrow_web/pages/access_management/booking_system/presentation/bloc/date_selection/date_selection_event.dart';
import 'date_selection_state.dart';
class DateSelectionBloc extends Bloc<DateSelectionEvent, DateSelectionState> {
DateSelectionBloc() : super(DateSelectionState.initial()) {
on<SelectDate>((event, emit) {
final newWeekStart = _getStartOfWeek(event.selectedDate);
emit(state.copyWith(
selectedDate: event.selectedDate,
weekStart: newWeekStart,
));
});
on<NextWeek>((event, emit) {
final newWeekStart = state.weekStart.add(const Duration(days: 7));
emit(state.copyWith(
weekStart: newWeekStart,
));
});
on<PreviousWeek>((event, emit) {
final newWeekStart = state.weekStart.subtract(const Duration(days: 7));
emit(state.copyWith(
weekStart: newWeekStart,
));
});
on<SelectDateFromSidebarCalendar>((event, emit) {
emit(state.copyWith(
selectedDateFromSideBarCalender: event.selectedDate,
));
});
}
static DateTime _getStartOfWeek(DateTime date) {
return date.subtract(Duration(days: date.weekday - 1));
}
}

View File

@ -0,0 +1,18 @@
abstract class DateSelectionEvent {
const DateSelectionEvent();
}
class SelectDate extends DateSelectionEvent {
final DateTime selectedDate;
const SelectDate(this.selectedDate);
}
class NextWeek extends DateSelectionEvent {}
class PreviousWeek extends DateSelectionEvent {}
class SelectDateFromSidebarCalendar extends DateSelectionEvent {
final DateTime selectedDate;
SelectDateFromSidebarCalendar(this.selectedDate);
}

View File

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

View File

@ -0,0 +1,14 @@
import 'package:bloc/bloc.dart';
import 'package:syncrow_web/pages/access_management/booking_system/domain/models/bookable_room.dart';
part 'selected_bookable_space_event.dart';
part 'selected_bookable_space_state.dart';
class SelectedBookableSpaceBloc
extends Bloc<SelectedBookableSpaceEvent, SelectedBookableSpaceState> {
SelectedBookableSpaceBloc() : super(const SelectedBookableSpaceState()) {
on<SelectBookableSpace>((event, emit) {
emit(SelectedBookableSpaceState(
selectedBookableSpace: event.bookableSpace));
});
}
}

View File

@ -0,0 +1,11 @@
part of 'selected_bookable_space_bloc.dart';
abstract class SelectedBookableSpaceEvent {
const SelectedBookableSpaceEvent();
}
class SelectBookableSpace extends SelectedBookableSpaceEvent {
final BookableSpaceModel bookableSpace;
const SelectBookableSpace(this.bookableSpace);
}

View File

@ -0,0 +1,9 @@
part of 'selected_bookable_space_bloc.dart';
class SelectedBookableSpaceState {
final BookableSpaceModel? selectedBookableSpace;
const SelectedBookableSpaceState(
{ this.selectedBookableSpace,}
);
}

View File

@ -0,0 +1,148 @@
import 'dart:async';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:syncrow_web/pages/access_management/booking_system/domain/load_bookable_spaces_param.dart';
import 'package:syncrow_web/pages/access_management/booking_system/domain/services/bookable_system_service.dart';
import 'package:syncrow_web/pages/access_management/booking_system/presentation/bloc/sidebar/sidebar_event.dart';
import 'package:syncrow_web/pages/access_management/booking_system/presentation/bloc/sidebar/sidebar_state.dart';
class SidebarBloc extends Bloc<SidebarEvent, SidebarState> {
final BookableSystemService _bookingService;
int _currentPage = 1;
final int _pageSize = 20;
String _currentSearch = '';
SidebarBloc(this._bookingService)
: super(SidebarState(
allRooms: [],
displayedRooms: [],
isLoading: true,
hasMore: true,
)) {
on<LoadBookableSpaces>(_onLoadBookableSpaces);
on<LoadMoreSpaces>(_onLoadMoreSpaces);
on<SelectRoomEvent>(_onSelectRoom);
on<SearchRoomsEvent>(_onSearchRooms);
on<ResetSearch>(_onResetSearch);
}
Future<void> _onLoadBookableSpaces(
LoadBookableSpaces event,
Emitter<SidebarState> emit,
) async {
try {
emit(state.copyWith(isLoading: true, errorMessage: null));
_currentPage = 1;
_currentSearch = '';
final paginatedSpaces = await _bookingService.getBookableSpaces(
param: LoadBookableSpacesParam(
page: _currentPage,
size: _pageSize,
search: _currentSearch,
),
);
emit(state.copyWith(
allRooms: paginatedSpaces.data,
displayedRooms: paginatedSpaces.data,
isLoading: false,
hasMore: paginatedSpaces.hasNext,
totalPages: paginatedSpaces.totalPage,
currentPage: _currentPage,
));
} catch (e) {
emit(state.copyWith(
isLoading: false,
errorMessage: 'Failed to load rooms: ${e.toString()}',
));
}
}
Future<void> _onLoadMoreSpaces(
LoadMoreSpaces event,
Emitter<SidebarState> emit,
) async {
if (!state.hasMore || state.isLoadingMore) return;
try {
emit(state.copyWith(isLoadingMore: true));
_currentPage++;
final paginatedSpaces = await _bookingService.getBookableSpaces(
param: LoadBookableSpacesParam(
page: _currentPage,
size: _pageSize,
search: _currentSearch,
),
);
final updatedRooms = [...state.allRooms, ...paginatedSpaces.data];
emit(state.copyWith(
allRooms: updatedRooms,
displayedRooms: updatedRooms,
isLoadingMore: false,
hasMore: paginatedSpaces.hasNext,
totalPages: paginatedSpaces.totalPage,
currentPage: _currentPage,
));
} catch (e) {
_currentPage--;
emit(state.copyWith(
isLoadingMore: false,
errorMessage: 'Failed to load more rooms: ${e.toString()}',
));
}
}
Future<void> _onSearchRooms(
SearchRoomsEvent event,
Emitter<SidebarState> emit,
) async {
try {
_currentSearch = event.query;
_currentPage = 1;
emit(state.copyWith(isLoading: true, errorMessage: null));
final paginatedSpaces = await _bookingService.getBookableSpaces(
param: LoadBookableSpacesParam(
page: _currentPage,
size: _pageSize,
search: _currentSearch,
),
);
emit(state.copyWith(
allRooms: paginatedSpaces.data,
displayedRooms: paginatedSpaces.data,
isLoading: false,
hasMore: paginatedSpaces.hasNext,
totalPages: paginatedSpaces.totalPage,
currentPage: _currentPage,
));
} catch (e) {
emit(state.copyWith(
isLoading: false,
errorMessage: 'Search failed: ${e.toString()}',
));
}
}
void _onResetSearch(
ResetSearch event,
Emitter<SidebarState> emit,
) {
_currentSearch = '';
add(LoadBookableSpaces());
}
void _onSelectRoom(
SelectRoomEvent event,
Emitter<SidebarState> emit,
) {
emit(state.copyWith(selectedRoomId: event.roomId));
}
@override
Future<void> close() {
return super.close();
}
}

View File

@ -0,0 +1,25 @@
abstract class SidebarEvent {}
class LoadBookableSpaces extends SidebarEvent {}
class SelectRoomEvent extends SidebarEvent {
final String roomId;
SelectRoomEvent(this.roomId);
}
class SearchRoomsEvent extends SidebarEvent {
final String query;
SearchRoomsEvent(this.query);
}
class LoadMoreSpaces extends SidebarEvent {}
class ResetSearch extends SidebarEvent {}
class ExecuteSearch extends SidebarEvent {
final String query;
ExecuteSearch(this.query);
}

View File

@ -0,0 +1,49 @@
import 'package:syncrow_web/pages/access_management/booking_system/domain/models/bookable_room.dart';
class SidebarState {
final List<BookableSpaceModel> allRooms;
final List<BookableSpaceModel> displayedRooms;
final bool isLoading;
final bool isLoadingMore;
final String? errorMessage;
final String? selectedRoomId;
final bool hasMore;
final int totalPages;
final int currentPage;
SidebarState({
required this.allRooms,
required this.displayedRooms,
required this.isLoading,
this.isLoadingMore = false,
this.errorMessage,
this.selectedRoomId,
this.hasMore = true,
this.totalPages = 0,
this.currentPage = 1,
});
SidebarState copyWith({
List<BookableSpaceModel>? allRooms,
List<BookableSpaceModel>? displayedRooms,
bool? isLoading,
bool? isLoadingMore,
String? errorMessage,
String? selectedRoomId,
bool? hasMore,
int? totalPages,
int? currentPage,
}) {
return SidebarState(
allRooms: allRooms ?? this.allRooms,
displayedRooms: displayedRooms ?? this.displayedRooms,
isLoading: isLoading ?? this.isLoading,
isLoadingMore: isLoadingMore ?? this.isLoadingMore,
errorMessage: errorMessage ?? this.errorMessage,
selectedRoomId: selectedRoomId ?? this.selectedRoomId,
hasMore: hasMore ?? this.hasMore,
totalPages: totalPages ?? this.totalPages,
currentPage: currentPage ?? this.currentPage,
);
}
}

View File

@ -0,0 +1,266 @@
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:intl/intl.dart';
import 'package:calendar_view/calendar_view.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_state.dart';
import 'package:syncrow_web/pages/access_management/booking_system/presentation/bloc/selected_bookable_space_bloc/selected_bookable_space_bloc.dart';
import 'package:syncrow_web/pages/access_management/booking_system/presentation/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/icon_text_button.dart';
import 'package:syncrow_web/pages/access_management/booking_system/presentation/view/widgets/weekly_calendar_page.dart';
import 'package:syncrow_web/utils/color_manager.dart';
import 'package:syncrow_web/utils/constants/assets.dart';
class BookingPage extends StatefulWidget {
const BookingPage({super.key});
@override
State<BookingPage> createState() => _BookingPageState();
}
class _BookingPageState extends State<BookingPage> {
late final EventController _eventController;
@override
void initState() {
super.initState();
_eventController = EventController();
}
@override
void dispose() {
_eventController.dispose();
super.dispose();
}
List<CalendarEventData> _generateDummyEventsForWeek(DateTime weekStart) {
final List<CalendarEventData> events = [];
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 _loadEventsForWeek(DateTime weekStart) {
_eventController.removeWhere((_) => true);
_eventController.addAll(_generateDummyEventsForWeek(weekStart));
}
@override
Widget build(BuildContext context) {
return MultiBlocProvider(
providers: [
BlocProvider(create: (_) => SelectedBookableSpaceBloc()),
BlocProvider(create: (_) => DateSelectionBloc()),
],
child: BlocListener<DateSelectionBloc, DateSelectionState>(
listenWhen: (previous, current) =>
previous.weekStart != current.weekStart,
listener: (context, state) {
_loadEventsForWeek(state.weekStart);
},
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Expanded(
child: Container(
decoration: BoxDecoration(
color: ColorsManager.whiteColors,
boxShadow: [
BoxShadow(
color: ColorsManager.blackColor.withOpacity(0.1),
offset: const Offset(3, 0),
blurRadius: 6,
spreadRadius: 0,
),
],
),
child: Column(
children: [
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(
flex: 4,
child: Padding(
padding: const EdgeInsets.all(20.0),
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(
children: [
IconButton(
iconSize: 15,
icon: const Icon(Icons.arrow_back_ios,
color: ColorsManager.lightGrayColor),
onPressed: () {
context
.read<DateSelectionBloc>()
.add(PreviousWeek());
},
),
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
.read<DateSelectionBloc>()
.add(NextWeek());
},
),
],
),
);
},
),
],
),
Expanded(
child: BlocBuilder<SelectedBookableSpaceBloc,
SelectedBookableSpaceState>(
builder: (context, roomState) {
final selectedRoom = roomState.selectedBookableSpace;
return BlocBuilder<DateSelectionBloc,
DateSelectionState>(
builder: (context, dateState) {
return 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,242 @@
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_svg/flutter_svg.dart';
import 'package:syncrow_web/pages/access_management/booking_system/data/services/remote_bookable_spaces_service.dart';
import 'package:syncrow_web/pages/access_management/booking_system/domain/models/bookable_room.dart';
import 'package:syncrow_web/pages/access_management/booking_system/presentation/bloc/sidebar/sidebar_bloc.dart';
import 'package:syncrow_web/pages/access_management/booking_system/presentation/bloc/sidebar/sidebar_event.dart';
import 'package:syncrow_web/pages/access_management/booking_system/presentation/bloc/sidebar/sidebar_state.dart';
import 'package:syncrow_web/pages/access_management/booking_system/presentation/view/widgets/room_list_item.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 BookingSidebar extends StatelessWidget {
final void Function(BookableSpaceModel) onRoomSelected;
const BookingSidebar({
super.key,
required this.onRoomSelected,
});
@override
Widget build(BuildContext context) {
return BlocProvider(
create: (context) => SidebarBloc(RemoteBookableSpacesService(
HTTPService(),
))
..add(LoadBookableSpaces()),
child: _SidebarContent(onRoomSelected: onRoomSelected),
);
}
}
class _SidebarContent extends StatefulWidget {
final void Function(BookableSpaceModel) onRoomSelected;
const _SidebarContent({
required this.onRoomSelected,
});
@override
State<_SidebarContent> createState() => __SidebarContentState();
}
class __SidebarContentState extends State<_SidebarContent> {
final TextEditingController searchController = TextEditingController();
final ScrollController _scrollController = ScrollController();
@override
void initState() {
super.initState();
_scrollController.addListener(_scrollListener);
}
@override
void dispose() {
_scrollController.dispose();
super.dispose();
}
void _scrollListener() {
if (_scrollController.position.pixels ==
_scrollController.position.maxScrollExtent) {
context.read<SidebarBloc>().add(LoadMoreSpaces());
}
}
void _handleSearch(String value) {
context.read<SidebarBloc>().add(SearchRoomsEvent(value));
}
@override
Widget build(BuildContext context) {
return BlocConsumer<SidebarBloc, SidebarState>(
listener: (context, state) {
if (state.currentPage == 1 && searchController.text.isNotEmpty) {
searchController.clear();
}
},
builder: (context, state) {
return Column(
children: [
const _SidebarHeader(title: 'Spaces'),
Container(
decoration: BoxDecoration(
color: ColorsManager.whiteColors,
borderRadius: BorderRadius.circular(8.0),
boxShadow: [
BoxShadow(
color: ColorsManager.blackColor.withOpacity(0.1),
offset: const Offset(0, -2),
blurRadius: 4,
spreadRadius: 0,
),
BoxShadow(
color: ColorsManager.blackColor.withOpacity(0.1),
offset: const Offset(0, 2),
blurRadius: 4,
spreadRadius: 0,
),
],
),
child: Padding(
padding: const EdgeInsets.all(8.0),
child: Container(
child: Padding(
padding: const EdgeInsets.symmetric(
horizontal: 16.0, vertical: 8.0),
child: Container(
decoration: BoxDecoration(
color: ColorsManager.counterBackgroundColor,
borderRadius: BorderRadius.circular(8.0),
),
child: Row(
children: [
Expanded(
child: TextField(
style: Theme.of(context)
.textTheme
.bodyMedium
?.copyWith(
color: ColorsManager.blackColor,
),
controller: searchController,
onChanged: _handleSearch,
decoration: InputDecoration(
hintText: 'Search',
suffixIcon: Padding(
padding: const EdgeInsets.all(8.0),
child: SizedBox(
width: 20,
height: 20,
child: SvgPicture.asset(
Assets.searchIconUser,
color: ColorsManager.primaryTextColor,
),
),
),
contentPadding: const EdgeInsets.symmetric(
vertical: 8, horizontal: 12),
border: const OutlineInputBorder(
borderSide: BorderSide.none),
),
),
),
if (searchController.text.isNotEmpty)
IconButton(
icon: const Icon(Icons.close),
onPressed: () {
context.read<SidebarBloc>().add(ResetSearch());
},
),
],
),
),
),
),
),
),
if (state.isLoading)
const Expanded(
child: Center(child: CircularProgressIndicator()),
)
else if (state.errorMessage != null)
Expanded(
child: Center(child: Text(state.errorMessage!)),
)
else
Expanded(
child: ListView.builder(
controller: _scrollController,
itemCount:
state.displayedRooms.length + (state.hasMore ? 1 : 0),
itemBuilder: (context, index) {
if (index == state.displayedRooms.length) {
return _buildLoadMoreIndicator(state);
}
final room = state.displayedRooms[index];
return RoomListItem(
room: room,
isSelected: state.selectedRoomId == room.uuid,
onTap: () {
context
.read<SidebarBloc>()
.add(SelectRoomEvent(room.uuid));
widget.onRoomSelected(room);
},
);
},
),
),
],
);
},
);
}
Widget _buildLoadMoreIndicator(SidebarState state) {
if (state.isLoadingMore) {
return const Padding(
padding: EdgeInsets.symmetric(vertical: 16.0),
child: Center(child: CircularProgressIndicator()),
);
} else if (state.hasMore) {
return const Padding(
padding: EdgeInsets.symmetric(vertical: 16.0),
child: Center(child: Text('Scroll to load more')),
);
} else {
return const SizedBox.shrink();
}
}
}
class _SidebarHeader extends StatelessWidget {
final String title;
const _SidebarHeader({
required this.title,
});
@override
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.all(16.0),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
title,
style: Theme.of(context).textTheme.titleLarge?.copyWith(
fontWeight: FontWeight.w400,
color: ColorsManager.primaryTextColor,
fontSize: 20,
),
),
],
),
);
}
}

View File

@ -0,0 +1,83 @@
import 'package:flutter/material.dart';
import 'package:calendar_date_picker2/calendar_date_picker2.dart';
import 'package:syncrow_web/utils/color_manager.dart';
class CustomCalendarPage extends StatefulWidget {
final DateTime selectedDate;
final Function(int day, int month, int year) onDateChanged;
const CustomCalendarPage({
super.key,
required this.selectedDate,
required this.onDateChanged,
});
@override
State<CustomCalendarPage> createState() => _CustomCalendarPageState();
}
class _CustomCalendarPageState extends State<CustomCalendarPage> {
late DateTime _selectedDate;
@override
void initState() {
super.initState();
_selectedDate = widget.selectedDate;
}
@override
void didUpdateWidget(CustomCalendarPage oldWidget) {
super.didUpdateWidget(oldWidget);
if (widget.selectedDate != oldWidget.selectedDate) {
setState(() {
_selectedDate = widget.selectedDate;
});
}
}
@override
Widget build(BuildContext context) {
final config = CalendarDatePicker2Config(
calendarType: CalendarDatePicker2Type.single,
selectedDayHighlightColor: const Color(0xFF3B82F6),
selectedDayTextStyle: const TextStyle(
color: Colors.white,
fontWeight: FontWeight.w400,
fontSize: 14,
),
dayTextStyle: const TextStyle(
color: ColorsManager.blackColor,
fontWeight: FontWeight.w400,
fontSize: 14,
),
weekdayLabelTextStyle: const TextStyle(
color: ColorsManager.grey50,
fontWeight: FontWeight.w400,
fontSize: 14,
),
controlsTextStyle: const TextStyle(
color: Color(0xFF232D3A),
fontWeight: FontWeight.w400,
fontSize: 18,
),
centerAlignModePicker: false,
disableMonthPicker: true,
firstDayOfWeek: 1,
weekdayLabels: const ['Mo', 'Tu', 'We', 'Th', 'Fr', 'Sa', 'Su'],
);
return CalendarDatePicker2(
config: config,
value: [_selectedDate],
onValueChanged: (dates) {
final picked = dates.first;
if (picked != null) {
setState(() {
_selectedDate = picked;
});
widget.onDateChanged(picked.day, picked.month, picked.year);
}
},
);
}
}

View File

@ -12,24 +12,21 @@ class SvgTextButton extends StatelessWidget {
final double borderRadius;
final List<BoxShadow> boxShadow;
final double svgSize;
final double? fontSize;
final FontWeight? fontWeight;
const SvgTextButton({
super.key,
required this.svgAsset,
this.fontSize,
this.fontWeight,
required this.label,
required this.onPressed,
this.backgroundColor = ColorsManager.circleRolesBackground,
this.svgColor = const Color(0xFF496EFF),
this.labelColor = Colors.black87,
this.labelColor = Colors.black,
this.borderRadius = 10.0,
this.boxShadow = const [
BoxShadow(
color: ColorsManager.textGray,
blurRadius: 12,
offset: Offset(0, 4),
color: ColorsManager.lightGrayColor,
blurRadius: 4,
offset: Offset(0, 1),
),
],
this.svgSize = 24.0,
@ -56,15 +53,14 @@ class SvgTextButton extends StatelessWidget {
svgAsset,
width: svgSize,
height: svgSize,
color: svgColor,
),
const SizedBox(width: 12),
Text(
label,
style: TextStyle(
color: labelColor,
fontSize: fontSize ?? 16,
fontWeight: fontWeight ?? FontWeight.w500,
fontSize: 12,
fontWeight: FontWeight.w400,
),
),
],

View File

@ -0,0 +1,42 @@
import 'package:flutter/material.dart';
import 'package:syncrow_web/pages/access_management/booking_system/domain/models/bookable_room.dart';
import 'package:syncrow_web/utils/color_manager.dart';
class RoomListItem extends StatelessWidget {
final BookableSpaceModel room;
final bool isSelected;
final VoidCallback onTap;
const RoomListItem({
required this.room,
required this.isSelected,
required this.onTap,
});
@override
Widget build(BuildContext context) {
return RadioListTile(
value: room.uuid,
contentPadding: const EdgeInsetsDirectional.symmetric(horizontal: 16),
groupValue: isSelected ? room.uuid : null,
visualDensity: const VisualDensity(vertical: -4),
onChanged: (value) => onTap(),
activeColor: ColorsManager.primaryColor,
title: Text(
room.spaceName,
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
color: ColorsManager.lightGrayColor,
fontWeight: FontWeight.w700,
fontSize: 12),
),
subtitle: Text(
room.virtualLocation,
style: Theme.of(context).textTheme.bodySmall?.copyWith(
fontSize: 10,
fontWeight: FontWeight.w400,
color: ColorsManager.textGray,
),
),
);
}
}

View File

@ -0,0 +1,292 @@
import 'package:flutter/material.dart';
import 'package:calendar_view/calendar_view.dart';
import 'package:intl/intl.dart';
import 'package:syncrow_web/utils/color_manager.dart';
class WeeklyCalendarPage extends StatelessWidget {
final DateTime weekStart;
final DateTime selectedDate;
final EventController eventController;
final String? startTime;
final String? endTime;
final DateTime? selectedDateFromSideBarCalender;
const WeeklyCalendarPage({
super.key,
required this.weekStart,
required this.selectedDate,
required this.eventController,
this.startTime,
this.endTime,
this.selectedDateFromSideBarCalender,
});
@override
Widget build(BuildContext context) {
final startHour = _parseHour(startTime, defaultValue: 0);
final endHour = _parseHour(endTime, defaultValue: 24);
if (endTime == null || endTime!.isEmpty) {
return const Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
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 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;
return LayoutBuilder(
builder: (context, constraints) {
final double calendarWidth = constraints.maxWidth;
final double dayColumnWidth =
(calendarWidth - timeLineWidth) / totalDays - 0.1;
return Padding(
padding: const EdgeInsets.only(left: 25.0, right: 25.0, top: 25),
child: Stack(
children: [
WeekView(
pageViewPhysics: const NeverScrollableScrollPhysics(),
key: ValueKey(weekStart),
controller: eventController,
initialDay: weekStart,
startHour: startHour - 1,
endHour: endHour,
heightPerMinute: 1.1,
showLiveTimeLineInAllDays: false,
showVerticalLines: true,
emulateVerticalOffsetBy: -80,
startDay: WeekDays.monday,
liveTimeIndicatorSettings: const LiveTimeIndicatorSettings(
showBullet: false,
height: 0,
),
weekDayBuilder: (date) {
final index = weekDays.indexWhere((d) => isSameDay(d, date));
final isSelectedDay = index == selectedDayIndex;
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) {
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,
),
],
),
),
);
},
timeLineWidth: timeLineWidth,
weekPageHeaderBuilder: (start, end) => Container(),
weekTitleHeight: 60,
weekNumberBuilder: (firstDayOfWeek) => Padding(
padding: const EdgeInsets.only(right: 15, bottom: 10),
child: Column(
crossAxisAlignment: CrossAxisAlignment.end,
mainAxisAlignment: MainAxisAlignment.end,
children: [
Text(
firstDayOfWeek.timeZoneName.replaceAll(':00', ''),
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
fontSize: 12,
color: ColorsManager.blackColor,
fontWeight: FontWeight.w400,
),
),
],
),
),
eventTileBuilder: (date, events, boundary, start, end) {
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(),
),
);
},
),
if (selectedDayIndex >= 0)
Positioned(
left: (timeLineWidth + 3) +
(dayColumnWidth - 8) * (selectedDayIndex - 0.01),
top: 0,
bottom: 0,
width: dayColumnWidth,
child: IgnorePointer(
child: Container(
margin: const EdgeInsets.symmetric(
vertical: 0, horizontal: 4),
color: ColorsManager.spaceColor.withOpacity(0.07),
),
),
),
if (selectedSidebarIndex >= 0 &&
selectedSidebarIndex != selectedDayIndex)
Positioned(
left: (timeLineWidth + 3) +
(dayColumnWidth - 8) * (selectedSidebarIndex - 0.01),
top: 0,
bottom: 0,
width: dayColumnWidth,
child: IgnorePointer(
child: Container(
margin: const EdgeInsets.symmetric(
vertical: 0, horizontal: 4),
color: Colors.orange.withOpacity(0.14),
),
),
),
Positioned(
right: 0,
top: 50,
bottom: 0,
child: IgnorePointer(
child: Container(
width: 1,
color: Theme.of(context).scaffoldBackgroundColor,
),
),
),
],
),
);
},
);
}
List<DateTime> _getWeekDays(DateTime date) {
final int weekday = date.weekday;
final DateTime monday = date.subtract(Duration(days: weekday - 1));
return List.generate(7, (i) => monday.add(Duration(days: i)));
}
}
bool isSameDay(DateTime d1, DateTime d2) {
return d1.year == d2.year && d1.month == d2.month && d1.day == d2.day;
}
int _parseHour(String? time, {required int defaultValue}) {
if (time == null || time.isEmpty || !time.contains(':')) {
return defaultValue;
}
try {
return int.parse(time.split(':')[0]);
} catch (e) {
return defaultValue;
}
}

View File

@ -1,59 +0,0 @@
import 'package:flutter/material.dart';
import 'package:syncrow_web/pages/access_management/booking_system/view/widgets/icon_text_button.dart';
import 'package:syncrow_web/utils/constants/assets.dart';
class BookingPage extends StatelessWidget {
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

@ -1,73 +0,0 @@
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/models/bookable_space_config.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/service/send_bookable_spaces_to_api_params.dart';
import 'package:syncrow_web/pages/space_management_v2/main_module/shared/models/paginated_data_model.dart';
class DummyNonNookableSpaces implements NonBookableSpacesService {
@override
Future<PaginatedDataModel<BookableSpacemodel>> load(
NonBookableSpacesParams params) {
return Future.value(PaginatedDataModel<BookableSpacemodel>(
data: [
BookableSpacemodel(
spaceName: 'space3',
spaceConfig: BookableSpaceConfig(
configUuid: 'uuid',
bookableDays: ['wed', 'saturday'],
availability: true,
bookingEndTime: const TimeOfDay(hour: 13, minute: 20),
bookingStartTime: const TimeOfDay(hour: 6, minute: 20),
cost: 6,
),
spaceUuid: 'uuiiddd',
spaceVirtualAddress: 'idvirtual1',
),
BookableSpacemodel(
spaceName: 'space3',
spaceConfig: BookableSpaceConfig(
configUuid: 'uuid',
bookableDays: ['wed', 'saturday', 'thuresday'],
availability: true,
bookingEndTime: const TimeOfDay(hour: 13, minute: 20),
bookingStartTime: const TimeOfDay(hour: 5, minute: 20),
cost: 5,
),
spaceUuid: 'uuiiddd',
spaceVirtualAddress: 'idvirtual2',
),
BookableSpacemodel(
spaceName: 'space3',
spaceConfig: BookableSpaceConfig(
configUuid: 'uuid',
bookableDays: [
'saturday',
'sunday',
'Monday',
'tuesday',
'wed',
'thuresday'
],
availability: true,
bookingEndTime: const TimeOfDay(hour: 13, minute: 20),
bookingStartTime: const TimeOfDay(hour: 15, minute: 20),
cost: 2,
),
spaceUuid: 'uuiiddd',
spaceVirtualAddress: 'idvirtual3',
)
],
page: 1,
size: 1,
hasNext: false,
totalPages: 0,
totalItems: 0,
));
}
@override
Future<void> sendBookableSpacesToApi(
SendBookableSpacesToApiParams params) async {}
}

View File

@ -1,47 +0,0 @@
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

@ -1,74 +0,0 @@
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/service/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

@ -1,40 +0,0 @@
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

@ -1,63 +0,0 @@
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,
bookingEndTime: parseTimeOfDay(json['startTime'] as String),
bookingStartTime: 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

@ -1,58 +0,0 @@
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

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

View File

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

View File

@ -1,24 +0,0 @@
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,
});
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

@ -1,8 +0,0 @@
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

@ -1,10 +0,0 @@
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/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

@ -1,41 +0,0 @@
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

@ -1,6 +0,0 @@
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

@ -1,63 +0,0 @@
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

@ -1,24 +0,0 @@
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

@ -1,28 +0,0 @@
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

@ -1,18 +0,0 @@
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

@ -1,14 +0,0 @@
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

@ -1,136 +0,0 @@
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/service/non_bookable_spaces_service.dart';
import 'package:syncrow_web/pages/access_management/manage_bookable_spaces/domain/service/send_bookable_spaces_to_api_params.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<LoadUnBookableSpacesEvent>(_onLoadUnBookableSpacesEvent);
on<AddToBookableSpaceEvent>(_onAddToBookableSpaceEvent);
on<RemoveFromBookableSpaceEvent>(_onRemoveFromBookableSpaceEvent);
on<SendBookableSpacesToApi>(_onSendBookableSpacesToApi);
on<CheckConfigurValidityEvent>(_onCheckConfigurValidityEvent);
}
TimeOfDay? get endTime =>
selectedBookableSpaces.first.spaceConfig!.bookingEndTime;
TimeOfDay? get startTime =>
selectedBookableSpaces.first.spaceConfig!.bookingStartTime;
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,
),
);
emit(NonBookableSpacesInitial());
} catch (e) {
emit(
NonBookableSpacesError(e.toString()),
);
}
}
void _onCheckConfigurValidityEvent(
CheckConfigurValidityEvent event, Emitter<NonBookableSpacesState> emit) {
if (selectedBookableSpaces.first.spaceConfig!.isValid) {
emit(ValidSaveButtonState());
} else {
emit(UnValidSaveButtonState());
}
}
}

View File

@ -1,33 +0,0 @@
part of 'non_bookaable_spaces_bloc.dart';
sealed class NonBookableSpacesEvent extends Equatable {
const NonBookableSpacesEvent();
@override
List<Object> get props => [];
}
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 {}

View File

@ -1,39 +0,0 @@
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

@ -1,18 +0,0 @@
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 goToNextStep() {
if (state is StepOneState) {
emit(StepTwoState());
}
}
}

View File

@ -1,16 +0,0 @@
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 {}
final class StepEditMode extends StepsState {}

View File

@ -1,35 +0,0 @@
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));
} on APIException catch (e) {
emit(UpdateBookableSpaceFailure(error: e.message));
} catch (e) {
emit(
UpdateBookableSpaceFailure(error: e.toString()),
);
}
}
}

View File

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

View File

@ -1,30 +0,0 @@
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

@ -1,87 +0,0 @@
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

@ -1,89 +0,0 @@
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/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();
SetupBookableSpacesDialog({super.key});
@override
Widget build(BuildContext context) {
return MultiBlocProvider(
providers: [
BlocProvider<StepsCubit>(
create: (context) => StepsCubit()..initDialogValue(),
),
BlocProvider<NonBookableSpacesBloc>(
create: (context) => NonBookableSpacesBloc(
RemoteNonBookableSpaces(HTTPService()),
)..add(
LoadUnBookableSpacesEvent(
nonBookableSpacesParams:
NonBookableSpacesParams(currentPage: 1),
),
),
),
],
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,
),
)
],
),
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);
}),
],
),
),
);
}
}

View File

@ -1,122 +0,0 @@
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/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 {
const BookingPeriodWidget({
super.key,
});
@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: 'Start Time',
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: 'End Time',
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

@ -1,107 +0,0 @@
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/pages/access_management/manage_bookable_spaces/presentation/blocs/steps_cubit/cubit/steps_cubit.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(
'Spaces Added 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

@ -1,48 +0,0 @@
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

@ -1,40 +0,0 @@
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

@ -1,55 +0,0 @@
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

@ -1,33 +0,0 @@
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/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;
const DetailsStepsWidget({
super.key,
required this.pointsController,
});
@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,
);
} else {
return const SizedBox();
}
},
),
);
}
}

View File

@ -1,158 +0,0 @@
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

@ -1,177 +0,0 @@
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/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/widgets/custom_data_table.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: () {},
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

@ -1,75 +0,0 @@
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/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

@ -1,32 +0,0 @@
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

@ -1,125 +0,0 @@
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/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 StatelessWidget {
const PointsPartWidget({
super.key,
required this.pointsController,
});
final TextEditingController pointsController;
@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();
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(
pointsController.text.isEmpty
? '0'
: pointsController.text,
),
);
context
.read<NonBookableSpacesBloc>()
.add(CheckConfigurValidityEvent());
},
controller: pointsController,
inputFormatters: [FilteringTextInputFormatter.digitsOnly],
suffix: const SizedBox(),
)
else
const SizedBox(),
],
);
},
);
}
}

View File

@ -1,40 +0,0 @@
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/widgets/buttons_divider_bottom_dialog_widget.dart';
class SaveSecondStepButton extends StatelessWidget {
final List<BookableSpacemodel> selectedSpaces;
final TextEditingController pointsController;
const SaveSecondStepButton({
super.key,
required this.selectedSpaces,
required this.pointsController,
});
@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,
)) {
context.read<NonBookableSpacesBloc>().add(
SendBookableSpacesToApi(),
);
}
},
onCancelPressed: () => context.pop(),
);
},
);
}
}

View File

@ -1,60 +0,0 @@
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

@ -1,181 +0,0 @@
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

@ -1,38 +0,0 @@
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.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;
const StepTwoDetailsWidget({
super.key,
required this.pointsController,
});
@override
Widget build(BuildContext context) {
return SizedBox(
width: 450,
height: 480,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const WeekDaysCheckboxRow(),
const SizedBox(
height: 20,
),
const BookingPeriodWidget(),
const SizedBox(
height: 20,
),
BlocProvider(
create: (context) => TogglePointsSwitchCubit()..activateSwitch(),
child: PointsPartWidget(pointsController: pointsController),
)
],
),
);
}
}

View File

@ -1,127 +0,0 @@
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 if (state is StepEditMode) {
return 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

@ -1,71 +0,0 @@
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_svg/flutter_svg.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';
import 'package:syncrow_web/utils/constants/assets.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

@ -1,52 +0,0 @@
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

@ -1,66 +0,0 @@
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';
class WeekDaysCheckboxRow extends StatefulWidget {
const WeekDaysCheckboxRow({super.key});
@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
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,8 +2,7 @@ import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:syncrow_web/pages/access_management/bloc/access_bloc.dart';
import 'package:syncrow_web/pages/access_management/bloc/access_event.dart';
import 'package:syncrow_web/pages/access_management/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';
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/utils/extension/build_context_x.dart';
@ -72,14 +71,9 @@ class _AccessManagementPageState extends State<AccessManagementPage>
scaffoldBody: PageView(
controller: _pageController,
physics: const NeverScrollableScrollPhysics(),
children: [
const AccessOverviewContent(),
BookingPage(
pageController: _pageController,
),
ManageBookableSpacesPage(
pageController: _pageController,
),
children: const [
AccessOverviewContent(),
BookingPage(),
],
),
),

View File

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

View File

@ -153,6 +153,7 @@ class EditUserModel {
final String? jobTitle; // can be empty
final String roleType; // e.g. "ADMIN"
final List<UserSpaceModel> spaces;
final String? companyName;
EditUserModel({
required this.uuid,
@ -167,6 +168,7 @@ class EditUserModel {
required this.jobTitle,
required this.roleType,
required this.spaces,
this.companyName,
});
/// Create a [UserData] from JSON data
@ -182,6 +184,7 @@ class EditUserModel {
invitedBy: json['invitedBy'] as String,
phoneNumber: json['phoneNumber'] ?? '',
jobTitle: json['jobTitle'] ?? '',
companyName: json['companyName'] as String?,
roleType: json['roleType'] as String,
spaces: (json['spaces'] as List<dynamic>)
.map((e) => UserSpaceModel.fromJson(e as Map<String, dynamic>))

View File

@ -12,7 +12,7 @@ class RolesUserModel {
final dynamic jobTitle;
final dynamic createdDate;
final dynamic createdTime;
final String? companyName;
RolesUserModel({
required this.uuid,
required this.createdAt,
@ -27,6 +27,7 @@ class RolesUserModel {
this.jobTitle,
required this.createdDate,
required this.createdTime,
this.companyName,
});
factory RolesUserModel.fromJson(Map<String, dynamic> json) {
@ -47,6 +48,7 @@ class RolesUserModel {
: json['jobTitle'],
createdDate: json['createdDate'],
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 emailController = TextEditingController();
final TextEditingController phoneController = TextEditingController();
final TextEditingController jobTitleController = TextEditingController();
final TextEditingController companyNameController = TextEditingController();
final TextEditingController roleSearchController = TextEditingController();
bool? isCompleteBasics;
@ -352,7 +352,7 @@ class UsersBloc extends Bloc<UsersEvent, UsersState> {
bool res = await UserPermissionApi().sendInviteUser(
email: emailController.text,
firstName: firstNameController.text,
jobTitle: jobTitleController.text,
companyName: companyNameController.text,
lastName: lastNameController.text,
phoneNumber: phoneController.text,
roleUuid: roleSelected,
@ -405,7 +405,7 @@ class UsersBloc extends Bloc<UsersEvent, UsersState> {
bool res = await UserPermissionApi().editInviteUser(
userId: event.userId,
firstName: firstNameController.text,
jobTitle: jobTitleController.text,
companyName: companyNameController.text,
lastName: lastNameController.text,
phoneNumber: phoneController.text,
roleUuid: roleSelected,
@ -529,7 +529,7 @@ class UsersBloc extends Bloc<UsersEvent, UsersState> {
lastNameController.text = res.lastName;
emailController.text = res.email;
phoneController.text = res.phoneNumber ?? '';
jobTitleController.text = res.jobTitle ?? '';
companyNameController.text = res.companyName ?? '';
res.roleType;
res.spaces.map((space) {
selectedIds.add(space.uuid);
@ -645,7 +645,7 @@ class UsersBloc extends Bloc<UsersEvent, UsersState> {
lastNameController.dispose();
emailController.dispose();
phoneController.dispose();
jobTitleController.dispose();
companyNameController.dispose();
roleSearchController.dispose();
return super.close();
}

View File

@ -317,7 +317,7 @@ class BasicsView extends StatelessWidget {
child: Row(
children: [
Text(
'Job Title',
'Company Name',
style: context.textTheme.bodyMedium?.copyWith(
fontWeight: FontWeight.w400,
fontSize: 13,
@ -328,11 +328,11 @@ class BasicsView extends StatelessWidget {
Padding(
padding: const EdgeInsets.all(8.0),
child: TextFormField(
controller: _blocRole.jobTitleController,
controller: _blocRole.companyNameController,
style:
const TextStyle(color: ColorsManager.blackColor),
decoration: inputTextFormDeco(
hintText: "Job Title (Optional)")
hintText: "Comapny Name (Optional)")
.copyWith(
hintStyle: context.textTheme.bodyMedium?.copyWith(
fontWeight: FontWeight.w400,

View File

@ -411,7 +411,7 @@ class UsersPage extends StatelessWidget {
titles: const [
"Full Name",
"Email Address",
"Job Title",
"Company Name",
"Role",
"Creation Date",
"Creation Time",
@ -424,7 +424,7 @@ class UsersPage extends StatelessWidget {
return [
Text('${user.firstName} ${user.lastName}'),
Text(user.email),
Text(user.jobTitle),
Center(child: Text(user.companyName ?? '-')),
Text(user.roleType ?? ''),
Text(user.createdDate ?? ''),
Text(user.createdTime ?? ''),

View File

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

View File

@ -40,22 +40,4 @@ class PaginatedDataModel<T> extends Equatable {
totalItems,
totalPages,
];
PaginatedDataModel<T> copyWith({
List<T>? data,
int? page,
int? size,
bool? hasNext,
int? totalItems,
int? totalPages,
}) {
return PaginatedDataModel<T>(
data: data ?? this.data,
page: page ?? this.page,
size: size ?? this.size,
hasNext: hasNext ?? this.hasNext,
totalItems: totalItems ?? this.totalItems,
totalPages: totalPages ?? this.totalPages,
);
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,6 +1,7 @@
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:syncrow_web/pages/space_management_v2/main_module/widgets/community_structure_canvas.dart';
import 'package:syncrow_web/pages/space_management_v2/main_module/widgets/community_structure_header.dart';
import 'package:syncrow_web/pages/space_management_v2/main_module/widgets/create_space_button.dart';
import 'package:syncrow_web/pages/space_management_v2/modules/communities/presentation/communities_tree_selection_bloc/communities_tree_selection_bloc.dart';
@ -18,10 +19,18 @@ class SpaceManagementCommunityStructure extends StatelessWidget {
replacement: const Row(
children: [spacer, Expanded(child: CreateSpaceButton()), spacer],
),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
const CommunityStructureHeader(),
Expanded(
child: CommunityStructureCanvas(
community: selectedCommunity,
selectedSpace: selectedSpace,
),
),
],
),
);
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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