Merge branch 'dev' of https://github.com/SyncrowIOT/web into Implement-Spaces-Table-Empty-Filled-Failure-states-bookable-spaces

This commit is contained in:
Rafeek-Khoudare
2025-07-15 15:35:31 +03:00
152 changed files with 6018 additions and 763 deletions

View File

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

View File

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

View File

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

112
.vscode/launch.json vendored
View File

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

View File

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

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

View File

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

View File

@ -0,0 +1,52 @@
import 'package:dio/dio.dart';
import 'package:syncrow_web/pages/access_management/booking_system/domain/load_bookable_spaces_param.dart';
import 'package:syncrow_web/pages/access_management/booking_system/domain/models/paginated_bookable_spaces.dart';
import 'package:syncrow_web/pages/access_management/booking_system/domain/services/bookable_system_service.dart';
import 'package:syncrow_web/services/api/api_exception.dart';
import 'package:syncrow_web/services/api/http_service.dart';
import 'package:syncrow_web/utils/constants/api_const.dart';
class RemoteBookableSpacesService implements BookableSystemService {
const RemoteBookableSpacesService(this._httpService);
final HTTPService _httpService;
static const _defaultErrorMessage = 'Failed to load bookable spaces';
@override
Future<PaginatedBookableSpaces> getBookableSpaces({
required LoadBookableSpacesParam param,
}) async {
try {
final response = await _httpService.get(
path: ApiEndpoints.getBookableSpaces,
queryParameters: {
'page': param.page,
'size': param.size,
'active': true,
'configured': true,
if (param.search != null &&
param.search.isNotEmpty &&
param.search != 'null')
'search': param.search,
},
expectedResponseModel: (json) {
return PaginatedBookableSpaces.fromJson(
json as Map<String, dynamic>,
);
},
);
return response;
} on DioException catch (e) {
final responseData = e.response?.data;
if (responseData is Map<String, dynamic>) {
final errorMessage = responseData['error']?['message'] as String? ??
responseData['message'] as String? ??
_defaultErrorMessage;
throw APIException(errorMessage);
}
throw APIException(_defaultErrorMessage);
} catch (e) {
throw APIException('$_defaultErrorMessage: ${e.toString()}');
}
}
}

View File

@ -0,0 +1,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,36 @@
import 'package:equatable/equatable.dart';
class LoadBookableSpacesParam extends Equatable {
const LoadBookableSpacesParam({
this.page = 1,
this.size = 25,
this.search = '',
this.active = true,
this.configured = true,
});
final int page;
final int size;
final String search;
final bool active;
final bool configured;
LoadBookableSpacesParam copyWith({
int? page,
int? size,
String? search,
bool? active,
bool? configured,
}) {
return LoadBookableSpacesParam(
page: page ?? this.page,
size: size ?? this.size,
search: search ?? this.search,
active: active ?? this.active,
configured: configured ?? this.configured,
);
}
@override
List<Object?> get props => [page, size, search, active, configured];
}

View File

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

View File

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

View File

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

View File

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

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

View File

