Compare commits

..

68 Commits

Author SHA1 Message Date
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
138 changed files with 4654 additions and 2201 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

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

View File

@ -1,61 +0,0 @@
import 'package:flutter/material.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';
class DummyBookableSpacesService implements BookableSpacesService {
@override
Future<PaginatedDataModel<BookableSpacemodel>> load(
BookableSpacesParams param) async {
return PaginatedDataModel(
data: [
BookableSpacemodel(
spaceName: 'space1',
spaceConfig: BookableSpaceConfig(
configUuid: 'uuid',
bookableDays: ['wed', 'saturday'],
availability: true,
bookingEndTime: const TimeOfDay(hour: 13, minute: 20),
bookingStartTime: const TimeOfDay(hour: 13, minute: 20),
cost: 6,
),
spaceUuid: 'uuiiddd',
spaceVirtualAddress: 'idvirtual1',
),
BookableSpacemodel(
spaceName: 'space2',
spaceConfig: BookableSpaceConfig(
configUuid: 'uuid',
bookableDays: ['wed', 'thur'],
availability: true,
bookingEndTime: const TimeOfDay(hour: 13, minute: 20),
bookingStartTime: const TimeOfDay(hour: 13, minute: 20),
cost: 6,
),
spaceUuid: 'uuiiddd',
spaceVirtualAddress: 'idvirtual1',
),
BookableSpacemodel(
spaceName: 'space3',
spaceConfig: BookableSpaceConfig(
configUuid: 'uuid',
bookableDays: ['wed', 'fri', 'tues'],
availability: true,
bookingEndTime: const TimeOfDay(hour: 13, minute: 20),
bookingStartTime: const TimeOfDay(hour: 13, minute: 20),
cost: 6,
),
spaceUuid: 'uuiiddd',
spaceVirtualAddress: 'idvirtual1',
)
],
page: 1,
size: 1,
hasNext: false,
totalItems: 3,
totalPages: 1,
);
}
}

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,45 +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 tags';
@override
Future<PaginatedDataModel<BookableSpacemodel>> load(
BookableSpacesParams param) async {
try {
final response = await _httpService.get(
//TODO: you have to Chage this API call Path
path: ApiEndpoints.listTags,
//*************|********** */
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,71 +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(
//TODO: you have to Chage this API call Path
path: ApiEndpoints.listTags,
//*************|********** */
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.addBookableSpaces,
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,45 +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,
required this.bookingEndTime,
required this.bookingStartTime,
required this.cost,
});
factory BookableSpaceConfig.zero() => BookableSpaceConfig(
configUuid: '',
bookableDays: [],
availability: false,
bookingEndTime: TimeOfDay.now(),
bookingStartTime: TimeOfDay.now(),
cost: -1,
);
factory BookableSpaceConfig.fromJson(Map<String, dynamic> json) =>
BookableSpaceConfig(
configUuid: json['uuid'] as String,
bookableDays: json['daysAvailable'] as List<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 =>
configUuid.isNotEmpty && bookableDays.isNotEmpty && cost > 0;
}

View File

@ -1,42 +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,
required this.spaceConfig,
required this.spaceVirtualAddress,
});
factory BookableSpacemodel.zero() => BookableSpacemodel(
spaceUuid: '',
spaceName: '',
spaceConfig: BookableSpaceConfig.zero(),
spaceVirtualAddress: '',
);
factory BookableSpacemodel.fromJson(Map<String, dynamic> json) =>
BookableSpacemodel(
spaceUuid: json['uuid'] as String,
spaceName: json['spaceName'] as String,
spaceConfig: BookableSpaceConfig.fromJson(
json['bookableConfig'] as Map<String, dynamic>),
spaceVirtualAddress: json['spaceVirtualAddress'] 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.isValid;
}

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,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,34 +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_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);
}
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()),
);
}
}
}

View File

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

View File

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

View File

