Compare commits

..

40 Commits

Author SHA1 Message Date
df29aab111 Setup new firebase project in the web platform. 2025-07-10 12:18:45 +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
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
119 changed files with 1944 additions and 3231 deletions

View File

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

View File

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

View File

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

34
.vscode/launch.json vendored
View File

@ -1,14 +1,9 @@
{
"configurations": [
{
"name": "DEVELOPMENT",
"request": "launch",
"type": "dart",
"args": [
"-d",
"chrome",
@ -16,19 +11,14 @@
"3000",
"-t",
"lib/main_dev.dart",
"--web-experimental-hot-reload",
"--web-experimental-hot-reload"
],
"flutterMode": "debug"
},{
},
{
"name": "STAGING",
"request": "launch",
"type": "dart",
"args": [
"-d",
"chrome",
@ -36,19 +26,14 @@
"3000",
"-t",
"lib/main_staging.dart",
"--web-experimental-hot-reload",
"--web-experimental-hot-reload"
],
"flutterMode": "debug"
},{
},
{
"name": "PRODUCTION",
"request": "launch",
"type": "dart",
"args": [
"-d",
"chrome",
@ -56,12 +41,9 @@
"3000",
"-t",
"lib/main.dart",
"--web-experimental-hot-reload",
"--web-experimental-hot-reload"
],
"flutterMode": "debug"
},
}
]
}

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

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