@ -0,0 +1,115 @@
import 'dart:async';
import 'package:bloc/bloc.dart';
import 'package:calendar_view/calendar_view.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_state.dart';
class CalendarEventsBloc extends Bloc<CalendarEventsEvent, CalendarEventState> {
final EventController eventController = EventController();
final CalendarSystemService calendarService;
CalendarEventsBloc({required this.calendarService}) : super(EventsInitial()) {
on<LoadEvents>(_onLoadEvents);
on<AddEvent>(_onAddEvent);
on<StartTimer>(_onStartTimer);
on<DisposeResources>(_onDisposeResources);
on<GoToWeek>(_onGoToWeek);
}
Future<void> _onLoadEvents(
LoadEvents event,
Emitter<CalendarEventState> emit,
) async {
emit(EventsLoading());
try {
final response = await calendarService.getCalendarEvents(
spaceId: event.spaceId,
);
final events =
response.data.map<CalendarEventData>(_toCalendarEventData).toList();
eventController.addAll(events);
emit(EventsLoaded(events: events));
} catch (e) {
emit(EventsError('Failed to load events'));
}
}
void _onAddEvent(AddEvent event, Emitter<CalendarEventState> emit) {
eventController.add(event.event);
if (state is EventsLoaded) {
final loaded = state as EventsLoaded;
emit(EventsLoaded(
events: [...eventController.events],
));
}
}
void _onStartTimer(StartTimer event, Emitter<CalendarEventState> emit) {}
void _onDisposeResources(
DisposeResources event, Emitter<CalendarEventState> emit) {
eventController.dispose();
}
void _onGoToWeek(GoToWeek event, Emitter<CalendarEventState> emit) {
if (state is EventsLoaded) {
final loaded = state as EventsLoaded;
final newWeekDays = _getWeekDays(event.weekDate);
emit(EventsLoaded(
events: loaded.events,
));
}
}
CalendarEventData _toCalendarEventData(CalendarEventBooking booking) {
final date = booking.date;
final localDate = date.toLocal();
final startParts = booking.startTime.split(':').map(int.parse).toList();
final endParts = booking.endTime.split(':').map(int.parse).toList();
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,
event: booking,
);
}
List<DateTime> _getWeekDays(DateTime date) {
final int weekday = date.weekday;
final DateTime monday = date.subtract(Duration(days: weekday - 1));
return List.generate(7, (i) => monday.add(Duration(days: i)));
}
@override
Future<void> close() {
eventController.dispose();
return super.close();
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -0,0 +1,240 @@
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.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_event.dart';
import 'package:syncrow_web/pages/access_management/booking_system/presentation/bloc/date_selection/date_selection_state.dart';
import 'package:syncrow_web/pages/access_management/booking_system/presentation/bloc/selected_bookable_space_bloc/selected_bookable_space_bloc.dart';
import 'package:syncrow_web/pages/access_management/booking_system/presentation/view/widgets/booking_sidebar.dart';
import 'package:syncrow_web/pages/access_management/booking_system/presentation/view/widgets/custom_calendar_page.dart';
import 'package:syncrow_web/pages/access_management/booking_system/presentation/view/widgets/icon_text_button.dart';
import 'package:syncrow_web/pages/access_management/booking_system/presentation/view/widgets/week_navigation.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/constants/assets.dart';
class BookingPage extends StatefulWidget {
const BookingPage({super.key});
@override
State<BookingPage> createState() => _BookingPageState();
}
class _BookingPageState extends State<BookingPage> {
late final EventController _eventController;
@override
void initState() {
super.initState();
_eventController = EventController();
}
@override
void dispose() {
_eventController.dispose();
super.dispose();
}
void _dispatchLoadEvents(BuildContext context) {
final selectedRoom =
context.read<SelectedBookableSpaceBloc>().state.selectedBookableSpace;
final dateState = context.read<DateSelectionBloc>().state;
if (selectedRoom != null) {
context.read<CalendarEventsBloc>().add(
LoadEvents(
spaceId: selectedRoom.uuid,
weekStart: dateState.weekStart,
weekEnd: dateState.weekStart.add(const Duration(days: 6)),
),
);
}
}
@override
Widget build(BuildContext context) {
return MultiBlocProvider(
providers: [
BlocProvider(create: (_) => SelectedBookableSpaceBloc()),
BlocProvider(create: (_) => DateSelectionBloc()),
BlocProvider(
create: (_) => CalendarEventsBloc(
calendarService:
FakeRemoteCalendarService(HTTPService(), useDummy: true),
),
),
],
child: Builder(
builder: (context) =>
BlocListener<CalendarEventsBloc, CalendarEventState>(
listenWhen: (prev, curr) => curr is EventsLoaded,
listener: (context, state) {
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(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Expanded(
child: Container(
decoration: BoxDecoration(
color: ColorsManager.whiteColors,
boxShadow: [
BoxShadow(
color: ColorsManager.blackColor.withOpacity(0.1),
offset: const Offset(3, 0),
blurRadius: 6,
spreadRadius: 0,
),
],
),
child: Column(
children: [
Expanded(
flex: 2,
child: BlocBuilder<SelectedBookableSpaceBloc,
SelectedBookableSpaceState>(
builder: (context, state) {
return BookingSidebar(
onRoomSelected: (selectedRoom) {
context
.read<SelectedBookableSpaceBloc>()
.add(SelectBookableSpace(selectedRoom));
},
);
},
),
),
Expanded(
child: BlocBuilder<DateSelectionBloc,
DateSelectionState>(
builder: (context, dateState) {
return CustomCalendarPage(
selectedDate: dateState.selectedDate,
onDateChanged: (day, month, year) {
final newDate = DateTime(year, month, day);
context
.read<DateSelectionBloc>()
.add(SelectDate(newDate));
context.read<DateSelectionBloc>().add(
SelectDateFromSidebarCalendar(newDate));
},
);
},
),
),
],
),
),
),
Expanded(
flex: 4,
child: Padding(
padding: const EdgeInsets.all(20.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Row(
children: [
SvgTextButton(
svgAsset: Assets.homeIcon,
label: 'Manage Bookable Spaces',
onPressed: () {},
),
const SizedBox(width: 20),
SvgTextButton(
svgAsset: Assets.groupIcon,
label: 'Manage Users',
onPressed: () {},
),
],
),
BlocBuilder<DateSelectionBloc,
DateSelectionState>(
builder: (context, state) {
final weekStart = state.weekStart;
final weekEnd =
weekStart.add(const Duration(days: 6));
return WeekNavigation(
weekStart: weekStart,
weekEnd: weekEnd,
onPreviousWeek: () {
context
.read<DateSelectionBloc>()
.add(PreviousWeek());
},
onNextWeek: () {
context
.read<DateSelectionBloc>()
.add(NextWeek());
},
);
},
),
],
),
Expanded(
child: BlocBuilder<SelectedBookableSpaceBloc,
SelectedBookableSpaceState>(
builder: (context, roomState) {
final selectedRoom =
roomState.selectedBookableSpace;
return BlocBuilder<DateSelectionBloc,
DateSelectionState>(
builder: (context, dateState) {
return BlocListener<CalendarEventsBloc,
CalendarEventState>(
listenWhen: (prev, curr) =>
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,
selectedDate: dateState.selectedDate,
eventController: _eventController,
selectedDateFromSideBarCalender: context
.watch<DateSelectionBloc>()
.state
.selectedDateFromSideBarCalender,
),
);
},
);
},
),
),
],
),
),
),
],
),
),
),
),
),
);
}
}

