Compare commits

..

27 Commits

Author SHA1 Message Date
5532935a3a Enhance UI components: update color management, adjust button styles,… (#350)
… and improve text formatting for better readability

<!--
  Thanks for contributing!

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

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



## Description

<!--- Describe your changes in detail -->
Enhance UI components: update color 
## Type of Change

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

- [ ]  New feature (non-breaking change which adds functionality)
- [x] 🛠️ Bug fix (non-breaking change which fixes an issue)
- [ ]  Breaking change (fix or feature that would cause existing
functionality to change)
- [ ] 🧹 Code refactor
- [ ]  Build configuration change
- [ ] 📝 Documentation
- [ ] 🗑️ Chore
2025-07-14 16:15:42 +03:00
249cbfc242 Merge branch 'dev' into Build-Schedule-List-View-Support-State-Persistence 2025-07-14 15:20:08 +03:00
8167926620 Enhance UI components: update color management, adjust button styles, and improve text formatting for better readability 2025-07-14 15:14:56 +03:00
559091faa0 Add calendar event management features and UI components and Implemen… (#349)
…t Calendar logic

<!--
  Thanks for contributing!

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

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



## Description

<!--- Describe your changes in detail -->
Implement Calendar logic

## Type of Change

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

- [x]  New feature (non-breaking change which adds functionality)
- [ ] 🛠️ Bug fix (non-breaking change which fixes an issue)
- [ ]  Breaking change (fix or feature that would cause existing
functionality to change)
- [ ] 🧹 Code refactor
- [ ]  Build configuration change
- [ ] 📝 Documentation
- [ ] 🗑️ Chore
2025-07-14 14:18:43 +03:00
eba351c9be Sp 1717 fe draw create edit space dialog (#348)
<!--
  Thanks for contributing!

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

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

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

## Description

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


## Type of Change

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

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


[SP-1717]:
https://syncrow.atlassian.net/browse/SP-1717?atlOrigin=eyJpIjoiNWRkNTljNzYxNjVmNDY3MDlhMDU5Y2ZhYzA5YTRkZjUiLCJwIjoiZ2l0aHViLWNvbS1KU1cifQ
2025-07-14 12:57:21 +03:00
c112cde634 Uses Inkwell instead of Gesture Detector for canvas widgets. 2025-07-14 11:04:29 +03:00
65d541d594 Add calendar event management features and UI components and Implement Calendar logic 2025-07-14 10:46:12 +03:00
7331c8440b Refactor SpaceManagementPage to use StatefulWidget and initialize CommunitiesBloc in initState. Update CommunityStructureHeader to handle community updates and improve state management in CommunitiesTreeSelectionBloc with new event for community state updates. 2025-07-14 10:27:22 +03:00
a409e34643 Merge branch 'SP-1717-FE-Draw-Create-Edit-Space-Dialog' of https://github.com/SyncrowIOT/web into SP-1717-FE-Draw-Create-Edit-Space-Dialog 2025-07-14 10:16:31 +03:00
7cc59e43df Setup new firebase project in the web platform. (#343)
<!--
  Thanks for contributing!

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

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

## Description

Setup new firebase project in the web platform.

## Type of Change

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

- [ ]  New feature (non-breaking change which adds functionality)
- [ ] 🛠️ Bug fix (non-breaking change which fixes an issue)
- [ ]  Breaking change (fix or feature that would cause existing
functionality to change)
- [ ] 🧹 Code refactor
- [x]  Build configuration change
- [ ] 📝 Documentation
- [ ] 🗑️ Chore
2025-07-13 13:28:32 +03:00
21f8b2962c Refactor date selection: add SelectDateFromSidebarCalendar event and … (#344)
…update state management for improved clarity

<!--
  Thanks for contributing!

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

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


## Description

<!--- Describe your changes in detail -->
implement highlighted selected day

## Type of Change

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

- [x]  New feature (non-breaking change which adds functionality)
- [ ] 🛠️ Bug fix (non-breaking change which fixes an issue)
- [ ]  Breaking change (fix or feature that would cause existing
functionality to change)
- [ ] 🧹 Code refactor
- [ ]  Build configuration change
- [ ] 📝 Documentation
- [ ] 🗑️ Chore
2025-07-10 15:05:09 +03:00
645a07287e Refactor date selection: add SelectDateFromSidebarCalendar event and update state management for improved clarity 2025-07-10 14:15:57 +03:00
df29aab111 Setup new firebase project in the web platform. 2025-07-10 12:18:45 +03:00
e55e793081 Implement-Calendar-ui (#342)
<!--
  Thanks for contributing!

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

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



## Description

<!--- Describe your changes in detail -->
Implement Calendar ui

## Type of Change

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

- [x]  New feature (non-breaking change which adds functionality)
- [ ] 🛠️ Bug fix (non-breaking change which fixes an issue)
- [ ]  Breaking change (fix or feature that would cause existing
functionality to change)
- [ ] 🧹 Code refactor
- [ ]  Build configuration change
- [ ] 📝 Documentation
- [ ] 🗑️ Chore
2025-07-10 12:13:40 +03:00
83202204b0 Remove BlocProvider for UpdateSpaceBloc in SpaceDetailsDialogHelper to streamline dependency management and improve code clarity. 2025-07-09 15:58:17 +03:00
d87739f1fd Refactor JSON Serialization in UpdateSpaceParam: Adjusted the _toJson method for Subspace to ensure 'subspaceName' is always included and 'uuid' is only added when applicable, enhancing clarity and consistency in data representation. 2025-07-09 15:25:41 +03:00
5cd083a37b Refactor Space and Tag Models: Removed unused JSON serialization methods from SpaceDetailsModel, ProductAllocation, and Subspace. Updated Tag model to eliminate unnecessary fields. Enhanced UpdateSpaceParam to streamline JSON conversion for subspaces and product allocations, improving data handling during updates. 2025-07-09 15:08:49 +03:00
2b8d987c69 Add SpaceReorderDataModel and integrate drag-and-drop functionality in CommunityStructureCanvas for improved space management. 2025-07-08 16:00:57 +03:00
707cb4791f Added CreateSpaceButton for improved user interaction and updated layout calculations to utilize context extensions for better responsiveness. 2025-07-08 13:08:43 +03:00
03c45ed8d0 Refactor SpaceCardWidget: Simplified widget structure by removing unnecessary SizedBox. 2025-07-08 13:07:55 +03:00
9e0ea4ad6f Adjust spacer flex in SpaceManagementCommunityStructure widget for improved layout consistency. 2025-07-08 13:07:39 +03:00
9e6b14737f Refactor CreateSpaceButton: Changed from StatelessWidget to StatefulWidget to manage hover state and added tooltip for improved user experience. Enhanced button styling and interaction feedback for better visual cues during space creation. 2025-07-08 13:07:26 +03:00
7c2aed2d58 Refactor RemoteUpdateSpaceService: Improved error handling in updateSpace method by checking API response success before returning the updated space. This enhances robustness and ensures proper error propagation for failed updates. 2025-07-08 12:20:10 +03:00
bcf62027bc Validate UUIDs in RemoteUpdateSpaceService: Added checks for empty space and community UUIDs before constructing the update URL, improving error handling and robustness in the update space process. 2025-07-08 11:12:12 +03:00
b001713ce4 Enhance Community Structure Widgets: Updated SpaceDetailsDialogHelper to accept community UUID for space creation and editing. Refactored CreateSpaceButton and CommunityStructureHeader to pass community UUID, improving data handling and consistency across the community structure features. 2025-07-08 11:10:22 +03:00
bab3226c73 Merge branch 'dev' of https://github.com/SyncrowIOT/web into SP-1717-FE-Draw-Create-Edit-Space-Dialog 2025-07-08 10:02:12 +03:00
fa1eaa570c Refactor Space Update Logic: Introduced UpdateSpaceParam for better parameter handling in update operations. Enhanced SpaceDetailsDialogHelper to manage loading and error states during space updates. Updated RemoteUpdateSpaceService to construct dynamic URLs for space updates based on community UUID. Improved CommunitiesTreeFailureWidget UI with SelectableText and added spacing for better layout. 2025-07-08 10:01:43 +03:00
63 changed files with 1794 additions and 914 deletions

View File

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

View File

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

View File

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

34
.vscode/launch.json vendored
View File

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

View File

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

16
lib/firebase_options.dart Normal file
View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -0,0 +1,134 @@
class CalendarEventBooking {
final String uuid;
final DateTime date;
final String startTime;
final String endTime;
final int cost;
final BookingUser user;
final BookingSpace space;
CalendarEventBooking({
required this.uuid,
required this.date,
required this.startTime,
required this.endTime,
required this.cost,
required this.user,
required this.space,
});
factory CalendarEventBooking.fromJson(Map<String, dynamic> json) {
return CalendarEventBooking(
uuid: json['uuid'] as String? ?? '',
date: json['date'] != null
? DateTime.parse(json['date'] as String)
: DateTime.now(),
startTime: json['startTime'] as String? ?? '',
endTime: json['endTime'] as String? ?? '',
cost: _parseInt(json['cost']),
user: json['user'] != null
? BookingUser.fromJson(json['user'] as Map<String, dynamic>)
: BookingUser.empty(),
space: json['space'] != null
? BookingSpace.fromJson(json['space'] as Map<String, dynamic>)
: BookingSpace.empty(),
);
}
static int _parseInt(dynamic value) {
if (value is int) return value;
if (value is String) return int.tryParse(value) ?? 0;
return 0;
}
}
class BookingUser {
final String uuid;
final String firstName;
final String lastName;
final String email;
final String? companyName;
BookingUser({
required this.uuid,
required this.firstName,
required this.lastName,
required this.email,
this.companyName,
});
factory BookingUser.fromJson(Map<String, dynamic> json) {
return BookingUser(
uuid: json['uuid'] as String? ?? '',
firstName: json['firstName'] as String? ?? '',
lastName: json['lastName'] as String? ?? '',
email: json['email'] as String? ?? '',
companyName: json['companyName'] as String?,
);
}
factory BookingUser.empty() {
return BookingUser(
uuid: '',
firstName: '',
lastName: '',
email: '',
companyName: null,
);
}
}
class BookingSpace {
final String uuid;
final String spaceName;
BookingSpace({
required this.uuid,
required this.spaceName,
});
factory BookingSpace.fromJson(Map<String, dynamic> json) {
return BookingSpace(
uuid: json['uuid'] as String? ?? '',
spaceName: json['spaceName'] as String? ?? '',
);
}
factory BookingSpace.empty() {
return BookingSpace(
uuid: '',
spaceName: '',
);
}
}
class CalendarEventsResponse {
final int statusCode;
final String message;
final List<CalendarEventBooking> data;
final bool success;
CalendarEventsResponse({
required this.statusCode,
required this.message,
required this.data,
required this.success,
});
factory CalendarEventsResponse.fromJson(Map<String, dynamic> json) {
return CalendarEventsResponse(
statusCode: _parseInt(json['statusCode']),
message: json['message'] as String? ?? '',
data: (json['data'] as List? ?? [])
.map((e) => CalendarEventBooking.fromJson(e as Map<String, dynamic>))
.toList(),
success: json['success'] as bool? ?? false,
);
}
}
int _parseInt(dynamic value) {
if (value is int) return value;
if (value is String) return int.tryParse(value) ?? 0;
return 0;
}

View File

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

View File

@ -2,13 +2,17 @@ import 'dart:async';
import 'package:bloc/bloc.dart'; import 'package:bloc/bloc.dart';
import 'package:calendar_view/calendar_view.dart'; import 'package:calendar_view/calendar_view.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:syncrow_web/pages/access_management/booking_system/domain/models/calendar_event_booking.dart';
import 'package:syncrow_web/pages/access_management/booking_system/domain/services/calendar_system_service.dart';
part 'events_event.dart'; part 'events_event.dart';
part 'events_state.dart'; part 'events_state.dart';
class CalendarEventsBloc extends Bloc<CalendarEventsEvent, CalendarEventState> { class CalendarEventsBloc extends Bloc<CalendarEventsEvent, CalendarEventState> {
final EventController eventController = EventController(); final EventController eventController = EventController();
final CalendarSystemService calendarService;
CalendarEventsBloc() : super(EventsInitial()) { CalendarEventsBloc({required this.calendarService}) : super(EventsInitial()) {
on<LoadEvents>(_onLoadEvents); on<LoadEvents>(_onLoadEvents);
on<AddEvent>(_onAddEvent); on<AddEvent>(_onAddEvent);
on<StartTimer>(_onStartTimer); on<StartTimer>(_onStartTimer);
@ -22,53 +26,24 @@ class CalendarEventsBloc extends Bloc<CalendarEventsEvent, CalendarEventState> {
) async { ) async {
emit(EventsLoading()); emit(EventsLoading());
try { try {
final events = _generateDummyEventsForWeek(event.weekStart); final response = await calendarService.getCalendarEvents(
spaceId: event.spaceId,
);
final events =
response.data.map<CalendarEventData>(_toCalendarEventData).toList();
eventController.addAll(events); eventController.addAll(events);
emit(EventsLoaded( emit(EventsLoaded(events: events));
events: events,
initialDate: event.weekStart,
weekDays: _getWeekDays(event.weekStart),
));
} catch (e) { } catch (e) {
emit(EventsError('Failed to load events')); emit(EventsError('Failed to load events'));
} }
} }
List<CalendarEventData> _generateDummyEventsForWeek(DateTime weekStart) {
final events = <CalendarEventData>[];
for (int i = 0; i < 7; i++) {
final date = weekStart.add(Duration(days: i));
events.add(CalendarEventData(
date: date,
startTime: date.copyWith(hour: 9, minute: 0),
endTime: date.copyWith(hour: 10, minute: 30),
title: 'Team Meeting',
description: 'Daily standup',
color: Colors.blue,
));
events.add(CalendarEventData(
date: date,
startTime: date.copyWith(hour: 14, minute: 0),
endTime: date.copyWith(hour: 15, minute: 0),
title: 'Client Call',
description: 'Project discussion',
color: Colors.green,
));
}
return events;
}
void _onAddEvent(AddEvent event, Emitter<CalendarEventState> emit) { void _onAddEvent(AddEvent event, Emitter<CalendarEventState> emit) {
eventController.add(event.event); eventController.add(event.event);
if (state is EventsLoaded) { if (state is EventsLoaded) {
final loaded = state as EventsLoaded; final loaded = state as EventsLoaded;
emit(EventsLoaded( emit(EventsLoaded(
events: [...eventController.events], events: [...eventController.events],
initialDate: loaded.initialDate,
weekDays: loaded.weekDays,
)); ));
} }
} }
@ -86,47 +61,44 @@ class CalendarEventsBloc extends Bloc<CalendarEventsEvent, CalendarEventState> {
final newWeekDays = _getWeekDays(event.weekDate); final newWeekDays = _getWeekDays(event.weekDate);
emit(EventsLoaded( emit(EventsLoaded(
events: loaded.events, events: loaded.events,
initialDate: event.weekDate,
weekDays: newWeekDays,
)); ));
} }
} }
List<CalendarEventData> _generateDummyEvents() { CalendarEventData _toCalendarEventData(CalendarEventBooking booking) {
final now = DateTime.now(); final date = booking.date;
return [
CalendarEventData( final localDate = date.toLocal();
date: now,
startTime: now.copyWith(hour: 8, minute: 00, second: 0), final startParts = booking.startTime.split(':').map(int.parse).toList();
endTime: now.copyWith(hour: 9, minute: 00, second: 0), final endParts = booking.endTime.split(':').map(int.parse).toList();
title: 'Team Meeting',
description: 'Weekly team sync', final startTime = DateTime(
localDate.year,
localDate.month,
localDate.day,
startParts[0],
startParts[1],
);
final endTime = DateTime(
localDate.year,
localDate.month,
localDate.day,
endParts[0],
endParts[1],
);
return CalendarEventData(
date: startTime,
startTime: startTime,
endTime: endTime,
title:
'${booking.space.spaceName} - ${booking.user.firstName} ${booking.user.lastName}',
description: 'Cost: ${booking.cost}',
color: Colors.blue, color: Colors.blue,
), event: booking,
CalendarEventData( );
date: now,
startTime: now.copyWith(hour: 9, minute: 00, second: 0),
endTime: now.copyWith(hour: 10, minute: 30, second: 0),
title: 'Team Meeting',
description: 'Weekly team sync',
color: Colors.blue,
),
CalendarEventData(
date: now.add(const Duration(days: 1)),
startTime: now.copyWith(hour: 14, day: now.day + 1),
endTime: now.copyWith(hour: 15, day: now.day + 1),
title: 'Client Call',
description: 'Project discussion',
color: Colors.green,
),
CalendarEventData(
date: now.add(const Duration(days: 2)),
startTime: now.copyWith(hour: 11, day: now.day + 2),
endTime: now.copyWith(hour: 12, day: now.day + 2),
title: 'Lunch with Team',
color: Colors.orange,
),
];
} }
List<DateTime> _getWeekDays(DateTime date) { List<DateTime> _getWeekDays(DateTime date) {

View File

@ -6,13 +6,20 @@ abstract class CalendarEventsEvent {
} }
class LoadEvents extends CalendarEventsEvent { class LoadEvents extends CalendarEventsEvent {
final String spaceId;
final DateTime weekStart; final DateTime weekStart;
const LoadEvents({required this.weekStart}); final DateTime weekEnd;
const LoadEvents({
required this.spaceId,
required this.weekStart,
required this.weekEnd,
});
} }
class AddEvent extends CalendarEventsEvent { class AddEvent extends CalendarEventsEvent {
final CalendarEventData event; final CalendarEventData event;
AddEvent(this.event); const AddEvent(this.event);
} }
class StartTimer extends CalendarEventsEvent {} class StartTimer extends CalendarEventsEvent {}
@ -23,3 +30,8 @@ class GoToWeek extends CalendarEventsEvent {
final DateTime weekDate; final DateTime weekDate;
GoToWeek(this.weekDate); GoToWeek(this.weekDate);
} }
class CheckWeekHasEvents extends CalendarEventsEvent {
final DateTime weekStart;
const CheckWeekHasEvents(this.weekStart);
}

View File

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

View File

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

View File

@ -11,3 +11,8 @@ class SelectDate extends DateSelectionEvent {
class NextWeek extends DateSelectionEvent {} class NextWeek extends DateSelectionEvent {}
class PreviousWeek extends DateSelectionEvent {} class PreviousWeek extends DateSelectionEvent {}
class SelectDateFromSidebarCalendar extends DateSelectionEvent {
final DateTime selectedDate;
SelectDateFromSidebarCalendar(this.selectedDate);
}

View File

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

View File

@ -1,7 +1,8 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:intl/intl.dart';
import 'package:calendar_view/calendar_view.dart'; import 'package:calendar_view/calendar_view.dart';
import 'package:syncrow_web/pages/access_management/booking_system/data/services/remote_calendar_service.dart';
import 'package:syncrow_web/pages/access_management/booking_system/presentation/bloc/calendar/events_bloc.dart';
import 'package:syncrow_web/pages/access_management/booking_system/presentation/bloc/date_selection/date_selection_bloc.dart'; import 'package:syncrow_web/pages/access_management/booking_system/presentation/bloc/date_selection/date_selection_bloc.dart';
import 'package:syncrow_web/pages/access_management/booking_system/presentation/bloc/date_selection/date_selection_event.dart'; import 'package:syncrow_web/pages/access_management/booking_system/presentation/bloc/date_selection/date_selection_event.dart';
import 'package:syncrow_web/pages/access_management/booking_system/presentation/bloc/date_selection/date_selection_state.dart'; import 'package:syncrow_web/pages/access_management/booking_system/presentation/bloc/date_selection/date_selection_state.dart';
@ -9,7 +10,9 @@ import 'package:syncrow_web/pages/access_management/booking_system/presentation/
import 'package:syncrow_web/pages/access_management/booking_system/presentation/view/widgets/booking_sidebar.dart'; import 'package:syncrow_web/pages/access_management/booking_system/presentation/view/widgets/booking_sidebar.dart';
import 'package:syncrow_web/pages/access_management/booking_system/presentation/view/widgets/custom_calendar_page.dart'; import 'package:syncrow_web/pages/access_management/booking_system/presentation/view/widgets/custom_calendar_page.dart';
import 'package:syncrow_web/pages/access_management/booking_system/presentation/view/widgets/icon_text_button.dart'; import 'package:syncrow_web/pages/access_management/booking_system/presentation/view/widgets/icon_text_button.dart';
import 'package:syncrow_web/pages/access_management/booking_system/presentation/view/widgets/week_navigation.dart';
import 'package:syncrow_web/pages/access_management/booking_system/presentation/view/widgets/weekly_calendar_page.dart'; import 'package:syncrow_web/pages/access_management/booking_system/presentation/view/widgets/weekly_calendar_page.dart';
import 'package:syncrow_web/services/api/http_service.dart';
import 'package:syncrow_web/utils/color_manager.dart'; import 'package:syncrow_web/utils/color_manager.dart';
import 'package:syncrow_web/utils/constants/assets.dart'; import 'package:syncrow_web/utils/constants/assets.dart';
@ -35,33 +38,20 @@ class _BookingPageState extends State<BookingPage> {
super.dispose(); super.dispose();
} }
List<CalendarEventData> _generateDummyEventsForWeek(DateTime weekStart) { void _dispatchLoadEvents(BuildContext context) {
final List<CalendarEventData> events = []; final selectedRoom =
for (int i = 0; i < 7; i++) { context.read<SelectedBookableSpaceBloc>().state.selectedBookableSpace;
final date = weekStart.add(Duration(days: i)); final dateState = context.read<DateSelectionBloc>().state;
events.add(CalendarEventData(
date: date,
startTime: date.copyWith(hour: 9, minute: 0),
endTime: date.copyWith(hour: 10, minute: 30),
title: 'Team Meeting',
description: 'Daily standup',
color: Colors.blue,
));
events.add(CalendarEventData(
date: date,
startTime: date.copyWith(hour: 14, minute: 0),
endTime: date.copyWith(hour: 15, minute: 0),
title: 'Client Call',
description: 'Project discussion',
color: Colors.green,
));
}
return events;
}
void _loadEventsForWeek(DateTime weekStart) { if (selectedRoom != null) {
_eventController.removeWhere((_) => true); context.read<CalendarEventsBloc>().add(
_eventController.addAll(_generateDummyEventsForWeek(weekStart)); LoadEvents(
spaceId: selectedRoom.uuid,
weekStart: dateState.weekStart,
weekEnd: dateState.weekStart.add(const Duration(days: 6)),
),
);
}
} }
@override @override
@ -70,13 +60,28 @@ class _BookingPageState extends State<BookingPage> {
providers: [ providers: [
BlocProvider(create: (_) => SelectedBookableSpaceBloc()), BlocProvider(create: (_) => SelectedBookableSpaceBloc()),
BlocProvider(create: (_) => DateSelectionBloc()), BlocProvider(create: (_) => DateSelectionBloc()),
BlocProvider(
create: (_) => CalendarEventsBloc(
calendarService:
FakeRemoteCalendarService(HTTPService(), useDummy: true),
),
),
], ],
child: BlocListener<DateSelectionBloc, DateSelectionState>( child: Builder(
listenWhen: (previous, current) => builder: (context) =>
previous.weekStart != current.weekStart, BlocListener<CalendarEventsBloc, CalendarEventState>(
listenWhen: (prev, curr) => curr is EventsLoaded,
listener: (context, state) { listener: (context, state) {
_loadEventsForWeek(state.weekStart); if (state is EventsLoaded) {
_eventController.removeWhere((_) => true);
_eventController.addAll(state.events);
}
}, },
child: BlocListener<SelectedBookableSpaceBloc,
SelectedBookableSpaceState>(
listener: (context, state) => _dispatchLoadEvents(context),
child: BlocListener<DateSelectionBloc, DateSelectionState>(
listener: (context, state) => _dispatchLoadEvents(context),
child: Row( child: Row(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
@ -111,7 +116,8 @@ class _BookingPageState extends State<BookingPage> {
), ),
), ),
Expanded( Expanded(
child: BlocBuilder<DateSelectionBloc, DateSelectionState>( child: BlocBuilder<DateSelectionBloc,
DateSelectionState>(
builder: (context, dateState) { builder: (context, dateState) {
return CustomCalendarPage( return CustomCalendarPage(
selectedDate: dateState.selectedDate, selectedDate: dateState.selectedDate,
@ -120,6 +126,8 @@ class _BookingPageState extends State<BookingPage> {
context context
.read<DateSelectionBloc>() .read<DateSelectionBloc>()
.add(SelectDate(newDate)); .add(SelectDate(newDate));
context.read<DateSelectionBloc>().add(
SelectDateFromSidebarCalendar(newDate));
}, },
); );
}, },
@ -154,59 +162,25 @@ class _BookingPageState extends State<BookingPage> {
), ),
], ],
), ),
BlocBuilder<DateSelectionBloc, DateSelectionState>( BlocBuilder<DateSelectionBloc,
DateSelectionState>(
builder: (context, state) { builder: (context, state) {
final weekStart = state.weekStart; final weekStart = state.weekStart;
final weekEnd = final weekEnd =
weekStart.add(const Duration(days: 6)); weekStart.add(const Duration(days: 6));
return Container( return WeekNavigation(
padding: const EdgeInsets.symmetric( weekStart: weekStart,
horizontal: 10, vertical: 5), weekEnd: weekEnd,
decoration: BoxDecoration( onPreviousWeek: () {
color: ColorsManager.circleRolesBackground,
borderRadius: BorderRadius.circular(10),
boxShadow: const [
BoxShadow(
color: ColorsManager.lightGrayColor,
blurRadius: 4,
offset: Offset(0, 1),
),
],
),
child: Row(
children: [
IconButton(
iconSize: 15,
icon: const Icon(Icons.arrow_back_ios,
color: ColorsManager.lightGrayColor),
onPressed: () {
context context
.read<DateSelectionBloc>() .read<DateSelectionBloc>()
.add(PreviousWeek()); .add(PreviousWeek());
}, },
), onNextWeek: () {
const SizedBox(width: 10),
Text(
_getMonthYearText(weekStart, weekEnd),
style: const TextStyle(
color: ColorsManager.lightGrayColor,
fontSize: 14,
fontWeight: FontWeight.w400,
),
),
const SizedBox(width: 10),
IconButton(
iconSize: 15,
icon: const Icon(Icons.arrow_forward_ios,
color: ColorsManager.lightGrayColor),
onPressed: () {
context context
.read<DateSelectionBloc>() .read<DateSelectionBloc>()
.add(NextWeek()); .add(NextWeek());
}, },
),
],
),
); );
}, },
), ),
@ -216,17 +190,35 @@ class _BookingPageState extends State<BookingPage> {
child: BlocBuilder<SelectedBookableSpaceBloc, child: BlocBuilder<SelectedBookableSpaceBloc,
SelectedBookableSpaceState>( SelectedBookableSpaceState>(
builder: (context, roomState) { builder: (context, roomState) {
final selectedRoom = roomState.selectedBookableSpace; final selectedRoom =
roomState.selectedBookableSpace;
return BlocBuilder<DateSelectionBloc, return BlocBuilder<DateSelectionBloc,
DateSelectionState>( DateSelectionState>(
builder: (context, dateState) { builder: (context, dateState) {
return WeeklyCalendarPage( return BlocListener<CalendarEventsBloc,
startTime: CalendarEventState>(
selectedRoom?.bookableConfig.startTime, listenWhen: (prev, curr) =>
endTime: selectedRoom?.bookableConfig.endTime, curr is EventsLoaded,
listener: (context, state) {
if (state is EventsLoaded) {
_eventController
.removeWhere((_) => true);
_eventController.addAll(state.events);
}
},
child: WeeklyCalendarPage(
startTime: selectedRoom
?.bookableConfig.startTime,
endTime: selectedRoom
?.bookableConfig.endTime,
weekStart: dateState.weekStart, weekStart: dateState.weekStart,
selectedDate: dateState.selectedDate, selectedDate: dateState.selectedDate,
eventController: _eventController, eventController: _eventController,
selectedDateFromSideBarCalender: context
.watch<DateSelectionBloc>()
.state
.selectedDateFromSideBarCalender,
),
); );
}, },
); );
@ -240,20 +232,9 @@ class _BookingPageState extends State<BookingPage> {
], ],
), ),
), ),
),
),
),
); );
} }
String _getMonthYearText(DateTime start, DateTime end) {
final startMonth = DateFormat('MMM').format(start);
final endMonth = DateFormat('MMM').format(end);
final year = start.year == end.year
? start.year.toString()
: '${start.year}-${end.year}';
if (start.month == end.month) {
return '$startMonth $year';
} else {
return '$startMonth - $endMonth $year';
}
}
} }