16
lib/firebase_options.dart Normal file
View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,14 +1,9 @@
import 'package:flutter/material.dart';
import 'package:syncrow_web/pages/access_management/booking_system/view/widgets/icon_text_button.dart';
import 'package:syncrow_web/utils/constants/assets.dart';
class BookingPage extends StatelessWidget {
final PageController pageController;
const BookingPage({
super.key,
required this.pageController,
});
const BookingPage({super.key});
@override
Widget build(BuildContext context) {
@ -38,10 +33,8 @@ class BookingPage extends StatelessWidget {
SvgTextButton(
svgAsset: Assets.homeIcon,
label: 'Manage Bookable Spaces',
onPressed: () {
pageController.jumpToPage(2);
}),
const SizedBox(width: 20),
onPressed: () {}),
SizedBox(width: 20),
SvgTextButton(
svgAsset: Assets.groupIcon,
label: 'Manage Users',

View File

@ -12,13 +12,10 @@ class SvgTextButton extends StatelessWidget {
final double borderRadius;
final List<BoxShadow> boxShadow;
final double svgSize;
final double? fontSize;
final FontWeight? fontWeight;
const SvgTextButton({
super.key,
required this.svgAsset,
this.fontSize,
this.fontWeight,
required this.label,
required this.onPressed,
this.backgroundColor = ColorsManager.circleRolesBackground,
@ -63,8 +60,8 @@ class SvgTextButton extends StatelessWidget {
label,
style: TextStyle(
color: labelColor,
fontSize: fontSize ?? 16,
fontWeight: fontWeight ?? FontWeight.w500,
fontSize: 16,
fontWeight: FontWeight.w500,
),
),
],

View File

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

View File

@ -1,47 +0,0 @@
import 'package:dio/dio.dart';
import 'package:syncrow_web/pages/access_management/manage_bookable_spaces/domain/models/bookable_space_model.dart';
import 'package:syncrow_web/pages/access_management/manage_bookable_spaces/domain/params/bookable_spaces_params.dart';
import 'package:syncrow_web/pages/access_management/manage_bookable_spaces/domain/service/bookable_spaces_service.dart';
import 'package:syncrow_web/pages/space_management_v2/main_module/shared/models/paginated_data_model.dart';
import 'package:syncrow_web/services/api/api_exception.dart';
import 'package:syncrow_web/services/api/http_service.dart';
import 'package:syncrow_web/utils/constants/api_const.dart';
class RemoteBookableSpacesService implements BookableSpacesService {
final HTTPService _httpService;
RemoteBookableSpacesService(this._httpService);
static const _defaultErrorMessage = 'Failed to load Bookable Spaces';
@override
Future<PaginatedDataModel<BookableSpacemodel>> load(
BookableSpacesParams param) async {
try {
final response = await _httpService.get(
path: ApiEndpoints.bookableSpaces,
queryParameters: {
'configured': true,
'page': param.currentPage,
},
expectedResponseModel: (json) {
final result = json as Map<String, dynamic>;
return PaginatedDataModel.fromJson(
result,
BookableSpacemodel.fromJsonList,
);
},
);
return response;
} on DioException catch (e) {
final message = e.response?.data as Map<String, dynamic>?;
final error = message?['error'] as Map<String, dynamic>?;
final errorMessage = error?['error'] as String? ?? '';
final formattedErrorMessage = [
_defaultErrorMessage,
errorMessage,
].join(': ');
throw APIException(formattedErrorMessage);
} catch (e) {
final formattedErrorMessage = [_defaultErrorMessage, '$e'].join(': ');
throw APIException(formattedErrorMessage);
}
}
}

View File

@ -1,74 +0,0 @@
import 'package:dio/dio.dart';
import 'package:syncrow_web/pages/access_management/manage_bookable_spaces/domain/models/bookable_space_model.dart';
import 'package:syncrow_web/pages/access_management/manage_bookable_spaces/domain/params/non_bookable_spaces_params.dart';
import 'package:syncrow_web/pages/access_management/manage_bookable_spaces/domain/service/non_bookable_spaces_service.dart';
import 'package:syncrow_web/pages/access_management/manage_bookable_spaces/domain/service/send_bookable_spaces_to_api_params.dart';
import 'package:syncrow_web/pages/space_management_v2/main_module/shared/models/paginated_data_model.dart';
import 'package:syncrow_web/services/api/api_exception.dart';
import 'package:syncrow_web/services/api/http_service.dart';
import 'package:syncrow_web/utils/constants/api_const.dart';
class RemoteNonBookableSpaces implements NonBookableSpacesService {
final HTTPService _httpService;
RemoteNonBookableSpaces(this._httpService);
static const _defaultErrorMessage = 'Failed to load Spaces';
@override
Future<PaginatedDataModel<BookableSpacemodel>> load(
NonBookableSpacesParams params) async {
try {
final response = await _httpService.get(
path: ApiEndpoints.bookableSpaces,
queryParameters: {
'configured': false,
'page': params.currentPage,
'search': params.searchedWords,
},
expectedResponseModel: (json) {
final result = json as Map<String, dynamic>;
return PaginatedDataModel.fromJson(
result,
BookableSpacemodel.fromJsonList,
);
},
);
return response;
} on DioException catch (e) {
final message = e.response?.data as Map<String, dynamic>?;
final error = message?['error'] as Map<String, dynamic>?;
final errorMessage = error?['error'] as String? ?? '';
final formattedErrorMessage = [
_defaultErrorMessage,
errorMessage,
].join(': ');
throw APIException(formattedErrorMessage);
} catch (e) {
final formattedErrorMessage = [_defaultErrorMessage, '$e'].join(': ');
throw APIException(formattedErrorMessage);
}
}
@override
Future<void> sendBookableSpacesToApi(
SendBookableSpacesToApiParams params) async {
try {
await _httpService.post(
path: ApiEndpoints.bookableSpaces,
body: params.toJson(),
expectedResponseModel: (p0) {},
);
} on DioException catch (e) {
final message = e.response?.data as Map<String, dynamic>?;
final error = message?['error'] as Map<String, dynamic>?;
final errorMessage = error?['error'] as String? ?? '';
final formattedErrorMessage = [
_defaultErrorMessage,
errorMessage,
].join(': ');
throw APIException(formattedErrorMessage);
} catch (e) {
final formattedErrorMessage = [_defaultErrorMessage, '$e'].join(': ');
throw APIException(formattedErrorMessage);
}
}
}

View File

@ -1,40 +0,0 @@
import 'package:dio/dio.dart';
import 'package:syncrow_web/pages/access_management/manage_bookable_spaces/domain/models/bookable_space_config.dart';
import 'package:syncrow_web/pages/access_management/manage_bookable_spaces/domain/params/update_bookable_space_param.dart';
import 'package:syncrow_web/pages/access_management/manage_bookable_spaces/domain/service/update_bookable_space_service.dart';
import 'package:syncrow_web/services/api/api_exception.dart';
import 'package:syncrow_web/services/api/http_service.dart';
import 'package:syncrow_web/utils/constants/api_const.dart';
class RemoteUpdateBookableSpaceService implements UpdateBookableSpaceService {
final HTTPService _httpService;
RemoteUpdateBookableSpaceService(this._httpService);
static const _defaultErrorMessage = 'Failed to load Bookable Spaces';
@override
Future<BookableSpaceConfig> update(
UpdateBookableSpaceParam updateParam) async {
try {
final response = await _httpService.put(
path: '${ApiEndpoints.bookableSpaces}/${updateParam.spaceUuid}',
body: updateParam.toJson(),
expectedResponseModel: (json) {
return BookableSpaceConfig.fromJson(
json['data'] as Map<String, dynamic>);
},
);
return response;
} on DioException catch (e) {
final message = e.response?.data as Map<String, dynamic>?;
final error = message?['error'] as Map<String, dynamic>?;
final errorMessage = error?['error'] as String? ?? '';
final formattedErrorMessage = [
_defaultErrorMessage,
errorMessage,
].join(': ');
throw APIException(formattedErrorMessage);
} catch (e) {
final formattedErrorMessage = [_defaultErrorMessage, '$e'].join(': ');
throw APIException(formattedErrorMessage);
}
}
}

View File

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

View File

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

View File

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

View File

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

View File

@ -1,24 +0,0 @@
class UpdateBookableSpaceParam {
String spaceUuid;
List<String>? bookableDays;
String? bookingStartTime;
String? bookingEndTime;
int? cost;
bool? availability;
UpdateBookableSpaceParam({
required this.spaceUuid,
this.bookingStartTime,
this.bookingEndTime,
this.bookableDays,
this.availability,
this.cost,
});
Map<String, dynamic> toJson() => {
if (bookableDays != null) 'daysAvailable': bookableDays,
if (bookingStartTime != null) 'startTime': bookingStartTime,
if (bookingEndTime != null) 'endTime': bookingEndTime,
if (cost != null) 'points': cost,
if (availability != null) 'active': availability,
};
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -7,6 +7,9 @@ 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/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 +29,13 @@ class SpaceManagementPage extends StatelessWidget {
)..add(const LoadCommunities(LoadCommunitiesParam())),
),
BlocProvider(create: (context) => CommunitiesTreeSelectionBloc()),
BlocProvider(
create: (context) => SpaceDetailsBloc(
UniqueSubspacesDecorator(
RemoteSpaceDetailsService(httpService: HTTPService()),
),
),
),
],
child: WebScaffold(
appBarTitle: Text(

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,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,7 @@
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';
class SpaceDetailsModel extends Equatable {
final String uuid;
@ -17,6 +18,13 @@ class SpaceDetailsModel extends Equatable {
required this.subspaces,
});
factory SpaceDetailsModel.empty() => const SpaceDetailsModel(
uuid: '',
spaceName: '',
icon: Assets.villa,
productAllocations: [],
subspaces: [],
);
factory SpaceDetailsModel.fromJson(Map<String, dynamic> json) {
return SpaceDetailsModel(
uuid: json['uuid'] as String,
@ -41,6 +49,22 @@ 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];
}
@ -48,12 +72,10 @@ class SpaceDetailsModel extends Equatable {
class ProductAllocation extends Equatable {
final Product product;
final Tag tag;
final String? location;
const ProductAllocation({
required this.product,
required this.tag,
this.location,
});
factory ProductAllocation.fromJson(Map<String, dynamic> json) {
@ -70,6 +92,16 @@ class ProductAllocation extends Equatable {
};
}
ProductAllocation copyWith({
Product? product,
Tag? tag,
}) {
return ProductAllocation(
product: product ?? this.product,
tag: tag ?? this.tag,
);
}
@override
List<Object?> get props => [product, tag];
}
@ -88,7 +120,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 +135,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: print,
),
),
);
}
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,45 @@
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,
});
final VoidCallback onCancel;
final VoidCallback? onSave;
@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: 'Cancel',
);
}
Widget _buildSaveButton() {
return DefaultButton(
onPressed: onSave,
borderRadius: 10,
backgroundColor: ColorsManager.secondaryColor,
foregroundColor: ColorsManager.whiteColors,
child: const Text('OK'),
);
}
}