@ -1,99 +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);
}
TimeOfDay get endTime =>
selectedBookableSpaces.first.spaceConfig.bookingEndTime;
TimeOfDay get startTime =>
selectedBookableSpaces.first.spaceConfig.bookingStartTime;
Future<void> _onLoadUnBookableSpacesEvent(LoadUnBookableSpacesEvent event,
Emitter<NonBookableSpacesState> emit) async {
emit(NonBookableSpacesLoading());
try {
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;
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;
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(NonBookableSpacesLoading());
try {
await nonBookableSpacesService.sendBookableSpacesToApi(
SendBookableSpacesToApiParams.fromBookableSpacesModel(
selectedBookableSpaces,
),
);
} catch (e) {
emit(
NonBookableSpacesError(e.toString()),
);
}
}
}

View File

@ -1,31 +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 {}

View File

@ -1,26 +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 {}
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);
}

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,201 +0,0 @@
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_svg/svg.dart';
import 'package:go_router/go_router.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/data/dummy_bookable_spaces_service.dart';
import 'package:syncrow_web/pages/access_management/manage_bookable_spaces/domain/models/bookable_space_model.dart';
import 'package:syncrow_web/pages/access_management/manage_bookable_spaces/domain/params/bookable_spaces_params.dart';
import 'package:syncrow_web/pages/access_management/manage_bookable_spaces/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/pages/access_management/manage_bookable_spaces/presentation/widgets/custom_data_table.dart';
import 'package:syncrow_web/pages/device_managment/shared/navigate_home_grid_view.dart';
import 'package:syncrow_web/utils/constants/assets.dart';
import 'package:syncrow_web/utils/extension/build_context_x.dart';
import 'package:syncrow_web/utils/theme/responsive_text_theme.dart';
import 'package:syncrow_web/web_layout/web_scaffold.dart';
class ManageBookableSpacesPage extends StatefulWidget {
const ManageBookableSpacesPage({super.key});
@override
State<ManageBookableSpacesPage> createState() =>
_ManageBookableSpacesPageState();
}
class _ManageBookableSpacesPageState extends State<ManageBookableSpacesPage> {
final PageController _pageController = PageController(initialPage: 1);
int _currentPageIndex = 1;
@override
Widget build(BuildContext context) {
return WebScaffold(
enableMenuSidebar: false,
appBarTitle: Text(
'Access Management',
style: ResponsiveTextTheme.of(context).deviceManagementTitle,
),
centerBody: Row(
mainAxisSize: MainAxisSize.min,
children: [
TextButton(
onPressed: () => _switchPage(0),
child: Text(
'Access Overview',
style: context.textTheme.titleMedium?.copyWith(
color: _currentPageIndex == 0 ? Colors.white : Colors.grey,
fontWeight:
_currentPageIndex == 0 ? FontWeight.w700 : FontWeight.w400,
),
),
),
TextButton(
onPressed: () => _switchPage(1),
child: Text(
'Booking System',
style: context.textTheme.titleMedium?.copyWith(
color: _currentPageIndex == 1 ? Colors.white : Colors.grey,
fontWeight:
_currentPageIndex == 1 ? FontWeight.w700 : FontWeight.w400,
),
),
),
],
),
rightBody: const NavigateHomeGridView(),
scaffoldBody: BlocProvider(
create: (context) => BookableSpacesBloc(
DummyBookableSpacesService(),
// RemoteBookableSpacesService(HTTPService()),
)..add(LoadBookableSpacesEvent(
BookableSpacesParams(currentPage: 1),
)),
child: const ManageBookableSpacesWidget(),
),
);
}
void _switchPage(int index) {
setState(() => _currentPageIndex = index);
_pageController.jumpToPage(index);
}
}
class ManageBookableSpacesWidget extends StatelessWidget {
const ManageBookableSpacesWidget({super.key});
@override
Widget build(BuildContext context) {
return Container(
padding: const EdgeInsets.all(20),
child: Column(
children: [
Expanded(
flex: 1,
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
SvgTextButton(
svgAsset: Assets.backButtonIcon,
label: 'Booking Home',
onPressed: () {
context.pop();
}),
SvgTextButton(
svgAsset: Assets.backButtonIcon,
label: 'Set Up a Bookable Spaces',
onPressed: () async => showDialog(
context: context,
builder: (context) => SetupBookableSpacesDialog(),
),
)
],
)),
Expanded(
flex: 9,
child: BlocBuilder<BookableSpacesBloc, BookableSpacesState>(
builder: (context, state) {
if (state is BookableSpacesLoading) {
return const CircularProgressIndicator();
} else if (state is BookableSpacesError) {
return Text(state.error);
} 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)),
),
DataCell(Padding(
padding: const EdgeInsetsGeometry.only(left: 10),
child: Text(space.spaceVirtualAddress))),
DataCell(SizedBox(
width: 200,
child: Wrap(
spacing: 4,
children: space.spaceConfig.bookableDays
.map((day) => Text(
day,
style: const TextStyle(fontSize: 12),
))
.toList(),
),
)),
DataCell(
Padding(
padding: const EdgeInsetsGeometry.only(left: 10),
child: Text(
space.spaceConfig.bookingStartTime.format(context),
),
),
),
DataCell(
Padding(
padding: const EdgeInsetsGeometry.only(left: 10),
child: Text(
space.spaceConfig.bookingEndTime.format(context),
),
),
),
DataCell(Padding(
padding: const EdgeInsetsGeometry.only(left: 10),
child: Text('${space.spaceConfig.cost} Points'))),
DataCell(Center(
child: Transform.scale(
scale: 0.7,
child: Switch(
value: space.spaceConfig.availability,
onChanged: (value) {},
),
),
)),
DataCell(Center(
child: ElevatedButton(
onPressed: () {},
child: SvgPicture.asset(Assets.settings),
),
)),
],
columnsTitles: const [
'space',
'space Virtual Address',
'Bookable Days',
'Booking Start Time',
'Booking End Time',
'Cost',
'Availability',
'Settings',
],
);
} else {
return const SizedBox();
}
},
),
)
],
),
);
}
}