View File

@ -0,0 +1,242 @@
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_svg/flutter_svg.dart';
import 'package:syncrow_web/pages/access_management/booking_system/data/services/remote_bookable_spaces_service.dart';
import 'package:syncrow_web/pages/access_management/booking_system/domain/models/bookable_room.dart';
import 'package:syncrow_web/pages/access_management/booking_system/presentation/bloc/sidebar/sidebar_bloc.dart';
import 'package:syncrow_web/pages/access_management/booking_system/presentation/bloc/sidebar/sidebar_event.dart';
import 'package:syncrow_web/pages/access_management/booking_system/presentation/bloc/sidebar/sidebar_state.dart';
import 'package:syncrow_web/pages/access_management/booking_system/presentation/view/widgets/room_list_item.dart';
import 'package:syncrow_web/services/api/http_service.dart';
import 'package:syncrow_web/utils/color_manager.dart';
import 'package:syncrow_web/utils/constants/assets.dart';
class BookingSidebar extends StatelessWidget {
final void Function(BookableSpaceModel) onRoomSelected;
const BookingSidebar({
super.key,
required this.onRoomSelected,
});
@override
Widget build(BuildContext context) {
return BlocProvider(
create: (context) => SidebarBloc(RemoteBookableSpacesService(
HTTPService(),
))
..add(LoadBookableSpaces()),
child: _SidebarContent(onRoomSelected: onRoomSelected),
);
}
}
class _SidebarContent extends StatefulWidget {
final void Function(BookableSpaceModel) onRoomSelected;
const _SidebarContent({
required this.onRoomSelected,
});
@override
State<_SidebarContent> createState() => __SidebarContentState();
}
class __SidebarContentState extends State<_SidebarContent> {
final TextEditingController searchController = TextEditingController();
final ScrollController _scrollController = ScrollController();
@override
void initState() {
super.initState();
_scrollController.addListener(_scrollListener);
}
@override
void dispose() {
_scrollController.dispose();
super.dispose();
}
void _scrollListener() {
if (_scrollController.position.pixels ==
_scrollController.position.maxScrollExtent) {
context.read<SidebarBloc>().add(LoadMoreSpaces());
}
}
void _handleSearch(String value) {
context.read<SidebarBloc>().add(SearchRoomsEvent(value));
}
@override
Widget build(BuildContext context) {
return BlocConsumer<SidebarBloc, SidebarState>(
listener: (context, state) {
if (state.currentPage == 1 && searchController.text.isNotEmpty) {
searchController.clear();
}
},
builder: (context, state) {
return Column(
children: [
const _SidebarHeader(title: 'Spaces'),
Container(
decoration: BoxDecoration(
color: ColorsManager.whiteColors,
borderRadius: BorderRadius.circular(8.0),
boxShadow: [
BoxShadow(
color: ColorsManager.blackColor.withOpacity(0.1),
offset: const Offset(0, -2),
blurRadius: 4,
spreadRadius: 0,
),
BoxShadow(
color: ColorsManager.blackColor.withOpacity(0.1),
offset: const Offset(0, 2),
blurRadius: 4,
spreadRadius: 0,
),
],
),
child: Padding(
padding: const EdgeInsets.all(8.0),
child: Container(
child: Padding(
padding: const EdgeInsets.symmetric(
horizontal: 16.0, vertical: 8.0),
child: Container(
decoration: BoxDecoration(
color: ColorsManager.counterBackgroundColor,
borderRadius: BorderRadius.circular(8.0),
),
child: Row(
children: [
Expanded(
child: TextField(
style: Theme.of(context)
.textTheme
.bodyMedium
?.copyWith(
color: ColorsManager.blackColor,
),
controller: searchController,
onChanged: _handleSearch,
decoration: InputDecoration(
hintText: 'Search',
suffixIcon: Padding(
padding: const EdgeInsets.all(8.0),
child: SizedBox(
width: 20,
height: 20,
child: SvgPicture.asset(
Assets.searchIconUser,
color: ColorsManager.primaryTextColor,
),
),
),
contentPadding: const EdgeInsets.symmetric(
vertical: 8, horizontal: 12),
border: const OutlineInputBorder(
borderSide: BorderSide.none),
),
),
),
if (searchController.text.isNotEmpty)
IconButton(
icon: const Icon(Icons.close),
onPressed: () {
context.read<SidebarBloc>().add(ResetSearch());
},
),
],
),
),
),
),
),
),
if (state.isLoading)
const Expanded(
child: Center(child: CircularProgressIndicator()),
)
else if (state.errorMessage != null)
Expanded(
child: Center(child: Text(state.errorMessage!)),
)
else
Expanded(
child: ListView.builder(
controller: _scrollController,
itemCount:
state.displayedRooms.length + (state.hasMore ? 1 : 0),
itemBuilder: (context, index) {
if (index == state.displayedRooms.length) {
return _buildLoadMoreIndicator(state);
}
final room = state.displayedRooms[index];
return RoomListItem(
room: room,
isSelected: state.selectedRoomId == room.uuid,
onTap: () {
context
.read<SidebarBloc>()
.add(SelectRoomEvent(room.uuid));
widget.onRoomSelected(room);
},
);
},
),
),
],
);
},
);
}
Widget _buildLoadMoreIndicator(SidebarState state) {
if (state.isLoadingMore) {
return const Padding(
padding: EdgeInsets.symmetric(vertical: 16.0),
child: Center(child: CircularProgressIndicator()),
);
} else if (state.hasMore) {
return const Padding(
padding: EdgeInsets.symmetric(vertical: 16.0),
child: Center(child: Text('Scroll to load more')),
);
} else {
return const SizedBox.shrink();
}
}
}
class _SidebarHeader extends StatelessWidget {
final String title;
const _SidebarHeader({
required this.title,
});
@override
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.all(16.0),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
title,
style: Theme.of(context).textTheme.titleLarge?.copyWith(
fontWeight: FontWeight.w400,
color: ColorsManager.primaryTextColor,
fontSize: 20,
),
),
],
),
);
}
}