View File

@ -0,0 +1,60 @@
import 'package:calendar_view/calendar_view.dart';
import 'package:flutter/material.dart';
import 'package:intl/intl.dart';
import 'package:syncrow_web/utils/color_manager.dart';
class EventTileWidget extends StatelessWidget {
final List<CalendarEventData<Object?>> events;
const EventTileWidget({
super.key,
required this.events,
});
@override
Widget build(BuildContext context) {
return Container(
margin: const EdgeInsets.symmetric(vertical: 2, horizontal: 2),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: events.map((event) {
final bool isEventEnded =
event.endTime != null && event.endTime!.isBefore(DateTime.now());
return Expanded(
child: Container(
width: double.infinity,
padding: const EdgeInsets.all(6),
decoration: BoxDecoration(
color: isEventEnded
? ColorsManager.lightGrayBorderColor
: ColorsManager.blue1.withOpacity(0.25),
borderRadius: BorderRadius.circular(6),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
DateFormat('h:mm a').format(event.startTime!),
style: const TextStyle(
fontWeight: FontWeight.bold,
fontSize: 12,
color: Colors.black87,
),
),
const SizedBox(height: 2),
Text(
event.title,
style: const TextStyle(
fontSize: 12,
color: ColorsManager.blackColor,
),
),
],
),
),
);
}).toList(),
),
);
}
}