View File

@ -1,146 +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/data/dummy_non_nookable_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/buttons_divider_bottom_dialog_widget.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';
import 'package:syncrow_web/pages/access_management/manage_bookable_spaces/presentation/widgets/stepper_part_widget.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(
DummyNonNookableSpaces(),
)..add(
LoadUnBookableSpacesEvent(
nonBookableSpacesParams:
NonBookableSpacesParams(currentPage: 1),
),
),
),
],
child: AlertDialog(
contentPadding: EdgeInsets.zero,
title: 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) {
return ButtonsDividerBottomDialogWidget(
onNextPressed: () {
final stepsState = context.read<StepsCubit>().state;
final selectedSpaces = context
.read<NonBookableSpacesBloc>()
.selectedBookableSpaces;
if (stepsState is StepOneState) {
if (selectedSpaces.isNotEmpty) {
context.read<StepsCubit>().goToNextStep();
} else {
ScaffoldMessenger.of(context).clearSnackBars();
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('Please select at least one space.'),
),
);
}
} else if (stepsState is StepTwoState) {
selectedSpaces.forEach(
(e) => e.spaceConfig.cost = int.parse(
pointsController.text.isEmpty
? '0'
: pointsController.text),
);
if (selectedSpaces.any(
(element) => !element.isValid,
)) {
ScaffoldMessenger.of(context).clearSnackBars();
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('Please fill the required fields.'),
),
);
} else {
print(selectedSpaces.first.spaceUuid);
}
}
},
onCancelPressed: () => context.pop(),
);
}),
],
),
),
);
}
}
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 SpacesStepDetailsWidget();
} else if (state is StepTwoState) {
return StepTwoDetailsWidget(
pointsController: pointsController,
);
} else {
return const SizedBox();
}
},
),
);
}
}

View File

@ -1,88 +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 ButtonsDividerBottomDialogWidget extends StatelessWidget {
final void Function() onNextPressed;
final void Function() onCancelPressed;
const ButtonsDividerBottomDialogWidget({
super.key,
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.grayBorder),
),
),
),
),
Expanded(
child: BlocBuilder<StepsCubit, StepsState>(
builder: (context, state) {
return InkWell(
borderRadius: const BorderRadius.only(
bottomRight: Radius.circular(26),
),
onTap: onNextPressed,
child: Container(
height: 40,
alignment: Alignment.center,
decoration: const BoxDecoration(
border: Border(
right: BorderSide(
color: ColorsManager.grayBorder,
),
),
borderRadius: BorderRadius.only(
bottomRight: Radius.circular(26),
),
),
child: Text(
state is StepOneState ? 'Next' : 'Save',
style: const TextStyle(
color: ColorsManager.blueColor,
),
),
),
);
},
),
)
],
)
],
);
}
}