View File

@ -0,0 +1,92 @@
import 'package:flutter/material.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/utils/color_manager.dart';
import 'package:syncrow_web/utils/constants/assets.dart';
class SpaceDetailsDevicesBox extends StatelessWidget {
const SpaceDetailsDevicesBox({
required this.space,
super.key,
});
final SpaceDetailsModel space;
@override
Widget build(BuildContext context) {
final productAllocations = space.productAllocations;
final subspaces = space.subspaces;
final isAnySubspaceHasProductAllocations =
subspaces.any((subspace) => subspace.productAllocations.isNotEmpty);
if (productAllocations.isNotEmpty || isAnySubspaceHasProductAllocations) {
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: [
// Combine tags from spaceModel and subspaces
// ...TagHelper.groupTags([
// ...?tags,
// ...?subspaces?.expand((subspace) => subspace.tags ?? [])
// ]).entries.map(
// (entry) => Chip(
// avatar: SizedBox(
// width: 24,
// height: 24,
// child: SvgPicture.asset(
// entry.key.icon ?? 'assets/icons/gateway.svg',
// fit: BoxFit.contain,
// ),
// ),
// label: Text(
// 'x${entry.value}',
// style: Theme.of(context)
// .textTheme
// .bodySmall
// ?.copyWith(color: ColorsManager.spaceColor),
// ),
// backgroundColor: ColorsManager.whiteColors,
// shape: RoundedRectangleBorder(
// borderRadius: BorderRadius.circular(16),
// side: const BorderSide(
// color: ColorsManager.spaceColor,
// ),
// ),
// ),
// ),
EditChip(
onTap: () {},
),
],
),
);
} else {
return TextButton(
onPressed: () {},
style: TextButton.styleFrom(
padding: EdgeInsets.zero,
),
child: const SizedBox(
width: double.infinity,
child: ButtonContentWidget(
svgAssets: Assets.addIcon,
label: 'Add Devices',
// disabled: isTagsAndSubspaceModelDisabled,
),
),
);
}
}
}

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,77 @@
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: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
SpaceNameTextField(
initialValue: space.spaceName,
isNameFieldExist: (value) => space.subspaces.any(
(subspace) => subspace.name == value,
),
),
const Spacer(),
SpaceSubSpacesBox(
subspaces: space.subspaces,
),
const SizedBox(height: 16),
SpaceDetailsDevicesBox(space: space),
],
),
),
],
),
),
actions: [
SpaceDetailsActionButtons(
onSave: () => onSave(space),
onCancel: Navigator.of(context).pop,
),
],
);
}),
);
}
}