View File

@ -0,0 +1,91 @@
import 'package:flutter/material.dart';
import 'dart:math' as math;
class HatchedColumnBackground extends StatelessWidget {
final Color backgroundColor;
final Color lineColor;
final double opacity;
final double stripeSpacing;
final BorderRadius? borderRadius;
const HatchedColumnBackground({
super.key,
required this.backgroundColor,
required this.lineColor,
this.opacity = 0.15,
this.stripeSpacing = 12,
this.borderRadius,
});
@override
Widget build(BuildContext context) {
return CustomPaint(
painter: _HatchedBackgroundPainter(
backgroundColor: backgroundColor,
opacity: opacity,
lineColor: lineColor,
stripeSpacing: stripeSpacing,
borderRadius: borderRadius,
),
size: Size.infinite,
);
}
}
class _HatchedBackgroundPainter extends CustomPainter {
final Color backgroundColor;
final double opacity;
final Color lineColor;
final double stripeSpacing;
final BorderRadius? borderRadius;
_HatchedBackgroundPainter({
required this.backgroundColor,
required this.opacity,
required this.lineColor,
required this.stripeSpacing,
this.borderRadius,
});
@override
void paint(Canvas canvas, Size size) {
final rect = Rect.fromLTWH(0, 0, size.width, size.height);
final RRect rrect = borderRadius?.toRRect(rect) ??
RRect.fromRectAndRadius(rect, Radius.zero);
final backgroundPaint = Paint()
..color = backgroundColor.withOpacity(0.02)
..style = PaintingStyle.fill;
canvas.drawRRect(rrect, backgroundPaint);
canvas.save();
canvas.clipRRect(rrect);
final linePaint = Paint()
..color = lineColor
..strokeWidth = 0.5
..style = PaintingStyle.stroke;
final maxExtent =
math.sqrt(size.width * size.width + size.height * size.height);
canvas.translate(0, size.height);
canvas.rotate(-math.pi / 4);
double y = -maxExtent;
while (y < maxExtent) {
canvas.drawLine(
Offset(-maxExtent, y),
Offset(maxExtent, y),
linePaint,
);
y += stripeSpacing;
}
canvas.restore();
}
@override
bool shouldRepaint(covariant _HatchedBackgroundPainter oldDelegate) {
return backgroundColor != oldDelegate.backgroundColor ||
opacity != oldDelegate.opacity ||
lineColor != oldDelegate.lineColor ||
stripeSpacing != oldDelegate.stripeSpacing ||
borderRadius != oldDelegate.borderRadius;
}
}

View File

@ -0,0 +1,49 @@
import 'package:flutter/material.dart';
import 'package:syncrow_web/utils/color_manager.dart';
class TimeLineWidget extends StatelessWidget {
final DateTime date;
const TimeLineWidget({Key? key, required this.date}) : super(key: key);
@override
Widget build(BuildContext context) {
int hour =
date.hour == 0 ? 12 : (date.hour > 12 ? date.hour - 12 : date.hour);
String period = date.hour >= 12 ? 'PM' : 'AM';
return Container(
height: 60,
alignment: Alignment.center,
child: RichText(
text: TextSpan(
children: [
TextSpan(
text: '$hour',
style: const TextStyle(
fontWeight: FontWeight.w700,
fontSize: 24,
color: ColorsManager.blackColor,
),
),
WidgetSpan(
child: Padding(
padding: const EdgeInsets.only(left: 2, top: 6),
child: Text(
period,
style: const TextStyle(
fontWeight: FontWeight.w400,
fontSize: 12,
color: ColorsManager.blackColor,
letterSpacing: 1,
),
),
),
alignment: PlaceholderAlignment.baseline,
baseline: TextBaseline.alphabetic,
),
],
),
),
);
}
}

View File

@ -0,0 +1,39 @@
import 'package:flutter/material.dart';
import 'package:intl/intl.dart';
import 'package:syncrow_web/utils/color_manager.dart';
class WeekDayHeader extends StatelessWidget {
final DateTime date;
final bool isSelectedDay;
const WeekDayHeader({
Key? key,
required this.date,
required this.isSelectedDay,
}) : super(key: key);
@override
Widget build(BuildContext context) {
return Column(
children: [
Text(
DateFormat('EEE').format(date).toUpperCase(),
style: TextStyle(
fontWeight: FontWeight.w400,
fontSize: 14,
color: isSelectedDay ? Colors.blue : Colors.black,
),
),
Text(
DateFormat('d').format(date),
style: TextStyle(
fontWeight: FontWeight.w700,
fontSize: 20,
color:
isSelectedDay ? ColorsManager.blue1 : ColorsManager.blackColor,
),
),
],
);
}
}

View File

@ -0,0 +1,76 @@
import 'package:flutter/material.dart';
import 'package:intl/intl.dart';
import 'package:syncrow_web/utils/color_manager.dart';
class WeekNavigation extends StatelessWidget {
final DateTime weekStart;
final DateTime weekEnd;
final VoidCallback onPreviousWeek;
final VoidCallback onNextWeek;
const WeekNavigation({
Key? key,
required this.weekStart,
required this.weekEnd,
required this.onPreviousWeek,
required this.onNextWeek,
}) : super(key: key);
@override
Widget build(BuildContext context) {
return Container(
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 5),
decoration: BoxDecoration(
color: ColorsManager.circleRolesBackground,
borderRadius: BorderRadius.circular(10),
boxShadow: const [
BoxShadow(
color: ColorsManager.lightGrayColor,
blurRadius: 4,
offset: Offset(0, 1),
),
],
),
child: Row(
children: [
IconButton(
iconSize: 15,
icon: const Icon(Icons.arrow_back_ios,
color: ColorsManager.lightGrayColor),
onPressed: onPreviousWeek,
),
const SizedBox(width: 10),
Text(
_getMonthYearText(weekStart, weekEnd),
style: const TextStyle(
color: ColorsManager.lightGrayColor,
fontSize: 14,
fontWeight: FontWeight.w400,
),
),
const SizedBox(width: 10),
IconButton(
iconSize: 15,
icon: const Icon(Icons.arrow_forward_ios,
color: ColorsManager.lightGrayColor),
onPressed: onNextWeek,
),
],
),
);
}
String _getMonthYearText(DateTime start, DateTime end) {
final startMonth = DateFormat('MMM').format(start);
final endMonth = DateFormat('MMM').format(end);
final year = start.year == end.year
? start.year.toString()
: '${start.year}-${end.year}';
if (start.month == end.month) {
return '$startMonth $year';
} else {
return '$startMonth - $endMonth $year';
}
}
}

View File