View File

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

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,59 +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,
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,166 +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/models/bookable_space_model.dart';
import 'package:syncrow_web/pages/access_management/manage_bookable_spaces/domain/params/non_bookable_spaces_params.dart';
import 'package:syncrow_web/pages/access_management/manage_bookable_spaces/presentation/blocs/non_bookable_spaces_bloc/non_bookaable_spaces_bloc.dart';
import 'package:syncrow_web/pages/access_management/manage_bookable_spaces/presentation/widgets/search_unbookable_spaces_widget.dart';
import 'package:syncrow_web/utils/color_manager.dart';
class SpacesStepDetailsWidget extends StatelessWidget {
SpacesStepDetailsWidget({
super.key,
});
Timer? _debounce;
@override
Widget build(BuildContext context) {
return BlocBuilder<NonBookableSpacesBloc, NonBookableSpacesState>(
builder: (context, state) {
if (state is NonBookableSpacesLoading) {
return const Center(child: CircularProgressIndicator());
} else if (state is NonBookableSpacesError) {
return Text(state.error);
} else if (state is NonBookableSpacesLoaded) {
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(0, 4),
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), () {
context.read<NonBookableSpacesBloc>().add(
LoadUnBookableSpacesEvent(
nonBookableSpacesParams:
NonBookableSpacesParams(
currentPage: 1,
searchedWords: p0,
),
),
);
});
},
),
),
Expanded(
child: 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,
),
itemCount: state.nonBookableSpaces.data.length,
itemBuilder: (context, index) => CheckBoxSpaceWidget(
nonBookableSpace:
state.nonBookableSpaces.data[index],
),
),
),
)
],
),
)
],
);
} else {
return const SizedBox();
}
},
);
}
}
class CheckBoxSpaceWidget extends StatefulWidget {
final BookableSpacemodel nonBookableSpace;
const CheckBoxSpaceWidget({
super.key,
required this.nonBookableSpace,
});
@override
State<CheckBoxSpaceWidget> createState() => _CheckBoxSpaceWidgetState();
}
class _CheckBoxSpaceWidgetState extends State<CheckBoxSpaceWidget> {
bool isChecked = false;
@override
Widget build(BuildContext context) {
return Row(
children: [
Checkbox(
value: isChecked,
onChanged: (value) => setState(() {
isChecked = value ?? false;
if (isChecked) {
context.read<NonBookableSpacesBloc>().add(
AddToBookableSpaceEvent(
nonBookableSpace: widget.nonBookableSpace,
),
);
} else {
context.read<NonBookableSpacesBloc>().add(
RemoveFromBookableSpaceEvent(
bookableSpace: widget.nonBookableSpace,
),
);
}
}),
),
const SizedBox(
width: 5,
),
Text(widget.nonBookableSpace.spaceName),
],
);
}
}

View File

@ -1,151 +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/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/time_picker_widget.dart';
import 'package:syncrow_web/pages/access_management/manage_bookable_spaces/presentation/widgets/week_checkbox_title_widget.dart';
import 'package:syncrow_web/utils/color_manager.dart';
import 'package:syncrow_web/utils/string_utils.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,
),
Row(
children: [
TitleAndTimePickerWidget(
title: 'Booking Start Time',
onTimePicked: (timePicked) {
if (timePicked == null) {
return;
}
final nonBookableBloc = context.read<NonBookableSpacesBloc>();
if (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 SizedBox(
width: 20,
),
TitleAndTimePickerWidget(
title: 'Booking End Time',
onTimePicked: (timePicked) {
if (timePicked == null) {
return;
}
final nonBookableBloc = context.read<NonBookableSpacesBloc>();
if (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,
);
}
},
)
],
),
const SizedBox(
height: 20,
),
Row(
mainAxisAlignment: MainAxisAlignment.start,
children: [
Text(
'* ',
style: Theme.of(context)
.textTheme
.bodyMedium!
.copyWith(color: Colors.red),
),
const Text('Points/hrs'),
],
),
const SizedBox(
height: 5,
),
SearchUnbookableSpacesWidget(
title: 'Ex: 0',
controller: pointsController,
inputFormatters: [FilteringTextInputFormatter.digitsOnly],
suffix: const SizedBox(),
),
],
),
);
}
}
class TitleAndTimePickerWidget extends StatelessWidget {
final void Function(TimeOfDay? timePicked) onTimePicked;
final String title;
const TitleAndTimePickerWidget({
super.key,
required this.onTimePicked,
required this.title,
});
@override
Widget build(BuildContext context) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.start,
children: [
Text(
'* ',
style: Theme.of(context)
.textTheme
.bodyMedium!
.copyWith(color: Colors.red),
),
Text(title),
],
),
const SizedBox(
height: 5,
),
TimePickerWidget(
onTimePicked: onTimePicked,
),
],
);
}
}