View File

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

View File

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

View File

@ -0,0 +1,85 @@
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:syncrow_web/pages/space_management_v2/modules/update_space/presentation/bloc/space_details_model_bloc/space_details_model_bloc.dart';
import 'package:syncrow_web/utils/color_manager.dart';
import 'package:syncrow_web/utils/extension/build_context_x.dart';
class SpaceNameTextField extends StatefulWidget {
const SpaceNameTextField({
required this.initialValue,
required this.isNameFieldExist,
super.key,
});
final String? initialValue;
final bool Function(String value) isNameFieldExist;
@override
State<SpaceNameTextField> createState() => _SpaceNameTextFieldState();
}
class _SpaceNameTextFieldState extends State<SpaceNameTextField> {
late final TextEditingController _controller;
@override
void initState() {
_controller = TextEditingController(text: widget.initialValue);
super.initState();
}
@override
void dispose() {
_controller.dispose();
super.dispose();
}
final _formKey = GlobalKey<FormState>();
String? _validateName(String? value) {
if (value == null || value.isEmpty) {
return '*Space name should not be empty.';
}
if (widget.isNameFieldExist(value)) {
return '*Name already exists';
}
return null;
}
@override
Widget build(BuildContext context) {
return Form(
key: _formKey,
autovalidateMode: AutovalidateMode.onUserInteraction,
child: TextFormField(
controller: _controller,
onChanged: (value) => context.read<SpaceDetailsModelBloc>().add(
UpdateSpaceDetailsName(value),
),
validator: _validateName,
style: context.textTheme.bodyMedium,
decoration: InputDecoration(
hintText: 'Please enter the name',
hintStyle: context.textTheme.bodyMedium!.copyWith(
color: ColorsManager.lightGrayColor,
),
filled: true,
fillColor: ColorsManager.boxColor,
enabledBorder: _buildBorder(context, ColorsManager.vividBlue),
focusedBorder: _buildBorder(context, ColorsManager.primaryColor),
errorBorder: _buildBorder(context, context.theme.colorScheme.error),
focusedErrorBorder: _buildBorder(context, context.theme.colorScheme.error),
errorStyle: context.textTheme.bodySmall?.copyWith(
color: context.theme.colorScheme.error,
),
),
),
);
}
OutlineInputBorder _buildBorder(BuildContext context, [Color? color]) {
return OutlineInputBorder(
borderRadius: BorderRadius.circular(10),
borderSide: BorderSide(width: 1, color: color ?? ColorsManager.boxColor),
);
}
}