@ -1,6 +1,9 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:calendar_view/calendar_view.dart'; import 'package:calendar_view/calendar_view.dart';
import 'package:intl/intl.dart'; import 'package:syncrow_web/pages/access_management/booking_system/presentation/view/widgets/event_tile_widget.dart';
import 'package:syncrow_web/pages/access_management/booking_system/presentation/view/widgets/hatched_column_background.dart';
import 'package:syncrow_web/pages/access_management/booking_system/presentation/view/widgets/time_line_widget.dart';
import 'package:syncrow_web/pages/access_management/booking_system/presentation/view/widgets/week_day_header.dart';
import 'package:syncrow_web/utils/color_manager.dart'; import 'package:syncrow_web/utils/color_manager.dart';
class WeeklyCalendarPage extends StatelessWidget { class WeeklyCalendarPage extends StatelessWidget {
@ -9,6 +12,7 @@ class WeeklyCalendarPage extends StatelessWidget {
final EventController eventController; final EventController eventController;
final String? startTime; final String? startTime;
final String? endTime; final String? endTime;
final DateTime? selectedDateFromSideBarCalender;
const WeeklyCalendarPage({ const WeeklyCalendarPage({
super.key, super.key,
@ -17,37 +21,81 @@ class WeeklyCalendarPage extends StatelessWidget {
required this.eventController, required this.eventController,
this.startTime, this.startTime,
this.endTime, this.endTime,
this.selectedDateFromSideBarCalender,
}); });
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final startHour = _parseHour(startTime, defaultValue: 0); final startHour = _parseHour(startTime, defaultValue: 0);
final endHour = _parseHour(endTime, defaultValue: 24); final endHour = _parseHour(endTime, defaultValue: 24);
if (endTime == null || endTime!.isEmpty) { if (endTime == null || endTime!.isEmpty) {
return const Center( return const Center(
child: Text( child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(
Icons.calendar_today,
color: ColorsManager.lightGrayColor,
size: 80,
),
SizedBox(height: 20),
Text(
'Please select a bookable space to view the calendar.', 'Please select a bookable space to view the calendar.',
style: TextStyle(fontSize: 16, fontWeight: FontWeight.w500), style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.w500,
color: ColorsManager.lightGrayColor),
),
],
), ),
); );
} }
final weekDays = _getWeekDays(weekStart); final weekDays = _getWeekDays(weekStart);
final selectedDayIndex =
weekDays.indexWhere((d) => isSameDay(d, selectedDate));
final selectedSidebarIndex = selectedDateFromSideBarCalender == null
? -1
: weekDays
.indexWhere((d) => isSameDay(d, selectedDateFromSideBarCalender!));
const double timeLineWidth = 80;
const int totalDays = 7;
final DateTime highlightStart = DateTime(2025, 7, 10);
final DateTime highlightEnd = DateTime(2025, 7, 19);
return LayoutBuilder( return LayoutBuilder(
builder: (context, constraints) { builder: (context, constraints) {
final double calendarWidth = constraints.maxWidth; final double calendarWidth = constraints.maxWidth;
const double timeLineWidth = 80;
const int totalDays = 7;
final double dayColumnWidth = final double dayColumnWidth =
(calendarWidth - timeLineWidth) / totalDays - 0.1; (calendarWidth - timeLineWidth) / totalDays - 0.1;
final selectedDayIndex = bool isInRange(DateTime date, DateTime start, DateTime end) {
weekDays.indexWhere((d) => isSameDay(d, selectedDate)); return !date.isBefore(start) && !date.isAfter(end);
}
return Padding( return Padding(
padding: const EdgeInsets.only(left: 25.0, right: 25.0, top: 25), padding: const EdgeInsets.only(left: 25.0, right: 25.0, top: 25),
child: Stack( child: Stack(
children: [ children: [
WeekView( WeekView(
weekDetectorBuilder: ({
required date,
required height,
required heightPerMinute,
required minuteSlotSize,
required width,
}) {
return isInRange(date, highlightStart, highlightEnd)
? HatchedColumnBackground(
backgroundColor: ColorsManager.grey800,
lineColor: ColorsManager.textGray,
opacity: 0.3,
stripeSpacing: 12,
borderRadius: BorderRadius.circular(8),
)
: const SizedBox();
},
pageViewPhysics: const NeverScrollableScrollPhysics(), pageViewPhysics: const NeverScrollableScrollPhysics(),
key: ValueKey(weekStart), key: ValueKey(weekStart),
controller: eventController, controller: eventController,
@ -64,84 +112,19 @@ class WeeklyCalendarPage extends StatelessWidget {
height: 0, height: 0,
), ),
weekDayBuilder: (date) { weekDayBuilder: (date) {
final weekDays = _getWeekDays(weekStart); return WeekDayHeader(
final selectedDayIndex = date: date,
weekDays.indexWhere((d) => isSameDay(d, selectedDate)); isSelectedDay: isSameDay(date, selectedDate),
final index = weekDays.indexWhere((d) => isSameDay(d, date));
final isSelectedDay = index == selectedDayIndex;
final isToday = isSameDay(date, DateTime.now());
return Column(
children: [
Text(
DateFormat('EEE').format(date).toUpperCase(),
style: TextStyle(
fontWeight: FontWeight.w400,
fontSize: 14,
color: isSelectedDay ? Colors.blue : Colors.black,
),
),
Text(
DateFormat('d').format(date),
style: TextStyle(
fontWeight: FontWeight.w700,
fontSize: 20,
color: isSelectedDay
? ColorsManager.blue1
: ColorsManager.blackColor,
),
),
],
); );
}, },
timeLineBuilder: (date) { timeLineBuilder: (date) {
int hour = date.hour == 0 return TimeLineWidget(date: date);
? 12
: (date.hour > 12 ? date.hour - 12 : date.hour);
String period = date.hour >= 12 ? 'PM' : 'AM';
return Container(
height: 60,
alignment: Alignment.center,
child: RichText(
text: TextSpan(
children: [
TextSpan(
text: '$hour',
style: const TextStyle(
fontWeight: FontWeight.w700,
fontSize: 24,
color: ColorsManager.blackColor,
),
),
WidgetSpan(
child: Padding(
padding: const EdgeInsets.only(left: 2, top: 6),
child: Text(
period,
style: const TextStyle(
fontWeight: FontWeight.w400,
fontSize: 12,
color: ColorsManager.blackColor,
letterSpacing: 1,
),
),
),
alignment: PlaceholderAlignment.baseline,
baseline: TextBaseline.alphabetic,
),
],
),
),
);
}, },
timeLineWidth: timeLineWidth, timeLineWidth: timeLineWidth,
weekPageHeaderBuilder: (start, end) => Container(), weekPageHeaderBuilder: (start, end) => Container(),
weekTitleHeight: 60, weekTitleHeight: 60,
weekNumberBuilder: (firstDayOfWeek) => Padding( weekNumberBuilder: (firstDayOfWeek) => Padding(
padding: const EdgeInsets.only( padding: const EdgeInsets.only(right: 15, bottom: 10),
right: 15,
bottom: 10,
),
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.end, crossAxisAlignment: CrossAxisAlignment.end,
mainAxisAlignment: MainAxisAlignment.end, mainAxisAlignment: MainAxisAlignment.end,
@ -158,49 +141,8 @@ class WeeklyCalendarPage extends StatelessWidget {
), ),
), ),
eventTileBuilder: (date, events, boundary, start, end) { eventTileBuilder: (date, events, boundary, start, end) {
return Container( return EventTileWidget(
margin: events: events,
const EdgeInsets.symmetric(vertical: 2, horizontal: 2),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: events.map((event) {
final bool isEventEnded = event.endTime != null &&
event.endTime!.isBefore(DateTime.now());
return Expanded(
child: Container(
width: double.infinity,
padding: const EdgeInsets.all(6),
decoration: BoxDecoration(
color: isEventEnded
? ColorsManager.lightGrayBorderColor
: ColorsManager.blue1.withOpacity(0.25),
borderRadius: BorderRadius.circular(6),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
DateFormat('h:mm a').format(event.startTime!),
style: const TextStyle(
fontWeight: FontWeight.bold,
fontSize: 12,
color: Colors.black87,
),
),
const SizedBox(height: 2),
Text(
event.title,
style: const TextStyle(
fontSize: 12,
color: ColorsManager.blackColor,
),
),
],
),
),
);
}).toList(),
),
); );
}, },
), ),
@ -219,6 +161,22 @@ class WeeklyCalendarPage extends StatelessWidget {
), ),
), ),
), ),
if (selectedSidebarIndex >= 0 &&
selectedSidebarIndex != selectedDayIndex)
Positioned(
left: (timeLineWidth + 3) +
(dayColumnWidth - 8) * (selectedSidebarIndex - 0.01),
top: 0,
bottom: 0,
width: dayColumnWidth,
child: IgnorePointer(
child: Container(
margin: const EdgeInsets.symmetric(
vertical: 0, horizontal: 4),
color: Colors.orange.withOpacity(0.14),
),
),
),
Positioned( Positioned(
right: 0, right: 0,
top: 50, top: 50,

View File

@ -29,7 +29,9 @@ class CountdownModeButtons extends StatelessWidget {
children: [ children: [
Expanded( Expanded(
child: DefaultButton( child: DefaultButton(
elevation: 2.5,
height: 40, height: 40,
borderRadius: 8,
onPressed: () => Navigator.pop(context), onPressed: () => Navigator.pop(context),
backgroundColor: ColorsManager.boxColor, backgroundColor: ColorsManager.boxColor,
child: Text('Cancel', style: context.textTheme.bodyMedium), child: Text('Cancel', style: context.textTheme.bodyMedium),
@ -39,6 +41,8 @@ class CountdownModeButtons extends StatelessWidget {
Expanded( Expanded(
child: isActive child: isActive
? DefaultButton( ? DefaultButton(
elevation: 2.5,
borderRadius: 8,
height: 40, height: 40,
onPressed: () { onPressed: () {
context.read<ScheduleBloc>().add( context.read<ScheduleBloc>().add(
@ -49,10 +53,12 @@ class CountdownModeButtons extends StatelessWidget {
), ),
); );
}, },
backgroundColor: Colors.red, backgroundColor: ColorsManager.red100,
child: const Text('Stop'), child: const Text('Stop'),
) )
: DefaultButton( : DefaultButton(
elevation: 2.5,
borderRadius: 8,
height: 40, height: 40,
onPressed: () { onPressed: () {
context.read<ScheduleBloc>().add( context.read<ScheduleBloc>().add(
@ -63,7 +69,7 @@ class CountdownModeButtons extends StatelessWidget {
countDownCode: countDownCode), countDownCode: countDownCode),
); );
}, },
backgroundColor: ColorsManager.primaryColor, backgroundColor: ColorsManager.primaryColorWithOpacity,
child: const Text('Save'), child: const Text('Save'),
), ),
), ),

View File

@ -226,6 +226,7 @@ class _CountdownInchingViewState extends State<CountdownInchingView> {
index.toString().padLeft(2, '0'), index.toString().padLeft(2, '0'),
style: TextStyle( style: TextStyle(
fontSize: 24, fontSize: 24,
fontWeight: FontWeight.w400,
color: isActive ? ColorsManager.grayColor : Colors.black, color: isActive ? ColorsManager.grayColor : Colors.black,
), ),
), ),
@ -240,7 +241,8 @@ class _CountdownInchingViewState extends State<CountdownInchingView> {
label, label,
style: const TextStyle( style: const TextStyle(
color: ColorsManager.grayColor, color: ColorsManager.grayColor,
fontSize: 18, fontSize: 24,
fontWeight: FontWeight.w400,
), ),
), ),
], ],

View File

@ -31,12 +31,11 @@ class BuildScheduleView extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return BlocProvider( return BlocProvider(
create: (_) => ScheduleBloc( create: (_) => ScheduleBloc(deviceId: deviceUuid,)
deviceId: deviceUuid,
)
..add(ScheduleGetEvent(category: category)) ..add(ScheduleGetEvent(category: category))
..add(ScheduleFetchStatusEvent( ..add(ScheduleFetchStatusEvent(
deviceId: deviceUuid, countdownCode: countdownCode ?? '')), deviceId: deviceUuid,
countdownCode: countdownCode ?? '')),
child: Dialog( child: Dialog(
backgroundColor: Colors.white, backgroundColor: Colors.white,
insetPadding: const EdgeInsets.all(20), insetPadding: const EdgeInsets.all(20),
@ -77,7 +76,8 @@ class BuildScheduleView extends StatelessWidget {
category: category, category: category,
time: '', time: '',
function: Status( function: Status(
code: code.toString(), value: null), code: code.toString(),
value: true),
days: [], days: [],
), ),
isEdit: false, isEdit: false,

View File

@ -13,9 +13,9 @@ class ScheduleHeader extends StatelessWidget {
Text( Text(
'Scheduling', 'Scheduling',
style: TextStyle( style: TextStyle(
fontWeight: FontWeight.bold, color: ColorsManager.primaryColorWithOpacity,
fontSize: 22, fontWeight: FontWeight.w700,
color: ColorsManager.dialogBlueTitle, fontSize: 30,
), ),
), ),
Container( Container(

View File

@ -27,7 +27,7 @@ class ScheduleManagementUI extends StatelessWidget {
width: 170, width: 170,
height: 40, height: 40,
child: DefaultButton( child: DefaultButton(
borderColor: ColorsManager.boxColor, borderColor: ColorsManager.grayColor.withOpacity(0.5),
padding: 2, padding: 2,
backgroundColor: ColorsManager.graysColor, backgroundColor: ColorsManager.graysColor,
borderRadius: 15, borderRadius: 15,

View File

@ -19,6 +19,8 @@ class ScheduleModeButtons extends StatelessWidget {
children: [ children: [
Expanded( Expanded(
child: DefaultButton( child: DefaultButton(
elevation: 2.5,
borderRadius: 8,
height: 40, height: 40,
onPressed: () { onPressed: () {
Navigator.pop(context); Navigator.pop(context);
@ -33,9 +35,11 @@ class ScheduleModeButtons extends StatelessWidget {
const SizedBox(width: 20), const SizedBox(width: 20),
Expanded( Expanded(
child: DefaultButton( child: DefaultButton(
elevation: 2.5,
borderRadius: 8,
height: 40, height: 40,
onPressed: onSave, onPressed: onSave,
backgroundColor: ColorsManager.primaryColor, backgroundColor: ColorsManager.primaryColorWithOpacity,
child: const Text('Save'), child: const Text('Save'),
), ),
), ),

View File

@ -35,12 +35,12 @@ class ScheduleModeSelector extends StatelessWidget {
), ),
const SizedBox(height: 4), const SizedBox(height: 4),
Row( Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [ children: [
_buildRadioTile( _buildRadioTile(
context, 'Countdown', ScheduleModes.countdown, currentMode), context, 'Countdown', ScheduleModes.countdown, currentMode),
_buildRadioTile( _buildRadioTile(
context, 'Schedule', ScheduleModes.schedule, currentMode), context, 'Schedule', ScheduleModes.schedule, currentMode),
const Spacer(flex: 1),
// _buildRadioTile( // _buildRadioTile(
// context, 'Circulate', ScheduleModes.circulate, currentMode), // context, 'Circulate', ScheduleModes.circulate, currentMode),
// _buildRadioTile( // _buildRadioTile(
@ -65,6 +65,7 @@ class ScheduleModeSelector extends StatelessWidget {
style: context.textTheme.bodySmall!.copyWith( style: context.textTheme.bodySmall!.copyWith(
fontSize: 13, fontSize: 13,
color: ColorsManager.blackColor, color: ColorsManager.blackColor,
fontWeight: FontWeight.w400,
), ),
), ),
leading: Radio<ScheduleModes>( leading: Radio<ScheduleModes>(

View File

@ -1,6 +1,8 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:syncrow_web/pages/device_managment/schedule_device/schedule_widgets/schedule_mode_buttons.dart';
import 'package:syncrow_web/pages/device_managment/water_heater/models/schedule_entry.dart'; import 'package:syncrow_web/pages/device_managment/water_heater/models/schedule_entry.dart';
import 'package:syncrow_web/pages/device_managment/all_devices/models/device_status.dart'; import 'package:syncrow_web/pages/device_managment/all_devices/models/device_status.dart';
import 'package:syncrow_web/utils/color_manager.dart';
class ScheduleDialogHelper { class ScheduleDialogHelper {
static const List<String> allDays = [ static const List<String> allDays = [
@ -56,8 +58,9 @@ class ScheduleDialogHelper {
Text( Text(
isEdit ? 'Edit Schedule' : 'Add Schedule', isEdit ? 'Edit Schedule' : 'Add Schedule',
style: Theme.of(context).textTheme.titleLarge!.copyWith( style: Theme.of(context).textTheme.titleLarge!.copyWith(
color: Colors.blue, color: ColorsManager.primaryColorWithOpacity,
fontWeight: FontWeight.bold, fontWeight: FontWeight.w700,
fontSize: 30,
), ),
), ),
const SizedBox(), const SizedBox(),
@ -69,9 +72,9 @@ class ScheduleDialogHelper {
height: 40, height: 40,
child: ElevatedButton( child: ElevatedButton(
style: ElevatedButton.styleFrom( style: ElevatedButton.styleFrom(
backgroundColor: Colors.grey[200], backgroundColor: ColorsManager.boxColor,
shape: RoundedRectangleBorder( shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(15), borderRadius: BorderRadius.circular(8),
), ),
), ),
onPressed: () async { onPressed: () async {
@ -110,19 +113,8 @@ class ScheduleDialogHelper {
], ],
), ),
actions: [ actions: [
SizedBox( ScheduleModeButtons(
width: 100, onSave: () {
child: OutlinedButton(
onPressed: () {
Navigator.pop(ctx, null);
},
child: const Text('Cancel'),
),
),
SizedBox(
width: 100,
child: ElevatedButton(
onPressed: () {
dynamic temp; dynamic temp;
if (deviceType == 'CUR_2') { if (deviceType == 'CUR_2') {
temp = functionOn! ? 'open' : 'close'; temp = functionOn! ? 'open' : 'close';
@ -141,8 +133,7 @@ class ScheduleDialogHelper {
); );
Navigator.pop(ctx, entry); Navigator.pop(ctx, entry);
}, },
child: const Text('Save'), ),
)),
], ],
); );
}, },

View File

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

View File

@ -16,21 +16,37 @@ import 'package:syncrow_web/services/api/http_service.dart';
import 'package:syncrow_web/utils/theme/responsive_text_theme.dart'; import 'package:syncrow_web/utils/theme/responsive_text_theme.dart';
import 'package:syncrow_web/web_layout/web_scaffold.dart'; import 'package:syncrow_web/web_layout/web_scaffold.dart';
class SpaceManagementPage extends StatelessWidget { class SpaceManagementPage extends StatefulWidget {
const SpaceManagementPage({super.key}); const SpaceManagementPage({super.key});
@override
State<SpaceManagementPage> createState() => _SpaceManagementPageState();
}
class _SpaceManagementPageState extends State<SpaceManagementPage> {
late final CommunitiesBloc communitiesBloc;
@override
void initState() {
communitiesBloc = CommunitiesBloc(
communitiesService: DebouncedCommunitiesService(
RemoteCommunitiesService(HTTPService()),
),
)..add(const LoadCommunities(LoadCommunitiesParam()));
super.initState();
}
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return MultiBlocProvider( return MultiBlocProvider(
providers: [ providers: [
BlocProvider.value(value: communitiesBloc),
BlocProvider( BlocProvider(
create: (context) => CommunitiesBloc( create: (context) => CommunitiesTreeSelectionBloc(
communitiesService: DebouncedCommunitiesService( communitiesBloc: communitiesBloc,
RemoteCommunitiesService(HTTPService()),
), ),
)..add(const LoadCommunities(LoadCommunitiesParam())),
), ),
BlocProvider(create: (context) => CommunitiesTreeSelectionBloc()),
BlocProvider( BlocProvider(
create: (context) => SpaceDetailsBloc( create: (context) => SpaceDetailsBloc(
UniqueSubspacesDecorator( UniqueSubspacesDecorator(

View File

@ -1,13 +1,17 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:syncrow_web/pages/space_management_v2/main_module/models/space_connection_model.dart'; import 'package:syncrow_web/pages/space_management_v2/main_module/models/space_connection_model.dart';
import 'package:syncrow_web/pages/space_management_v2/main_module/models/space_reorder_data_model.dart';
import 'package:syncrow_web/pages/space_management_v2/main_module/painters/spaces_connections_arrow_painter.dart'; import 'package:syncrow_web/pages/space_management_v2/main_module/painters/spaces_connections_arrow_painter.dart';
import 'package:syncrow_web/pages/space_management_v2/main_module/widgets/create_space_button.dart';
import 'package:syncrow_web/pages/space_management_v2/main_module/widgets/space_card_widget.dart'; import 'package:syncrow_web/pages/space_management_v2/main_module/widgets/space_card_widget.dart';
import 'package:syncrow_web/pages/space_management_v2/main_module/widgets/space_cell.dart'; import 'package:syncrow_web/pages/space_management_v2/main_module/widgets/space_cell.dart';
import 'package:syncrow_web/pages/space_management_v2/modules/communities/domain/models/community_model.dart'; import 'package:syncrow_web/pages/space_management_v2/modules/communities/domain/models/community_model.dart';
import 'package:syncrow_web/pages/space_management_v2/modules/communities/domain/models/space_model.dart'; import 'package:syncrow_web/pages/space_management_v2/modules/communities/domain/models/space_model.dart';
import 'package:syncrow_web/pages/space_management_v2/modules/communities/presentation/bloc/communities_bloc.dart';
import 'package:syncrow_web/pages/space_management_v2/modules/communities/presentation/communities_tree_selection_bloc/communities_tree_selection_bloc.dart'; import 'package:syncrow_web/pages/space_management_v2/modules/communities/presentation/communities_tree_selection_bloc/communities_tree_selection_bloc.dart';
import 'package:syncrow_web/pages/space_management_v2/modules/space_details/presentation/helpers/space_details_dialog_helper.dart'; import 'package:syncrow_web/pages/space_management_v2/modules/space_details/presentation/helpers/space_details_dialog_helper.dart';
import 'package:syncrow_web/utils/extension/build_context_x.dart';
class CommunityStructureCanvas extends StatefulWidget { class CommunityStructureCanvas extends StatefulWidget {
const CommunityStructureCanvas({ const CommunityStructureCanvas({
@ -31,8 +35,9 @@ class _CommunityStructureCanvasState extends State<CommunityStructureCanvas>
final double _horizontalSpacing = 150.0; final double _horizontalSpacing = 150.0;
final double _verticalSpacing = 120.0; final double _verticalSpacing = 120.0;
late TransformationController _transformationController; late final TransformationController _transformationController;
late AnimationController _animationController; late final AnimationController _animationController;
SpaceReorderDataModel? _draggedData;
@override @override
void initState() { void initState() {
@ -97,7 +102,7 @@ class _CommunityStructureCanvasState extends State<CommunityStructureCanvas>
final position = _positions[space.uuid]; final position = _positions[space.uuid];
if (position == null) return; if (position == null) return;
const scale = 1.5; const scale = 1;
final viewSize = context.size; final viewSize = context.size;
if (viewSize == null) return; if (viewSize == null) return;
@ -112,16 +117,33 @@ class _CommunityStructureCanvasState extends State<CommunityStructureCanvas>
_runAnimation(matrix); _runAnimation(matrix);
} }
void _onReorder(SpaceReorderDataModel data, int newIndex) {
final newCommunity = widget.community.copyWith();
final children = data.parent?.children ?? newCommunity.spaces;
final oldIndex = children.indexWhere((s) => s.uuid == data.space.uuid);
if (oldIndex != -1) {
final item = children.removeAt(oldIndex);
if (newIndex > oldIndex) {
children.insert(newIndex - 1, item);
} else {
children.insert(newIndex, item);
}
}
context.read<CommunitiesBloc>().add(
CommunitiesUpdateCommunity(newCommunity),
);
}
void _onSpaceTapped(SpaceModel? space) { void _onSpaceTapped(SpaceModel? space) {
context.read<CommunitiesTreeSelectionBloc>().add( context.read<CommunitiesTreeSelectionBloc>().add(
SelectSpaceEvent(community: widget.community, space: space), SelectSpaceEvent(community: widget.community, space: space),
); );
} }
void _resetSelectionAndZoom() { void _resetSelectionAndZoom([CommunityModel? community]) {
context.read<CommunitiesTreeSelectionBloc>().add( context.read<CommunitiesTreeSelectionBloc>().add(
SelectSpaceEvent( SelectSpaceEvent(
community: widget.community, community: community ?? widget.community,
space: null, space: null,
), ),
); );
@ -182,7 +204,8 @@ class _CommunityStructureCanvasState extends State<CommunityStructureCanvas>
_positions.clear(); _positions.clear();
final community = widget.community; final community = widget.community;
_calculateLayout(community.spaces, 0, {}); final levelXOffset = <int, double>{};
_calculateLayout(community.spaces, 0, levelXOffset);
final selectedSpace = widget.selectedSpace; final selectedSpace = widget.selectedSpace;
final highlightedUuids = <String>{}; final highlightedUuids = <String>{};
@ -193,7 +216,24 @@ class _CommunityStructureCanvasState extends State<CommunityStructureCanvas>
final widgets = <Widget>[]; final widgets = <Widget>[];
final connections = <SpaceConnectionModel>[]; final connections = <SpaceConnectionModel>[];
_generateWidgets(community.spaces, widgets, connections, highlightedUuids); _generateWidgets(
widget.community.spaces,
widgets,
connections,
highlightedUuids,
community: widget.community,
);
final createButtonX = levelXOffset[0] ?? 0.0;
const createButtonY = 0.0;
widgets.add(
Positioned(
left: createButtonX,
top: createButtonY,
child: CreateSpaceButton(communityUuid: widget.community.uuid),
),
);
return [ return [
CustomPaint( CustomPaint(
@ -211,22 +251,30 @@ class _CommunityStructureCanvasState extends State<CommunityStructureCanvas>
List<SpaceModel> spaces, List<SpaceModel> spaces,
List<Widget> widgets, List<Widget> widgets,
List<SpaceConnectionModel> connections, List<SpaceConnectionModel> connections,
Set<String> highlightedUuids, Set<String> highlightedUuids, {
) { CommunityModel? community,
for (final space in spaces) { SpaceModel? parent,
}) {
if (spaces.isNotEmpty) {
final firstChildPos = _positions[spaces.first.uuid]!;
final targetPos = Offset(
firstChildPos.dx - (_horizontalSpacing / 4),
firstChildPos.dy,
);
widgets.add(_buildDropTarget(parent, community, 0, targetPos));
}
for (var i = 0; i < spaces.length; i++) {
final space = spaces[i];
final position = _positions[space.uuid]; final position = _positions[space.uuid];
if (position == null) continue; if (position == null) {
continue;
}
final isHighlighted = highlightedUuids.contains(space.uuid); final isHighlighted = highlightedUuids.contains(space.uuid);
final hasNoSelectedSpace = widget.selectedSpace == null; final hasNoSelectedSpace = widget.selectedSpace == null;
widgets.add( final spaceCard = SpaceCardWidget(
Positioned(
left: position.dx,
top: position.dy,
width: _cardWidth,
height: _cardHeight,
child: SpaceCardWidget(
buildSpaceContainer: () { buildSpaceContainer: () {
return Opacity( return Opacity(
opacity: hasNoSelectedSpace || isHighlighted ? 1.0 : 0.5, opacity: hasNoSelectedSpace || isHighlighted ? 1.0 : 0.5,
@ -241,28 +289,140 @@ class _CommunityStructureCanvasState extends State<CommunityStructureCanvas>
), ),
); );
}, },
onTap: () => SpaceDetailsDialogHelper.showCreate(context), onTap: () => SpaceDetailsDialogHelper.showCreate(
context,
communityUuid: widget.community.uuid,
),
);
final reorderData = SpaceReorderDataModel(
space: space,
parent: parent,
community: community,
);
widgets.add(
Positioned(
left: position.dx,
top: position.dy,
width: _cardWidth,
height: _cardHeight,
child: Draggable<SpaceReorderDataModel>(
data: reorderData,
feedback: Material(
color: Colors.transparent,
child: Opacity(
opacity: 0.2,
child: SizedBox(
width: _cardWidth,
height: _cardHeight,
child: spaceCard,
),
),
),
onDragStarted: () => setState(() => _draggedData = reorderData),
onDragEnd: (_) => setState(() => _draggedData = null),
onDraggableCanceled: (_, __) => setState(() => _draggedData = null),
childWhenDragging: Opacity(opacity: 0.4, child: spaceCard),
child: spaceCard,
), ),
), ),
); );
final targetPos = Offset(
position.dx + _cardWidth + (_horizontalSpacing / 4) - 20,
position.dy,
);
widgets.add(_buildDropTarget(parent, community, i + 1, targetPos));
for (final child in space.children) { for (final child in space.children) {
connections.add( connections.add(SpaceConnectionModel(from: space.uuid, to: child.uuid));
SpaceConnectionModel(from: space.uuid, to: child.uuid), }
if (space.children.isNotEmpty) {
_generateWidgets(
space.children,
widgets,
connections,
highlightedUuids,
parent: space,
); );
} }
_generateWidgets(space.children, widgets, connections, highlightedUuids);
} }
} }
Widget _buildDropTarget(
SpaceModel? parent,
CommunityModel? community,
int index,
Offset position,
) {
return Positioned(
left: position.dx,
top: position.dy,
width: 40,
height: _cardHeight,
child: DragTarget<SpaceReorderDataModel>(
builder: (context, candidateData, rejectedData) {
if (_draggedData == null) {
return const SizedBox();
}
final isTargetForDragged = (_draggedData?.parent?.uuid == parent?.uuid &&
_draggedData?.community == null) ||
(_draggedData?.community?.uuid == community?.uuid &&
_draggedData?.parent == null);
if (!isTargetForDragged) {
return const SizedBox();
}
return Container(
width: 40,
height: _cardHeight,
decoration: BoxDecoration(
color: context.theme.colorScheme.primary.withValues(
alpha: candidateData.isNotEmpty ? 0.7 : 0.3,
),
borderRadius: BorderRadius.circular(8),
),
child: Icon(
Icons.add,
color: context.theme.colorScheme.onPrimary,
),
);
},
onWillAcceptWithDetails: (data) {
final children = parent?.children ?? community?.spaces ?? [];
final isSameParent = (data.data.parent?.uuid == parent?.uuid &&
data.data.community == null) ||
(data.data.community?.uuid == community?.uuid &&
data.data.parent == null);
if (!isSameParent) {
return false;
}
final oldIndex =
children.indexWhere((s) => s.uuid == data.data.space.uuid);
if (oldIndex == index || oldIndex == index - 1) {
return false;
}
return true;
},
onAcceptWithDetails: (data) => _onReorder(data.data, index),
),
);
}
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final treeWidgets = _buildTreeWidgets(); final treeWidgets = _buildTreeWidgets();
return InteractiveViewer( return InteractiveViewer(
transformationController: _transformationController, transformationController: _transformationController,
boundaryMargin: EdgeInsets.symmetric( boundaryMargin: EdgeInsets.symmetric(
horizontal: MediaQuery.sizeOf(context).width * 0.3, horizontal: context.screenWidth * 0.3,
vertical: MediaQuery.sizeOf(context).height * 0.3, vertical: context.screenHeight * 0.3,
), ),
minScale: 0.5, minScale: 0.5,
maxScale: 3.0, maxScale: 3.0,
@ -270,8 +430,8 @@ class _CommunityStructureCanvasState extends State<CommunityStructureCanvas>
child: GestureDetector( child: GestureDetector(
onTap: _resetSelectionAndZoom, onTap: _resetSelectionAndZoom,
child: SizedBox( child: SizedBox(
width: MediaQuery.sizeOf(context).width * 5, width: context.screenWidth * 5,
height: MediaQuery.sizeOf(context).height * 5, height: context.screenHeight * 5,
child: Stack(children: treeWidgets), child: Stack(children: treeWidgets),
), ),
), ),

View File

@ -3,7 +3,10 @@ import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_svg/svg.dart'; import 'package:flutter_svg/svg.dart';
import 'package:syncrow_web/pages/space_management_v2/main_module/shared/helpers/space_management_community_dialog_helper.dart'; import 'package:syncrow_web/pages/space_management_v2/main_module/shared/helpers/space_management_community_dialog_helper.dart';
import 'package:syncrow_web/pages/space_management_v2/main_module/widgets/community_structure_header_action_buttons.dart'; import 'package:syncrow_web/pages/space_management_v2/main_module/widgets/community_structure_header_action_buttons.dart';
import 'package:syncrow_web/pages/space_management_v2/modules/communities/domain/models/space_model.dart';
import 'package:syncrow_web/pages/space_management_v2/modules/communities/presentation/bloc/communities_bloc.dart';
import 'package:syncrow_web/pages/space_management_v2/modules/communities/presentation/communities_tree_selection_bloc/communities_tree_selection_bloc.dart'; import 'package:syncrow_web/pages/space_management_v2/modules/communities/presentation/communities_tree_selection_bloc/communities_tree_selection_bloc.dart';
import 'package:syncrow_web/pages/space_management_v2/modules/space_details/domain/models/space_details_model.dart';
import 'package:syncrow_web/pages/space_management_v2/modules/space_details/presentation/helpers/space_details_dialog_helper.dart'; import 'package:syncrow_web/pages/space_management_v2/modules/space_details/presentation/helpers/space_details_dialog_helper.dart';
import 'package:syncrow_web/utils/color_manager.dart'; import 'package:syncrow_web/utils/color_manager.dart';
import 'package:syncrow_web/utils/constants/assets.dart'; import 'package:syncrow_web/utils/constants/assets.dart';
@ -11,6 +14,26 @@ import 'package:syncrow_web/utils/constants/assets.dart';
class CommunityStructureHeader extends StatelessWidget { class CommunityStructureHeader extends StatelessWidget {
const CommunityStructureHeader({super.key}); const CommunityStructureHeader({super.key});
List<SpaceModel> _updateRecursive(
List<SpaceModel> spaces,
SpaceDetailsModel updatedSpace,
) {
return spaces.map((space) {
if (space.uuid == updatedSpace.uuid) {
return space.copyWith(
spaceName: updatedSpace.spaceName,
icon: updatedSpace.icon,
);
}
if (space.children.isNotEmpty) {
return space.copyWith(
children: _updateRecursive(space.children, updatedSpace),
);
}
return space;
}).toList();
}
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final theme = Theme.of(context); final theme = Theme.of(context);
@ -55,8 +78,9 @@ class CommunityStructureHeader extends StatelessWidget {
children: [ children: [
Text( Text(
'Community Structure', 'Community Structure',
style: theme.textTheme.headlineLarge style: theme.textTheme.headlineLarge?.copyWith(
?.copyWith(color: ColorsManager.blackColor), color: ColorsManager.blackColor,
),
), ),
if (selectedCommunity != null) if (selectedCommunity != null)
Row( Row(
@ -67,8 +91,9 @@ class CommunityStructureHeader extends StatelessWidget {
Flexible( Flexible(
child: SelectableText( child: SelectableText(
selectedCommunity.name, selectedCommunity.name,
style: theme.textTheme.bodyLarge style: theme.textTheme.bodyLarge?.copyWith(
?.copyWith(color: ColorsManager.blackColor), color: ColorsManager.blackColor,
),
maxLines: 1, maxLines: 1,
), ),
), ),
@ -93,12 +118,24 @@ class CommunityStructureHeader extends StatelessWidget {
CommunityStructureHeaderActionButtons( CommunityStructureHeaderActionButtons(
onDelete: (space) {}, onDelete: (space) {},
onDuplicate: (space) {}, onDuplicate: (space) {},
onEdit: (space) { onEdit: (space) => SpaceDetailsDialogHelper.showEdit(
SpaceDetailsDialogHelper.showEdit(
context, context,
spaceModel: selectedSpace!, spaceModel: selectedSpace!,
communityUuid: selectedCommunity.uuid,
onSuccess: (updatedSpaceDetails) {
final communitiesBloc = context.read<CommunitiesBloc>();
final updatedSpaces = _updateRecursive(
selectedCommunity.spaces,
updatedSpaceDetails,
); );
final community = selectedCommunity.copyWith(
spaces: updatedSpaces,
);
communitiesBloc.add(CommunitiesUpdateCommunity(community));
}, },
),
selectedSpace: selectedSpace, selectedSpace: selectedSpace,
), ),
], ],

View File

@ -2,42 +2,70 @@ import 'package:flutter/material.dart';
import 'package:syncrow_web/pages/space_management_v2/modules/space_details/presentation/helpers/space_details_dialog_helper.dart'; import 'package:syncrow_web/pages/space_management_v2/modules/space_details/presentation/helpers/space_details_dialog_helper.dart';
import 'package:syncrow_web/utils/color_manager.dart'; import 'package:syncrow_web/utils/color_manager.dart';
class CreateSpaceButton extends StatelessWidget { class CreateSpaceButton extends StatefulWidget {
const CreateSpaceButton({super.key}); const CreateSpaceButton({
required this.communityUuid,
super.key,
});
final String communityUuid;
@override
State<CreateSpaceButton> createState() => _CreateSpaceButtonState();
}
class _CreateSpaceButtonState extends State<CreateSpaceButton> {
bool _isHovered = false;
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return GestureDetector( return Tooltip(
onTap: () => SpaceDetailsDialogHelper.showCreate(context), margin: const EdgeInsets.symmetric(vertical: 24),
message: 'Create a new space',
child: InkWell(
onTap: () => SpaceDetailsDialogHelper.showCreate(
context,
communityUuid: widget.communityUuid,
),
child: MouseRegion(
onEnter: (_) => setState(() => _isHovered = true),
onExit: (_) => setState(() => _isHovered = false),
child: AnimatedOpacity(
duration: const Duration(milliseconds: 100),
opacity: _isHovered ? 1.0 : 0.45,
child: Container( child: Container(
height: 60, width: 150,
height: 90,
decoration: BoxDecoration( decoration: BoxDecoration(
color: Colors.white, color: Colors.white,
borderRadius: BorderRadius.circular(20), borderRadius: BorderRadius.circular(20),
boxShadow: [ boxShadow: [
BoxShadow( BoxShadow(
color: Colors.grey.withValues(alpha: 0.5), color: Colors.grey.withValues(alpha: 0.2),
spreadRadius: 5, spreadRadius: 3,
blurRadius: 7, blurRadius: 8,
offset: const Offset(0, 3), offset: const Offset(0, 4),
), ),
], ],
), ),
child: Center(
child: Container( child: Container(
width: 40, margin: const EdgeInsets.symmetric(vertical: 20),
height: 40, decoration: BoxDecoration(
decoration: const BoxDecoration( border: Border.all(color: ColorsManager.borderColor, width: 2),
color: ColorsManager.boxColor, color: ColorsManager.boxColor,
shape: BoxShape.circle, shape: BoxShape.circle,
), ),
child: const Icon( child: const Center(
child: Icon(
Icons.add, Icons.add,
color: Colors.blue, color: Colors.blue,
), ),
), ),
), ),
), ),
),
),
),
); );
} }
} }

View File

@ -22,7 +22,6 @@ class _SpaceCardWidgetState extends State<SpaceCardWidget> {
return MouseRegion( return MouseRegion(
onEnter: (_) => setState(() => isHovered = true), onEnter: (_) => setState(() => isHovered = true),
onExit: (_) => setState(() => isHovered = false), onExit: (_) => setState(() => isHovered = false),
child: SizedBox(
child: Stack( child: Stack(
clipBehavior: Clip.none, clipBehavior: Clip.none,
alignment: Alignment.center, alignment: Alignment.center,
@ -38,7 +37,6 @@ class _SpaceCardWidgetState extends State<SpaceCardWidget> {
), ),
], ],
), ),
),
); );
} }
} }

View File

@ -17,7 +17,7 @@ class SpaceCell extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return GestureDetector( return InkWell(
onTap: onTap, onTap: onTap,
child: Container( child: Container(
width: 150, width: 150,

View File

@ -13,11 +13,17 @@ class SpaceManagementCommunityStructure extends StatelessWidget {
final selectionBloc = context.watch<CommunitiesTreeSelectionBloc>().state; final selectionBloc = context.watch<CommunitiesTreeSelectionBloc>().state;
final selectedCommunity = selectionBloc.selectedCommunity; final selectedCommunity = selectionBloc.selectedCommunity;
final selectedSpace = selectionBloc.selectedSpace; final selectedSpace = selectionBloc.selectedSpace;
const spacer = Spacer(flex: 10); const spacer = Spacer(flex: 6);
return Visibility( return Visibility(
visible: selectedCommunity!.spaces.isNotEmpty, visible: selectedCommunity!.spaces.isNotEmpty,
replacement: const Row( replacement: Row(
children: [spacer, Expanded(child: CreateSpaceButton()), spacer], children: [
spacer,
Expanded(
child: CreateSpaceButton(communityUuid: selectedCommunity.uuid),
),
spacer
],
), ),
child: Column( child: Column(
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,

View File

@ -46,6 +46,25 @@ class SpaceModel extends Equatable {
); );
} }
SpaceModel copyWith({
String? uuid,
DateTime? createdAt,
DateTime? updatedAt,
String? spaceName,
String? icon,
List<SpaceModel>? children,
}) {
return SpaceModel(
uuid: uuid ?? this.uuid,
createdAt: createdAt ?? this.createdAt,
updatedAt: updatedAt ?? this.updatedAt,
spaceName: spaceName ?? this.spaceName,
icon: icon ?? this.icon,
children: children ?? this.children,
parent: parent,
);
}
@override @override
List<Object?> get props => [uuid, spaceName, icon, children]; List<Object?> get props => [uuid, spaceName, icon, children];
} }

View File

@ -1,17 +1,39 @@
import 'dart:async';
import 'package:bloc/bloc.dart'; import 'package:bloc/bloc.dart';
import 'package:equatable/equatable.dart'; import 'package:equatable/equatable.dart';
import 'package:syncrow_web/pages/space_management_v2/modules/communities/domain/models/community_model.dart'; import 'package:syncrow_web/pages/space_management_v2/modules/communities/domain/models/community_model.dart';
import 'package:syncrow_web/pages/space_management_v2/modules/communities/domain/models/space_model.dart'; import 'package:syncrow_web/pages/space_management_v2/modules/communities/domain/models/space_model.dart';
import 'package:syncrow_web/pages/space_management_v2/modules/communities/presentation/bloc/communities_bloc.dart';
part 'communities_tree_selection_event.dart'; part 'communities_tree_selection_event.dart';
part 'communities_tree_selection_state.dart'; part 'communities_tree_selection_state.dart';
class CommunitiesTreeSelectionBloc class CommunitiesTreeSelectionBloc
extends Bloc<CommunitiesTreeSelectionEvent, CommunitiesTreeSelectionState> { extends Bloc<CommunitiesTreeSelectionEvent, CommunitiesTreeSelectionState> {
CommunitiesTreeSelectionBloc() : super(const CommunitiesTreeSelectionState()) { CommunitiesTreeSelectionBloc({
required CommunitiesBloc communitiesBloc,
}) : _communitiesBloc = communitiesBloc,
super(const CommunitiesTreeSelectionState()) {
on<SelectCommunityEvent>(_onSelectCommunity); on<SelectCommunityEvent>(_onSelectCommunity);
on<SelectSpaceEvent>(_onSelectSpace); on<SelectSpaceEvent>(_onSelectSpace);
on<ClearCommunitiesTreeSelectionEvent>(_onClearSelection); on<ClearCommunitiesTreeSelectionEvent>(_onClearSelection);
on<_CommunitiesStateUpdated>(_onCommunitiesStateUpdated);
_communitiesSubscription = _communitiesBloc.stream.listen((communitiesState) {
if (state.selectedCommunity != null) {
add(_CommunitiesStateUpdated(communitiesState));
}
});
}
final CommunitiesBloc _communitiesBloc;
late final StreamSubscription<CommunitiesState> _communitiesSubscription;
@override
Future<void> close() {
_communitiesSubscription.cancel();
return super.close();
} }
void _onSelectCommunity( void _onSelectCommunity(
@ -44,4 +66,59 @@ class CommunitiesTreeSelectionBloc
) { ) {
emit(const CommunitiesTreeSelectionState()); emit(const CommunitiesTreeSelectionState());
} }
void _onCommunitiesStateUpdated(
_CommunitiesStateUpdated event,
Emitter<CommunitiesTreeSelectionState> emit,
) {
if (state.selectedCommunity == null) return;
final communities = event.communitiesState.communities;
try {
final updatedCommunity = communities.firstWhere(
(c) => c.uuid == state.selectedCommunity!.uuid,
);
var updatedSelectedSpace = state.selectedSpace;
if (state.selectedSpace != null) {
updatedSelectedSpace = _findSpaceInCommunity(
updatedCommunity,
state.selectedSpace!.uuid,
);
}
emit(
state.copyWith(
selectedCommunity: updatedCommunity,
selectedSpace: updatedSelectedSpace,
clearSelectedSpace: updatedSelectedSpace == null,
),
);
} catch (_) {
add(const ClearCommunitiesTreeSelectionEvent());
}
}
SpaceModel? _findSpaceInCommunity(CommunityModel community, String spaceUuid) {
try {
return _findSpaceRecursive(community.spaces, spaceUuid);
} catch (_) {
return null;
}
}
SpaceModel _findSpaceRecursive(List<SpaceModel> spaces, String spaceUuid) {
for (final space in spaces) {
if (space.uuid == spaceUuid) {
return space;
}
if (space.children.isNotEmpty) {
try {
return _findSpaceRecursive(space.children, spaceUuid);
} catch (_) {
// not found in this branch
}
}
}
throw Exception('Space not found');
}
} }

View File

@ -29,3 +29,12 @@ final class ClearCommunitiesTreeSelectionEvent
extends CommunitiesTreeSelectionEvent { extends CommunitiesTreeSelectionEvent {
const ClearCommunitiesTreeSelectionEvent(); const ClearCommunitiesTreeSelectionEvent();
} }
final class _CommunitiesStateUpdated extends CommunitiesTreeSelectionEvent {
const _CommunitiesStateUpdated(this.communitiesState);
final CommunitiesState communitiesState;
@override
List<Object> get props => [communitiesState];
}

View File

@ -12,18 +12,14 @@ final class CommunitiesTreeSelectionState extends Equatable {
CommunitiesTreeSelectionState copyWith({ CommunitiesTreeSelectionState copyWith({
CommunityModel? selectedCommunity, CommunityModel? selectedCommunity,
SpaceModel? selectedSpace, SpaceModel? selectedSpace,
List<CommunityModel>? expandedCommunities, bool clearSelectedSpace = false,
List<SpaceModel>? expandedSpaces,
}) { }) {
return CommunitiesTreeSelectionState( return CommunitiesTreeSelectionState(
selectedCommunity: selectedCommunity ?? this.selectedCommunity, selectedCommunity: selectedCommunity ?? this.selectedCommunity,
selectedSpace: selectedSpace ?? this.selectedSpace, selectedSpace: clearSelectedSpace ? null : selectedSpace ?? this.selectedSpace,
); );
} }
@override @override
List<Object?> get props => [ List<Object?> get props => [selectedCommunity, selectedSpace];
selectedCommunity, }
selectedSpace,
];
}

View File

@ -13,14 +13,14 @@ class CommunitiesTreeFailureWidget extends StatelessWidget {
return Expanded( return Expanded(
child: Center( child: Center(
child: Column( child: Column(
spacing: 16,
mainAxisAlignment: MainAxisAlignment.center, mainAxisAlignment: MainAxisAlignment.center,
children: [ children: [
Text( SelectableText(
errorMessage ?? 'Something went wrong', errorMessage ?? 'Something went wrong',
textAlign: TextAlign.center, textAlign: TextAlign.center,
), ),
const SizedBox(height: 16), FilledButton(
ElevatedButton(
onPressed: () => context.read<CommunitiesBloc>().add( onPressed: () => context.read<CommunitiesBloc>().add(
LoadCommunities( LoadCommunities(
LoadCommunitiesParam( LoadCommunitiesParam(

View File

@ -40,16 +40,6 @@ class SpaceDetailsModel extends Equatable {
); );
} }
Map<String, dynamic> toJson() {
return {
'uuid': uuid,
'spaceName': spaceName,
'icon': icon,
'productAllocations': productAllocations.map((e) => e.toJson()).toList(),
'subspaces': subspaces.map((e) => e.toJson()).toList(),
};
}
SpaceDetailsModel copyWith({ SpaceDetailsModel copyWith({
String? uuid, String? uuid,
String? spaceName, String? spaceName,
@ -89,14 +79,6 @@ class ProductAllocation extends Equatable {
); );
} }
Map<String, dynamic> toJson() {
return {
'uuid': uuid,
'product': product.toJson(),
'tag': tag.toJson(),
};
}
ProductAllocation copyWith({ ProductAllocation copyWith({
String? uuid, String? uuid,
Product? product, Product? product,
@ -134,14 +116,6 @@ class Subspace extends Equatable {
); );
} }
Map<String, dynamic> toJson() {
return {
'uuid': uuid,
'name': name,
'productAllocations': productAllocations.map((e) => e.toJson()).toList(),
};
}
Subspace copyWith({ Subspace copyWith({
String? uuid, String? uuid,
String? name, String? name,

View File

@ -2,23 +2,37 @@ import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:syncrow_web/pages/space_management_v2/modules/communities/domain/models/space_model.dart'; import 'package:syncrow_web/pages/space_management_v2/modules/communities/domain/models/space_model.dart';
import 'package:syncrow_web/pages/space_management_v2/modules/space_details/data/services/remote_space_details_service.dart'; import 'package:syncrow_web/pages/space_management_v2/modules/space_details/data/services/remote_space_details_service.dart';
import 'package:syncrow_web/pages/space_management_v2/modules/space_details/domain/models/space_details_model.dart';
import 'package:syncrow_web/pages/space_management_v2/modules/space_details/presentation/bloc/space_details_bloc.dart'; import 'package:syncrow_web/pages/space_management_v2/modules/space_details/presentation/bloc/space_details_bloc.dart';
import 'package:syncrow_web/pages/space_management_v2/modules/space_details/presentation/widgets/space_details_dialog.dart'; import 'package:syncrow_web/pages/space_management_v2/modules/space_details/presentation/widgets/space_details_dialog.dart';
import 'package:syncrow_web/pages/space_management_v2/modules/update_space/data/services/remote_update_space_service.dart';
import 'package:syncrow_web/pages/space_management_v2/modules/update_space/domain/params/update_space_param.dart';
import 'package:syncrow_web/pages/space_management_v2/modules/update_space/presentation/bloc/update_space_bloc.dart';
import 'package:syncrow_web/services/api/http_service.dart'; import 'package:syncrow_web/services/api/http_service.dart';
abstract final class SpaceDetailsDialogHelper { abstract final class SpaceDetailsDialogHelper {
static void showCreate(BuildContext context) { static void showCreate(
BuildContext context, {
required String communityUuid,
}) {
showDialog<void>( showDialog<void>(
context: context, context: context,
builder: (_) => BlocProvider( builder: (_) => MultiBlocProvider(
providers: [
BlocProvider(
create: (context) => SpaceDetailsBloc( create: (context) => SpaceDetailsBloc(
RemoteSpaceDetailsService(httpService: HTTPService()), RemoteSpaceDetailsService(httpService: HTTPService()),
), ),
child: SpaceDetailsDialog( ),
],
child: Builder(
builder: (context) => SpaceDetailsDialog(
context: context, context: context,
title: const SelectableText('Create Space'), title: const SelectableText('Create Space'),
spaceModel: SpaceModel.empty(), spaceModel: SpaceModel.empty(),
onSave: (space) {}, onSave: (space) {},
communityUuid: communityUuid,
),
), ),
), ),
); );
@ -27,20 +41,98 @@ abstract final class SpaceDetailsDialogHelper {
static void showEdit( static void showEdit(
BuildContext context, { BuildContext context, {
required SpaceModel spaceModel, required SpaceModel spaceModel,
required String communityUuid,
required void Function(SpaceDetailsModel updatedSpaceDetails)? onSuccess,
}) { }) {
showDialog<void>( showDialog<void>(
context: context, context: context,
builder: (_) => BlocProvider( builder: (_) => MultiBlocProvider(
providers: [
BlocProvider(
create: (context) => SpaceDetailsBloc( create: (context) => SpaceDetailsBloc(
RemoteSpaceDetailsService(httpService: HTTPService()), RemoteSpaceDetailsService(httpService: HTTPService()),
), ),
),
BlocProvider(
create: (context) => UpdateSpaceBloc(
RemoteUpdateSpaceService(HTTPService()),
),
),
],
child: Builder(
builder: (context) => BlocListener<UpdateSpaceBloc, UpdateSpaceState>(
listener: (context, state) => _updateListener(
context,
state,
onSuccess,
),
child: SpaceDetailsDialog( child: SpaceDetailsDialog(
context: context, context: context,
title: const SelectableText('Edit Space'), title: const SelectableText('Edit Space'),
spaceModel: spaceModel, spaceModel: spaceModel,
onSave: (space) {}, onSave: (space) => context.read<UpdateSpaceBloc>().add(
UpdateSpace(
UpdateSpaceParam(
communityUuid: communityUuid,
space: space,
),
),
),
communityUuid: communityUuid,
),
),
), ),
), ),
); );
} }
static void _updateListener(
BuildContext context,
UpdateSpaceState state,
void Function(SpaceDetailsModel updatedSpaceDetails)? onSuccess,
) {
return switch (state) {
UpdateSpaceInitial() => null,
UpdateSpaceLoading() => _onLoading(context),
UpdateSpaceSuccess(:final space) =>
_onUpdateSuccess(context, space, onSuccess),
UpdateSpaceFailure(:final errorMessage) => _onError(context, errorMessage),
};
}
static void _onUpdateSuccess(
BuildContext context,
SpaceDetailsModel space,
void Function(SpaceDetailsModel updatedSpaceDetails)? onSuccess,
) {
Navigator.of(context).pop();
Navigator.of(context).pop();
onSuccess?.call(space);
}
static void _onLoading(BuildContext context) {
showDialog<void>(
context: context,
barrierDismissible: false,
builder: (_) => const Center(child: CircularProgressIndicator()),
);
}
static void _onError(BuildContext context, String errorMessage) {
Navigator.of(context).pop();
showDialog<void>(
context: context,
barrierDismissible: false,
builder: (_) => AlertDialog(
title: const Text('Error'),
content: Text(errorMessage),
actions: [
TextButton(
onPressed: Navigator.of(context).pop,
child: const Text('OK'),
),
],
),
);
}
} }

View File

@ -1,7 +1,6 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:syncrow_web/pages/space_management_v2/modules/communities/domain/models/space_model.dart'; import 'package:syncrow_web/pages/space_management_v2/modules/communities/domain/models/space_model.dart';
import 'package:syncrow_web/pages/space_management_v2/modules/communities/presentation/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/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/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/bloc/space_details_bloc.dart';
@ -15,6 +14,7 @@ class SpaceDetailsDialog extends StatefulWidget {
required this.spaceModel, required this.spaceModel,
required this.onSave, required this.onSave,
required this.context, required this.context,
required this.communityUuid,
super.key, super.key,
}); });
@ -22,6 +22,7 @@ class SpaceDetailsDialog extends StatefulWidget {
final SpaceModel spaceModel; final SpaceModel spaceModel;
final void Function(SpaceDetailsModel space) onSave; final void Function(SpaceDetailsModel space) onSave;
final BuildContext context; final BuildContext context;
final String communityUuid;
@override @override
State<SpaceDetailsDialog> createState() => _SpaceDetailsDialogState(); State<SpaceDetailsDialog> createState() => _SpaceDetailsDialogState();
@ -35,11 +36,7 @@ class _SpaceDetailsDialogState extends State<SpaceDetailsDialog> {
if (!isCreateMode) { if (!isCreateMode) {
final param = LoadSpaceDetailsParam( final param = LoadSpaceDetailsParam(
spaceUuid: widget.spaceModel.uuid, spaceUuid: widget.spaceModel.uuid,
communityUuid: widget.context communityUuid: widget.communityUuid,
.read<CommunitiesTreeSelectionBloc>()
.state
.selectedCommunity!
.uuid,
); );
widget.context.read<SpaceDetailsBloc>().add(LoadSpaceDetails(param)); widget.context.read<SpaceDetailsBloc>().add(LoadSpaceDetails(param));
} }

View File

@ -37,7 +37,7 @@ class _SpaceSubSpacesDialogState extends State<SpaceSubSpacesDialog> {
..._subspaces, ..._subspaces,
Subspace( Subspace(
name: name, name: name,
uuid: const Uuid().v4(), uuid: '${const Uuid().v4()}-NewTag',
productAllocations: const [], productAllocations: const [],
), ),
]; ];

View File

@ -3,41 +3,19 @@ import 'package:equatable/equatable.dart';
class Tag extends Equatable { class Tag extends Equatable {
final String uuid; final String uuid;
final String name; final String name;
final String createdAt;
final String updatedAt;
const Tag({ const Tag({
required this.uuid, required this.uuid,
required this.name, required this.name,
required this.createdAt,
required this.updatedAt,
}); });
factory Tag.empty() => const Tag(
uuid: '',
name: '',
createdAt: '',
updatedAt: '',
);
factory Tag.fromJson(Map<String, dynamic> json) { factory Tag.fromJson(Map<String, dynamic> json) {
return Tag( return Tag(
uuid: json['uuid'] as String, uuid: json['uuid'] as String,
name: json['name'] as String, name: json['name'] as String,
createdAt: json['createdAt'] as String,
updatedAt: json['updatedAt'] as String,
); );
} }
Map<String, dynamic> toJson() {
return {
'uuid': uuid,
'name': name,
'createdAt': createdAt,
'updatedAt': updatedAt,
};
}
@override @override
List<Object?> get props => [uuid, name, createdAt, updatedAt]; List<Object?> get props => [uuid, name];
} }

View File

@ -214,9 +214,12 @@ class _AssignTagsDialogState extends State<AssignTagsDialog> {
for (final product in newProducts) { for (final product in newProducts) {
_space.productAllocations.add( _space.productAllocations.add(
ProductAllocation( ProductAllocation(
uuid: const Uuid().v4(), uuid: '${const Uuid().v4()}-NewProductUuid',
product: product, product: product,
tag: Tag.empty(), tag: Tag(
uuid: '${const Uuid().v4()}-NewTag',
name: '',
),
), ),
); );
} }

View File

@ -2,6 +2,7 @@ import 'package:flutter/material.dart';
import 'package:syncrow_web/pages/space_management_v2/modules/tags/domain/models/tag.dart'; import 'package:syncrow_web/pages/space_management_v2/modules/tags/domain/models/tag.dart';
import 'package:syncrow_web/utils/color_manager.dart'; import 'package:syncrow_web/utils/color_manager.dart';
import 'package:syncrow_web/utils/extension/build_context_x.dart'; import 'package:syncrow_web/utils/extension/build_context_x.dart';
import 'package:uuid/uuid.dart';
class ProductTagField extends StatefulWidget { class ProductTagField extends StatefulWidget {
final List<Tag> items; final List<Tag> items;
@ -53,13 +54,8 @@ class _ProductTagFieldState extends State<ProductTagField> {
void _submit(String value) { void _submit(String value) {
final lowerCaseValue = value.toLowerCase(); final lowerCaseValue = value.toLowerCase();
final selectedTag = widget.items.firstWhere( final selectedTag = widget.items.firstWhere(
(tag) => tag.name.toLowerCase() == lowerCaseValue, (e) => e.name.toLowerCase() == lowerCaseValue,
orElse: () => Tag( orElse: () => Tag(uuid: '${const Uuid().v4()}-NewTag', name: value),
name: value,
uuid: '',
createdAt: '',
updatedAt: '',
),
); );
widget.onSelected(selectedTag); widget.onSelected(selectedTag);
_closeDropdown(); _closeDropdown();

View File

@ -1,5 +1,7 @@
import 'package:dio/dio.dart'; 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/models/space_details_model.dart';
import 'package:syncrow_web/pages/space_management_v2/modules/update_space/domain/params/update_space_param.dart';
import 'package:syncrow_web/pages/space_management_v2/modules/update_space/domain/services/update_space_service.dart'; import 'package:syncrow_web/pages/space_management_v2/modules/update_space/domain/services/update_space_service.dart';
import 'package:syncrow_web/services/api/api_exception.dart'; import 'package:syncrow_web/services/api/api_exception.dart';
import 'package:syncrow_web/services/api/http_service.dart'; import 'package:syncrow_web/services/api/http_service.dart';
@ -12,17 +14,23 @@ class RemoteUpdateSpaceService implements UpdateSpaceService {
static const _defaultErrorMessage = 'Failed to update space'; static const _defaultErrorMessage = 'Failed to update space';
@override @override
Future<SpaceDetailsModel> updateSpace(SpaceDetailsModel space) async { Future<SpaceDetailsModel> updateSpace(UpdateSpaceParam param) async {
try { try {
final response = await _httpService.put( final path = await _makeUrl(param);
path: 'endpoint', await _httpService.put(
body: space.toJson(), path: path,
expectedResponseModel: (data) => SpaceDetailsModel.fromJson( body: param.toJson(),
data as Map<String, dynamic>, expectedResponseModel: (data) {
), final response = data as Map<String, dynamic>;
final isSuccess = response['success'] as bool;
if (!isSuccess) {
throw APIException(response['error'] as String);
}
return isSuccess;
},
); );
return response; return param.space;
} on DioException catch (e) { } on DioException catch (e) {
final message = e.response?.data as Map<String, dynamic>?; final message = e.response?.data as Map<String, dynamic>?;
final error = message?['error'] as Map<String, dynamic>?; final error = message?['error'] as Map<String, dynamic>?;
@ -37,4 +45,23 @@ class RemoteUpdateSpaceService implements UpdateSpaceService {
throw APIException(formattedErrorMessage); throw APIException(formattedErrorMessage);
} }
} }
Future<String> _makeUrl(UpdateSpaceParam param) async {
final projectUuid = await ProjectManager.getProjectUUID();
if (projectUuid == null || projectUuid.isEmpty) {
throw APIException('Project UUID is not set');
}
final spaceUuid = param.space.uuid;
if (spaceUuid.isEmpty) {
throw APIException('Space UUID is not set');
}
final communityUuid = param.communityUuid;
if (communityUuid.isEmpty) {
throw APIException('Community UUID is not set');
}
return '/projects/$projectUuid/communities/$communityUuid/spaces/$spaceUuid';
}
} }

View File

@ -0,0 +1,42 @@
import 'package:syncrow_web/pages/space_management_v2/modules/space_details/domain/models/space_details_model.dart';
class UpdateSpaceParam {
UpdateSpaceParam({
required this.space,
required this.communityUuid,
});
final SpaceDetailsModel space;
final String communityUuid;
Map<String, dynamic> toJson() {
return {
'spaceName': space.spaceName,
'icon': space.icon,
'subspaces': space.subspaces.map((e) => e._toJson()).toList(),
'productAllocations':
space.productAllocations.map((e) => e._toJson()).toList(),
};
}
}
extension _ProductAllocationToJson on ProductAllocation {
Map<String, dynamic> _toJson() {
final isNewTag = tag.uuid.isEmpty;
return <String, dynamic>{
if (isNewTag) 'tagName': tag.name else 'tagUuid': tag.uuid,
'productUuid': product.uuid,
};
}
}
extension _SubspaceToJson on Subspace {
Map<String, dynamic> _toJson() {
final isNewSubspace = uuid.endsWith('-NewTag');
return <String, dynamic>{
if (!isNewSubspace) 'uuid': uuid,
'subspaceName': name,
'productAllocations': productAllocations.map((e) => e._toJson()).toList(),
};
}
}

View File

@ -1,5 +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/models/space_details_model.dart';
import 'package:syncrow_web/pages/space_management_v2/modules/update_space/domain/params/update_space_param.dart';
abstract class UpdateSpaceService { abstract interface class UpdateSpaceService {
Future<SpaceDetailsModel> updateSpace(SpaceDetailsModel space); Future<SpaceDetailsModel> updateSpace(UpdateSpaceParam param);
} }

View File

@ -1,6 +1,7 @@
import 'package:bloc/bloc.dart'; import 'package:bloc/bloc.dart';
import 'package:equatable/equatable.dart'; import 'package:equatable/equatable.dart';
import 'package:syncrow_web/pages/space_management_v2/modules/space_details/domain/models/space_details_model.dart'; import 'package:syncrow_web/pages/space_management_v2/modules/space_details/domain/models/space_details_model.dart';
import 'package:syncrow_web/pages/space_management_v2/modules/update_space/domain/params/update_space_param.dart';
import 'package:syncrow_web/pages/space_management_v2/modules/update_space/domain/services/update_space_service.dart'; import 'package:syncrow_web/pages/space_management_v2/modules/update_space/domain/services/update_space_service.dart';
import 'package:syncrow_web/services/api/api_exception.dart'; import 'package:syncrow_web/services/api/api_exception.dart';
@ -20,7 +21,7 @@ class UpdateSpaceBloc extends Bloc<UpdateSpaceEvent, UpdateSpaceState> {
) async { ) async {
emit(UpdateSpaceLoading()); emit(UpdateSpaceLoading());
try { try {
final updatedSpace = await _updateSpaceService.updateSpace(event.space); final updatedSpace = await _updateSpaceService.updateSpace(event.param);
emit(UpdateSpaceSuccess(updatedSpace)); emit(UpdateSpaceSuccess(updatedSpace));
} on APIException catch (e) { } on APIException catch (e) {
emit(UpdateSpaceFailure(e.message)); emit(UpdateSpaceFailure(e.message));

View File

@ -8,10 +8,10 @@ sealed class UpdateSpaceEvent extends Equatable {
} }
final class UpdateSpace extends UpdateSpaceEvent { final class UpdateSpace extends UpdateSpaceEvent {
const UpdateSpace(this.space); const UpdateSpace(this.param);
final SpaceDetailsModel space; final UpdateSpaceParam param;
@override @override
List<Object> get props => [space]; List<Object> get props => [param];
} }

View File

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

View File

@ -69,7 +69,6 @@ abstract class ColorsManager {
static const Color invitedOrange = Color(0xFFFFE193); static const Color invitedOrange = Color(0xFFFFE193);
static const Color invitedOrangeText = Color(0xFFFFBF00); static const Color invitedOrangeText = Color(0xFFFFBF00);
static const Color lightGrayBorderColor = Color(0xB2D5D5D5); static const Color lightGrayBorderColor = Color(0xB2D5D5D5);
//background: #F8F8F8;
static const Color vividBlue = Color(0xFF023DFE); static const Color vividBlue = Color(0xFF023DFE);
static const Color semiTransparentRed = Color(0x99FF0000); static const Color semiTransparentRed = Color(0x99FF0000);
static const Color grey700 = Color(0xFF2D3748); static const Color grey700 = Color(0xFF2D3748);
@ -85,4 +84,6 @@ abstract class ColorsManager {
static const Color minBlueDot = Color(0xFF023DFE); static const Color minBlueDot = Color(0xFF023DFE);
static const Color grey25 = Color(0xFFF9F9F9); static const Color grey25 = Color(0xFFF9F9F9);
static const Color grey50 = Color(0xFF718096); static const Color grey50 = Color(0xFF718096);
static const Color red100 = Color(0xFFFE0202);
static const Color grey800 = Color(0xffF8F8F8);
} }

View File

@ -141,4 +141,5 @@ abstract class ApiEndpoints {
static const String saveSchedule = '/schedule/{deviceUuid}'; static const String saveSchedule = '/schedule/{deviceUuid}';
static const String getBookableSpaces = '/bookable-spaces'; static const String getBookableSpaces = '/bookable-spaces';
static const String getCalendarEvents = '/api';
} }