View File

@ -1,117 +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(
alignment: Alignment.centerLeft,
height: 50,
child: const VerticalDivider(
width: 8,
)),
const CircleTitleStepperWidget(
title: 'Settings',
titleColor: ColorsManager.softGray,
circleColor: ColorsManager.softGray,
)
],
);
} 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: 3,
),
Container(
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? titleColor;
final String title;
const CircleTitleStepperWidget({
super.key,
required this.title,
this.circleColor,
this.cicleIcon,
this.titleColor,
this.radius,
});
@override
Widget build(BuildContext context) {
return Row(
children: [
CircleAvatar(
minRadius: radius ?? 5,
backgroundColor: circleColor ?? ColorsManager.blue1,
child: cicleIcon,
),
const SizedBox(
width: 10,
),
Text(
title,
style: TextStyle(
fontWeight: FontWeight.w700,
color: titleColor ?? ColorsManager.blackColor,
),
),
],
);
}
}

View File

@ -1,83 +0,0 @@
import 'package:flutter/material.dart';
import 'package:flutter_svg/flutter_svg.dart';
import 'package:syncrow_web/utils/color_manager.dart';
import 'package:syncrow_web/utils/constants/assets.dart';
class TimePickerWidget extends StatefulWidget {
const TimePickerWidget({
super.key,
required this.onTimePicked,
});
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;
setState(() {});
},
child: Row(
children: [
Container(
width: 56,
height: 32,
decoration: const BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.only(
topLeft: Radius.circular(10),
bottomLeft: Radius.circular(10),
),
),
alignment: Alignment.center,
child: SvgPicture.asset(Assets.clockIcon),
),
Container(
width: 120,
height: 32,
decoration: const BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.only(
topRight: Radius.circular(10),
bottomRight: Radius.circular(10),
),
),
alignment: Alignment.centerLeft,
padding: const EdgeInsets.symmetric(horizontal: 8),
child: Text(
timePicked == null
? TimeOfDay.now().format(context)
: timePicked!.format(context),
style: const TextStyle(color: Color(0xB2D5D5D5)),
),
),
],
),
);
}
}

View File

@ -1,62 +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;
}
});
},
),
),
Expanded(
child: Text(
entry.key,
style: const TextStyle(fontSize: 10),
)),
],
),
);
}).toList(),
);
}
}

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_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/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';

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

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

View File

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

View File

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

View File

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

View File

@ -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',
);
onCreateCommunity.call(community);
context.read<CommunitiesBloc>().add(
InsertCommunity(community),
);
context.read<CommunitiesTreeSelectionBloc>().add(
SelectCommunityEvent(community: community),
);
break;
case CreateCommunityFailure():
Navigator.of(context).pop();
break;
default:
break;
}
},
child: CreateCommunityDialogWidget(
title: title,
initialName: initialName,
),
builder: (BuildContext context, CreateCommunityState state) {
return CommunityDialog(
title: const Text('Create Community'),
initialName: null,
onSubmit: (name) => context.read<CreateCommunityBloc>().add(
CreateCommunity(CreateCommunityParam(name: name)),
),
errorMessage: state is CreateCommunityFailure ? state.message : null,
);
},
),
);
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -0,0 +1,27 @@
import 'package:syncrow_web/pages/space_management_v2/modules/space_details/domain/models/space_details_model.dart';
import 'package:syncrow_web/pages/space_management_v2/modules/space_details/domain/params/load_space_details_param.dart';
import 'package:syncrow_web/pages/space_management_v2/modules/space_details/domain/services/space_details_service.dart';
class UniqueSubspacesDecorator implements SpaceDetailsService {
final SpaceDetailsService _decoratee;
const UniqueSubspacesDecorator(this._decoratee);
@override
Future<SpaceDetailsModel> getSpaceDetails(LoadSpaceDetailsParam param) async {
final response = await _decoratee.getSpaceDetails(param);
final uniqueSubspaces = <String, Subspace>{};
for (final subspace in response.subspaces) {
final normalizedName = subspace.name.trim().toLowerCase();
if (!uniqueSubspaces.containsKey(normalizedName)) {
uniqueSubspaces[normalizedName] = subspace;
}
}
return response.copyWith(
subspaces: uniqueSubspaces.values.toList(),
);
}
}