View File

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

View File

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

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

View File

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

View File

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

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

View File

@ -4,6 +4,7 @@ import 'package:syncrow_web/pages/access_management/bloc/access_bloc.dart';
import 'package:syncrow_web/pages/access_management/bloc/access_event.dart'; import 'package:syncrow_web/pages/access_management/bloc/access_event.dart';
import 'package:syncrow_web/pages/access_management/booking_system/view/booking_page.dart'; import 'package:syncrow_web/pages/access_management/booking_system/view/booking_page.dart';
import 'package:syncrow_web/pages/access_management/manage_bookable_spaces/presentation/screens/manage_bookable_spaces_screen.dart'; import 'package:syncrow_web/pages/access_management/manage_bookable_spaces/presentation/screens/manage_bookable_spaces_screen.dart';
import 'package:syncrow_web/pages/access_management/booking_system/presentation/view/booking_page.dart' hide BookingPage;
import 'package:syncrow_web/pages/access_management/view/access_overview_content.dart'; import 'package:syncrow_web/pages/access_management/view/access_overview_content.dart';
import 'package:syncrow_web/pages/device_managment/shared/navigate_home_grid_view.dart'; import 'package:syncrow_web/pages/device_managment/shared/navigate_home_grid_view.dart';
import 'package:syncrow_web/utils/extension/build_context_x.dart'; import 'package:syncrow_web/utils/extension/build_context_x.dart';

View File

@ -132,6 +132,8 @@ class _DynamicTableState extends State<DynamicTable> {
child: SingleChildScrollView( child: SingleChildScrollView(
controller: _horizontalScrollController, controller: _horizontalScrollController,
scrollDirection: Axis.horizontal, scrollDirection: Axis.horizontal,
physics:
widget.isEmpty ? const NeverScrollableScrollPhysics() : null,
child: SizedBox( child: SizedBox(
width: _totalTableWidth, width: _totalTableWidth,
child: Column( child: Column(
@ -164,7 +166,6 @@ class _DynamicTableState extends State<DynamicTable> {
], ],
), ),
), ),
Expanded( Expanded(
child: widget.isEmpty child: widget.isEmpty
? _buildEmptyState() ? _buildEmptyState()
@ -265,7 +266,7 @@ class _DynamicTableState extends State<DynamicTable> {
), ),
], ],
), ),
SizedBox(height: widget.size.height * 0.5), SizedBox(height: widget.size.height * 0.2),
], ],
), ),
); );