View File

@ -0,0 +1,74 @@
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:syncrow_web/common/edit_chip.dart';
import 'package:syncrow_web/pages/space_management_v2/modules/space_details/domain/models/space_details_model.dart';
import 'package:syncrow_web/pages/space_management_v2/modules/space_details/presentation/widgets/button_content_widget.dart';
import 'package:syncrow_web/pages/space_management_v2/modules/space_details/presentation/widgets/space_sub_spaces_dialog.dart';
import 'package:syncrow_web/pages/space_management_v2/modules/space_details/presentation/widgets/subspace_name_display_widget.dart';
import 'package:syncrow_web/pages/space_management_v2/modules/update_space/presentation/bloc/space_details_model_bloc/space_details_model_bloc.dart';
import 'package:syncrow_web/utils/color_manager.dart';
import 'package:syncrow_web/utils/constants/assets.dart';
class SpaceSubSpacesBox extends StatelessWidget {
const SpaceSubSpacesBox({super.key, required this.subspaces});
final List<Subspace> subspaces;
@override
Widget build(BuildContext context) {
if (subspaces.isEmpty) {
return TextButton(
style: TextButton.styleFrom(
padding: EdgeInsets.zero,
overlayColor: ColorsManager.transparentColor,
),
onPressed: () => _showSubSpacesDialog(context),
child: const SizedBox(
width: double.infinity,
child: ButtonContentWidget(
svgAssets: Assets.addIcon,
label: 'Create Sub Spaces',
),
),
);
} else {
return Container(
padding: const EdgeInsets.all(8.0),
width: double.infinity,
decoration: BoxDecoration(
color: ColorsManager.textFieldGreyColor,
borderRadius: BorderRadius.circular(15),
border: Border.all(
color: ColorsManager.textFieldGreyColor,
width: 3.0,
),
),
child: Wrap(
spacing: 8.0,
runSpacing: 8.0,
children: [
...subspaces.map((e) => SubspaceNameDisplayWidget(subSpace: e)),
EditChip(
onTap: () => _showSubSpacesDialog(context),
),
],
),
);
}
}
void _showSubSpacesDialog(BuildContext context) {
showDialog<void>(
context: context,
barrierDismissible: false,
builder: (_) => SpaceSubSpacesDialog(
subspaces: subspaces,
onSave: (subspaces) {
context.read<SpaceDetailsModelBloc>().add(
UpdateSpaceDetailsSubspaces(subspaces),
);
},
),
);
}
}

View File