View File

@ -1,6 +1,8 @@
import 'package:equatable/equatable.dart';
import 'package:syncrow_web/pages/space_management_v2/modules/products/domain/models/product.dart';
import 'package:syncrow_web/pages/space_management_v2/modules/tags/domain/models/tag.dart';
import 'package:syncrow_web/utils/constants/assets.dart';
import 'package:uuid/uuid.dart';
class SpaceDetailsModel extends Equatable {
final String uuid;
@ -17,6 +19,13 @@ class SpaceDetailsModel extends Equatable {
required this.subspaces,
});
factory SpaceDetailsModel.empty() => const SpaceDetailsModel(
uuid: '',
spaceName: '',
icon: Assets.location,
productAllocations: [],
subspaces: [],
);
factory SpaceDetailsModel.fromJson(Map<String, dynamic> json) {
return SpaceDetailsModel(
uuid: json['uuid'] as String,
@ -41,23 +50,40 @@ class SpaceDetailsModel extends Equatable {
};
}
SpaceDetailsModel copyWith({
String? uuid,
String? spaceName,
String? icon,
List<ProductAllocation>? productAllocations,
List<Subspace>? subspaces,
}) {
return SpaceDetailsModel(
uuid: uuid ?? this.uuid,
spaceName: spaceName ?? this.spaceName,
icon: icon ?? this.icon,
productAllocations: productAllocations ?? this.productAllocations,
subspaces: subspaces ?? this.subspaces,
);
}
@override
List<Object?> get props => [uuid, spaceName, icon, productAllocations, subspaces];
}
class ProductAllocation extends Equatable {
final String uuid;
final Product product;
final Tag tag;
final String? location;
const ProductAllocation({
required this.uuid,
required this.product,
required this.tag,
this.location,
});
factory ProductAllocation.fromJson(Map<String, dynamic> json) {
return ProductAllocation(
uuid: json['uuid'] as String? ?? const Uuid().v4(),
product: Product.fromJson(json['product'] as Map<String, dynamic>),
tag: Tag.fromJson(json['tag'] as Map<String, dynamic>),
);
@ -65,13 +91,26 @@ class ProductAllocation extends Equatable {
Map<String, dynamic> toJson() {
return {
'uuid': uuid,
'product': product.toJson(),
'tag': tag.toJson(),
};
}
ProductAllocation copyWith({
String? uuid,
Product? product,
Tag? tag,
}) {
return ProductAllocation(
uuid: uuid ?? this.uuid,
product: product ?? this.product,
tag: tag ?? this.tag,
);
}
@override
List<Object?> get props => [product, tag];
List<Object?> get props => [uuid, product, tag];
}
class Subspace extends Equatable {
@ -88,7 +127,7 @@ class Subspace extends Equatable {
factory Subspace.fromJson(Map<String, dynamic> json) {
return Subspace(
uuid: json['uuid'] as String,
name: json['name'] as String,
name: json['subspaceName'] as String,
productAllocations: (json['productAllocations'] as List)
.map((e) => ProductAllocation.fromJson(e as Map<String, dynamic>))
.toList(),
@ -103,6 +142,18 @@ class Subspace extends Equatable {
};
}
Subspace copyWith({
String? uuid,
String? name,
List<ProductAllocation>? productAllocations,
}) {
return Subspace(
uuid: uuid ?? this.uuid,
name: name ?? this.name,
productAllocations: productAllocations ?? this.productAllocations,
);
}
@override
List<Object?> get props => [uuid, name, productAllocations];
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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