View File

@ -46,15 +46,15 @@ class DeviceManagementBloc
final projectUuid = await ProjectManager.getProjectUUID() ?? ''; final projectUuid = await ProjectManager.getProjectUUID() ?? '';
if (spaceBloc.state.selectedCommunities.isEmpty) { if (spaceBloc.state.selectedCommunities.isEmpty) {
devices = await DevicesManagementApi().fetchDevices('', '', projectUuid); devices = await DevicesManagementApi().fetchDevices(
projectUuid,
);
} else { } else {
for (final community in spaceBloc.state.selectedCommunities) { for (var community in spaceBloc.state.selectedCommunities) {
final spacesList = final spacesList =
spaceBloc.state.selectedCommunityAndSpaces[community] ?? []; spaceBloc.state.selectedCommunityAndSpaces[community] ?? [];
for (final space in spacesList) { devices.addAll(await DevicesManagementApi()
devices.addAll(await DevicesManagementApi() .fetchDevices(projectUuid, spacesId: spacesList));
.fetchDevices(community, space, projectUuid));
}
} }
} }

View File

@ -24,12 +24,12 @@ class DeviceManagementPage extends StatefulWidget with HelperResponsiveLayout {
} }
class _DeviceManagementPageState extends State<DeviceManagementPage> { class _DeviceManagementPageState extends State<DeviceManagementPage> {
@override
@override
void initState() { void initState() {
context.read<SpaceTreeBloc>().add(InitialEvent()); context.read<SpaceTreeBloc>().add(InitialEvent());
super.initState(); super.initState();
} }
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return MultiBlocProvider( return MultiBlocProvider(
@ -90,7 +90,7 @@ class _DeviceManagementPageState extends State<DeviceManagementPage> {
const TriggerSwitchTabsEvent(isRoutineTab: true)); const TriggerSwitchTabsEvent(isRoutineTab: true));
}, },
child: Text( child: Text(
'Routines', 'Workflow Automation',
style: context.textTheme.titleMedium?.copyWith( style: context.textTheme.titleMedium?.copyWith(
color: state.routineTab color: state.routineTab
? ColorsManager.whiteColors ? ColorsManager.whiteColors

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -12,7 +12,7 @@ class RolesUserModel {
final dynamic jobTitle; final dynamic jobTitle;
final dynamic createdDate; final dynamic createdDate;
final dynamic createdTime; final dynamic createdTime;
final String? companyName;
RolesUserModel({ RolesUserModel({
required this.uuid, required this.uuid,
required this.createdAt, required this.createdAt,
@ -27,6 +27,7 @@ class RolesUserModel {
this.jobTitle, this.jobTitle,
required this.createdDate, required this.createdDate,
required this.createdTime, required this.createdTime,
this.companyName,
}); });
factory RolesUserModel.fromJson(Map<String, dynamic> json) { factory RolesUserModel.fromJson(Map<String, dynamic> json) {
@ -47,6 +48,7 @@ class RolesUserModel {
: json['jobTitle'], : json['jobTitle'],
createdDate: json['createdDate'], createdDate: json['createdDate'],
createdTime: json['createdTime'], createdTime: json['createdTime'],
companyName: json['companyName'] as String?,
); );
} }
} }

View File

@ -52,7 +52,7 @@ class UsersBloc extends Bloc<UsersEvent, UsersState> {
final TextEditingController lastNameController = TextEditingController(); final TextEditingController lastNameController = TextEditingController();
final TextEditingController emailController = TextEditingController(); final TextEditingController emailController = TextEditingController();
final TextEditingController phoneController = TextEditingController(); final TextEditingController phoneController = TextEditingController();
final TextEditingController jobTitleController = TextEditingController(); final TextEditingController companyNameController = TextEditingController();
final TextEditingController roleSearchController = TextEditingController(); final TextEditingController roleSearchController = TextEditingController();
bool? isCompleteBasics; bool? isCompleteBasics;
@ -352,7 +352,7 @@ class UsersBloc extends Bloc<UsersEvent, UsersState> {
bool res = await UserPermissionApi().sendInviteUser( bool res = await UserPermissionApi().sendInviteUser(
email: emailController.text, email: emailController.text,
firstName: firstNameController.text, firstName: firstNameController.text,
jobTitle: jobTitleController.text, companyName: companyNameController.text,
lastName: lastNameController.text, lastName: lastNameController.text,
phoneNumber: phoneController.text, phoneNumber: phoneController.text,
roleUuid: roleSelected, roleUuid: roleSelected,
@ -405,7 +405,7 @@ class UsersBloc extends Bloc<UsersEvent, UsersState> {
bool res = await UserPermissionApi().editInviteUser( bool res = await UserPermissionApi().editInviteUser(
userId: event.userId, userId: event.userId,
firstName: firstNameController.text, firstName: firstNameController.text,
jobTitle: jobTitleController.text, companyName: companyNameController.text,
lastName: lastNameController.text, lastName: lastNameController.text,
phoneNumber: phoneController.text, phoneNumber: phoneController.text,
roleUuid: roleSelected, roleUuid: roleSelected,
@ -455,7 +455,7 @@ class UsersBloc extends Bloc<UsersEvent, UsersState> {
Future<void> checkEmail( Future<void> checkEmail(
CheckEmailEvent event, Emitter<UsersState> emit) async { CheckEmailEvent event, Emitter<UsersState> emit) async {
emit(UsersLoadingState()); emit(UsersLoadingState());
String? res = await UserPermissionApi().checkEmail( String? res = await UserPermissionApi().checkEmail(
emailController.text, emailController.text,
); );
checkEmailValid = res!; checkEmailValid = res!;
@ -529,7 +529,7 @@ class UsersBloc extends Bloc<UsersEvent, UsersState> {
lastNameController.text = res.lastName; lastNameController.text = res.lastName;
emailController.text = res.email; emailController.text = res.email;
phoneController.text = res.phoneNumber ?? ''; phoneController.text = res.phoneNumber ?? '';
jobTitleController.text = res.jobTitle ?? ''; companyNameController.text = res.companyName ?? '';
res.roleType; res.roleType;
res.spaces.map((space) { res.spaces.map((space) {
selectedIds.add(space.uuid); selectedIds.add(space.uuid);
@ -645,7 +645,7 @@ class UsersBloc extends Bloc<UsersEvent, UsersState> {
lastNameController.dispose(); lastNameController.dispose();
emailController.dispose(); emailController.dispose();
phoneController.dispose(); phoneController.dispose();
jobTitleController.dispose(); companyNameController.dispose();
roleSearchController.dispose(); roleSearchController.dispose();
return super.close(); return super.close();
} }

View File

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

View File

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

View File

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

View File

@ -96,9 +96,7 @@ class _WallPresenceSensorState extends State<FlushPresenceSensor> {
child: Column( child: Column(
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
children: [ children: [
DialogHeader(widget.dialogType == 'THEN' const DialogHeader('Presence Sensor'),
? 'Presence Sensor Functions'
: 'Presence Sensor Condition'),
Expanded(child: _buildMainContent(context, state)), Expanded(child: _buildMainContent(context, state)),
_buildDialogFooter(context, state), _buildDialogFooter(context, state),
], ],

View File

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

View File

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

View File

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

View File

@ -7,25 +7,58 @@ import 'package:syncrow_web/pages/space_management_v2/modules/communities/data/s
import 'package:syncrow_web/pages/space_management_v2/modules/communities/domain/params/load_communities_param.dart'; import 'package:syncrow_web/pages/space_management_v2/modules/communities/domain/params/load_communities_param.dart';
import 'package:syncrow_web/pages/space_management_v2/modules/communities/presentation/bloc/communities_bloc.dart'; import 'package:syncrow_web/pages/space_management_v2/modules/communities/presentation/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/products/data/services/remote_products_service.dart';
import 'package:syncrow_web/pages/space_management_v2/modules/products/presentation/bloc/products_bloc.dart';
import 'package:syncrow_web/pages/space_management_v2/modules/space_details/data/services/remote_space_details_service.dart';
import 'package:syncrow_web/pages/space_management_v2/modules/space_details/data/services/unique_subspaces_decorator.dart';
import 'package:syncrow_web/pages/space_management_v2/modules/space_details/presentation/bloc/space_details_bloc.dart';
import 'package:syncrow_web/services/api/http_service.dart'; import 'package:syncrow_web/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) => SpaceDetailsBloc(
UniqueSubspacesDecorator(
RemoteSpaceDetailsService(httpService: HTTPService()),
),
),
),
BlocProvider(
create: (context) => ProductsBloc(
RemoteProductsService(HTTPService()),
),
), ),
BlocProvider(create: (context) => CommunitiesTreeSelectionBloc()),
], ],
child: WebScaffold( child: WebScaffold(
appBarTitle: Text( appBarTitle: Text(

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,58 +251,178 @@ 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;
final spaceCard = SpaceCardWidget(
buildSpaceContainer: () {
return Opacity(
opacity: hasNoSelectedSpace || isHighlighted ? 1.0 : 0.5,
child: Tooltip(
message: space.spaceName,
preferBelow: false,
child: SpaceCell(
onTap: () => _onSpaceTapped(space),
icon: space.icon,
name: space.spaceName,
),
),
);
},
onTap: () => SpaceDetailsDialogHelper.showCreate(
context,
communityUuid: widget.community.uuid,
),
);
final reorderData = SpaceReorderDataModel(
space: space,
parent: parent,
community: community,
);
widgets.add( widgets.add(
Positioned( Positioned(
left: position.dx, left: position.dx,
top: position.dy, top: position.dy,
width: _cardWidth, width: _cardWidth,
height: _cardHeight, height: _cardHeight,
child: SpaceCardWidget( child: Draggable<SpaceReorderDataModel>(
buildSpaceContainer: () { data: reorderData,
return Opacity( feedback: Material(
opacity: hasNoSelectedSpace || isHighlighted ? 1.0 : 0.5, color: Colors.transparent,
child: Tooltip( child: Opacity(
message: space.spaceName, opacity: 0.2,
preferBelow: false, child: SizedBox(
child: SpaceCell( width: _cardWidth,
onTap: () => _onSpaceTapped(space), height: _cardHeight,
icon: space.icon, child: spaceCard,
name: space.spaceName,
),
), ),
); ),
}, ),
onTap: () => SpaceDetailsDialogHelper.showCreate(context), 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

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

View File

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

View File

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

View File

@ -2,38 +2,66 @@ 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),
child: Container( message: 'Create a new space',
height: 60, child: InkWell(
decoration: BoxDecoration( onTap: () => SpaceDetailsDialogHelper.showCreate(
color: Colors.white, context,
borderRadius: BorderRadius.circular(20), communityUuid: widget.communityUuid,
boxShadow: [
BoxShadow(
color: Colors.grey.withValues(alpha: 0.5),
spreadRadius: 5,
blurRadius: 7,
offset: const Offset(0, 3),
),
],
), ),
child: Center( child: MouseRegion(
child: Container( onEnter: (_) => setState(() => _isHovered = true),
width: 40, onExit: (_) => setState(() => _isHovered = false),
height: 40, child: AnimatedOpacity(
decoration: const BoxDecoration( duration: const Duration(milliseconds: 100),
color: ColorsManager.boxColor, opacity: _isHovered ? 1.0 : 0.45,
shape: BoxShape.circle, child: Container(
), width: 150,
child: const Icon( height: 90,
Icons.add, decoration: BoxDecoration(
color: Colors.blue, color: Colors.white,
borderRadius: BorderRadius.circular(20),
boxShadow: [
BoxShadow(
color: Colors.grey.withValues(alpha: 0.2),
spreadRadius: 3,
blurRadius: 8,
offset: const Offset(0, 4),
),
],
),
child: Container(
margin: const EdgeInsets.symmetric(vertical: 20),
decoration: BoxDecoration(
border: Border.all(color: ColorsManager.borderColor, width: 2),
color: ColorsManager.boxColor,
shape: BoxShape.circle,
),
child: const Center(
child: Icon(
Icons.add,
color: Colors.blue,
),
),
),
), ),
), ),
), ),

View File

@ -22,22 +22,20 @@ 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, children: [
children: [ widget.buildSpaceContainer(),
widget.buildSpaceContainer(), if (isHovered)
if (isHovered) Positioned(
Positioned( bottom: 0,
bottom: 0, child: PlusButtonWidget(
child: PlusButtonWidget( offset: Offset.zero,
offset: Offset.zero, onButtonTap: widget.onTap,
onButtonTap: widget.onTap,
),
), ),
], ),
), ],
), ),
); );
} }

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

@ -1,6 +1,7 @@
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/widgets/community_structure_canvas.dart'; import 'package:syncrow_web/pages/space_management_v2/main_module/widgets/community_structure_canvas.dart';
import 'package:syncrow_web/pages/space_management_v2/main_module/widgets/community_structure_header.dart';
import 'package:syncrow_web/pages/space_management_v2/main_module/widgets/create_space_button.dart'; import 'package:syncrow_web/pages/space_management_v2/main_module/widgets/create_space_button.dart';
import 'package:syncrow_web/pages/space_management_v2/modules/communities/presentation/communities_tree_selection_bloc/communities_tree_selection_bloc.dart'; import 'package:syncrow_web/pages/space_management_v2/modules/communities/presentation/communities_tree_selection_bloc/communities_tree_selection_bloc.dart';
@ -12,15 +13,29 @@ 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: CommunityStructureCanvas( child: Column(
community: selectedCommunity, mainAxisSize: MainAxisSize.min,
selectedSpace: selectedSpace, children: [
const CommunityStructureHeader(),
Expanded(
child: CommunityStructureCanvas(
community: selectedCommunity,
selectedSpace: selectedSpace,
),
),
],
), ),
); );
} }

View File

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

View File

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

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

View File

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

View File

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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,6 +1,8 @@
import 'package:equatable/equatable.dart'; import 'package:equatable/equatable.dart';
import 'package:syncrow_web/pages/space_management_v2/modules/products/domain/models/product.dart'; import 'package:syncrow_web/pages/space_management_v2/modules/products/domain/models/product.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/constants/assets.dart';
import 'package:uuid/uuid.dart';
class SpaceDetailsModel extends Equatable { class SpaceDetailsModel extends Equatable {
final String uuid; final String uuid;
@ -17,6 +19,13 @@ class SpaceDetailsModel extends Equatable {
required this.subspaces, required this.subspaces,
}); });
factory SpaceDetailsModel.empty() => const SpaceDetailsModel(
uuid: '',
spaceName: '',
icon: Assets.location,
productAllocations: [],
subspaces: [],
);
factory SpaceDetailsModel.fromJson(Map<String, dynamic> json) { factory SpaceDetailsModel.fromJson(Map<String, dynamic> json) {
return SpaceDetailsModel( return SpaceDetailsModel(
uuid: json['uuid'] as String, uuid: json['uuid'] as String,
@ -31,14 +40,20 @@ class SpaceDetailsModel extends Equatable {
); );
} }
Map<String, dynamic> toJson() { SpaceDetailsModel copyWith({
return { String? uuid,
'uuid': uuid, String? spaceName,
'spaceName': spaceName, String? icon,
'icon': icon, List<ProductAllocation>? productAllocations,
'productAllocations': productAllocations.map((e) => e.toJson()).toList(), List<Subspace>? subspaces,
'subspaces': subspaces.map((e) => e.toJson()).toList(), }) {
}; return SpaceDetailsModel(
uuid: uuid ?? this.uuid,
spaceName: spaceName ?? this.spaceName,
icon: icon ?? this.icon,
productAllocations: productAllocations ?? this.productAllocations,
subspaces: subspaces ?? this.subspaces,
);
} }
@override @override
@ -46,32 +61,38 @@ class SpaceDetailsModel extends Equatable {
} }
class ProductAllocation extends Equatable { class ProductAllocation extends Equatable {
final String uuid;
final Product product; final Product product;
final Tag tag; final Tag tag;
final String? location;
const ProductAllocation({ const ProductAllocation({
required this.uuid,
required this.product, required this.product,
required this.tag, required this.tag,
this.location,
}); });
factory ProductAllocation.fromJson(Map<String, dynamic> json) { factory ProductAllocation.fromJson(Map<String, dynamic> json) {
return ProductAllocation( return ProductAllocation(
uuid: json['uuid'] as String? ?? const Uuid().v4(),
product: Product.fromJson(json['product'] as Map<String, dynamic>), product: Product.fromJson(json['product'] as Map<String, dynamic>),
tag: Tag.fromJson(json['tag'] as Map<String, dynamic>), tag: Tag.fromJson(json['tag'] as Map<String, dynamic>),
); );
} }
Map<String, dynamic> toJson() { ProductAllocation copyWith({
return { String? uuid,
'product': product.toJson(), Product? product,
'tag': tag.toJson(), Tag? tag,
}; }) {
return ProductAllocation(
uuid: uuid ?? this.uuid,
product: product ?? this.product,
tag: tag ?? this.tag,
);
} }
@override @override
List<Object?> get props => [product, tag]; List<Object?> get props => [uuid, product, tag];
} }
class Subspace extends Equatable { class Subspace extends Equatable {
@ -88,19 +109,23 @@ class Subspace extends Equatable {
factory Subspace.fromJson(Map<String, dynamic> json) { factory Subspace.fromJson(Map<String, dynamic> json) {
return Subspace( return Subspace(
uuid: json['uuid'] as String, uuid: json['uuid'] as String,
name: json['name'] as String, name: json['subspaceName'] as String,
productAllocations: (json['productAllocations'] as List) productAllocations: (json['productAllocations'] as List)
.map((e) => ProductAllocation.fromJson(e as Map<String, dynamic>)) .map((e) => ProductAllocation.fromJson(e as Map<String, dynamic>))
.toList(), .toList(),
); );
} }
Map<String, dynamic> toJson() { Subspace copyWith({
return { String? uuid,
'uuid': uuid, String? name,
'name': name, List<ProductAllocation>? productAllocations,
'productAllocations': productAllocations.map((e) => e.toJson()).toList(), }) {
}; return Subspace(
uuid: uuid ?? this.uuid,
name: name ?? this.name,
productAllocations: productAllocations ?? this.productAllocations,
);
} }
@override @override

View File

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

View File

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

View File

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

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