@ -0,0 +1,90 @@
import 'package:flutter/material.dart';
import 'package:syncrow_web/pages/space_management_v2/modules/space_details/domain/models/space_details_model.dart';
import 'package:syncrow_web/pages/space_management_v2/modules/space_details/presentation/widgets/space_details_action_buttons.dart';
import 'package:syncrow_web/pages/space_management_v2/modules/space_details/presentation/widgets/sub_spaces_input.dart';
import 'package:uuid/uuid.dart';
class SpaceSubSpacesDialog extends StatefulWidget {
const SpaceSubSpacesDialog({
required this.subspaces,
required this.onSave,
super.key,
});
final List<Subspace> subspaces;
final void Function(List<Subspace> subspaces) onSave;
@override
State<SpaceSubSpacesDialog> createState() => _SpaceSubSpacesDialogState();
}
class _SpaceSubSpacesDialogState extends State<SpaceSubSpacesDialog> {
late List<Subspace> _subspaces;
bool get _hasDuplicateNames =>
_subspaces.map((subspace) => subspace.name.toLowerCase()).toSet().length !=
_subspaces.length;
@override
void initState() {
super.initState();
_subspaces = List.from(widget.subspaces);
}
void _handleSubspaceAdded(String name) {
setState(() {
_subspaces = [
..._subspaces,
Subspace(
name: name,
uuid: const Uuid().v4(),
productAllocations: const [],
),
];
});
}
void _handleSubspaceDeleted(String uuid) => setState(
() => _subspaces = _subspaces.where((s) => s.uuid != uuid).toList(),
);
void _handleSave() {
widget.onSave(_subspaces);
Navigator.of(context).pop();
}
@override
Widget build(BuildContext context) {
return AlertDialog(
title: const SelectableText('Create Sub Spaces'),
content: Column(
spacing: 12,
mainAxisSize: MainAxisSize.min,
children: [
SubSpacesInput(
subSpaces: _subspaces,
onSubspaceAdded: _handleSubspaceAdded,
onSubspaceDeleted: _handleSubspaceDeleted,
),
AnimatedSwitcher(
duration: const Duration(milliseconds: 100),
child: Visibility(
key: ValueKey(_hasDuplicateNames),
visible: _hasDuplicateNames,
child: const SelectableText(
'Error: Duplicate subspace names are not allowed.',
style: TextStyle(color: Colors.red),
),
),
),
],
),
actions: [
SpaceDetailsActionButtons(
onSave: _hasDuplicateNames ? null : _handleSave,
onCancel: Navigator.of(context).pop,
)
],
);
}
}

View File

@ -0,0 +1,107 @@
import 'package:flutter/material.dart';
import 'package:syncrow_web/pages/space_management_v2/modules/space_details/domain/models/space_details_model.dart';
import 'package:syncrow_web/pages/space_management_v2/modules/space_details/presentation/widgets/subspace_chip.dart';
import 'package:syncrow_web/utils/color_manager.dart';
import 'package:syncrow_web/utils/extension/build_context_x.dart';
class SubSpacesInput extends StatefulWidget {
const SubSpacesInput({
super.key,
required this.subSpaces,
required this.onSubspaceAdded,
required this.onSubspaceDeleted,
});
final List<Subspace> subSpaces;
final void Function(String name) onSubspaceAdded;
final void Function(String uuid) onSubspaceDeleted;
@override
State<SubSpacesInput> createState() => _SubSpacesInputState();
}
class _SubSpacesInputState extends State<SubSpacesInput> {
late final TextEditingController _subspaceNameController;
late final FocusNode _focusNode;
@override
void initState() {
super.initState();
_subspaceNameController = TextEditingController();
_focusNode = FocusNode();
}
@override
void dispose() {
_subspaceNameController.dispose();
_focusNode.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Container(
width: context.screenWidth * 0.35,
padding: const EdgeInsets.symmetric(
vertical: 10,
horizontal: 16,
),
decoration: BoxDecoration(
color: ColorsManager.boxColor,
borderRadius: BorderRadius.circular(10),
),
child: Wrap(
spacing: 8,
runSpacing: 8,
alignment: WrapAlignment.start,
crossAxisAlignment: WrapCrossAlignment.center,
children: [
...widget.subSpaces.asMap().entries.map(
(entry) {
final index = entry.key;
final subSpace = entry.value;
final lowerName = subSpace.name.toLowerCase();
final duplicateIndices = widget.subSpaces
.asMap()
.entries
.where((e) => e.value.name.toLowerCase() == lowerName)
.map((e) => e.key)
.toList();
final isDuplicate = duplicateIndices.length > 1 &&
duplicateIndices.indexOf(index) != 0;
return SubspaceChip(
subSpace: subSpace,
isDuplicate: isDuplicate,
onDeleted: () => widget.onSubspaceDeleted(subSpace.uuid),
);
},
),
SizedBox(
width: 200,
child: TextField(
focusNode: _focusNode,
controller: _subspaceNameController,
decoration: InputDecoration(
border: InputBorder.none,
hintText: widget.subSpaces.isEmpty ? 'Please enter the name' : null,
hintStyle: context.textTheme.bodySmall?.copyWith(
color: ColorsManager.lightGrayColor,
),
),
onSubmitted: (value) {
final trimmedValue = value.trim();
if (trimmedValue.isNotEmpty) {
widget.onSubspaceAdded(trimmedValue);
_subspaceNameController.clear();
_focusNode.requestFocus();
}
},
style: context.textTheme.bodyMedium,
),
),
],
),
);
}
}

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