mirror of
https://github.com/SyncrowIOT/web.git
synced 2025-07-14 17:25:50 +00:00
Compare commits
72 Commits
42c410d982
...
Implement_
Author | SHA1 | Date | |
---|---|---|---|
65d541d594 | |||
7cc59e43df | |||
21f8b2962c | |||
645a07287e | |||
df29aab111 | |||
e55e793081 | |||
885ef61114 | |||
bfd6b5c3a0 | |||
2b638940ae | |||
3e95bf4473 | |||
2d16bda61d | |||
5c90d5f6b9 | |||
d6a48850a7 | |||
6cac94a1c4 | |||
9f28e1ccef | |||
6534bfae5b | |||
4cfb984d2c | |||
4c06479469 | |||
3101960201 | |||
ddfd4ee153 | |||
7f0484eec6 | |||
dc7064d142 | |||
e523a83912 | |||
e917225c3d | |||
66ed30b50c | |||
47bd6ff89e | |||
138390496c | |||
df87e41d61 | |||
f0bfe085a4 | |||
bb846f797f | |||
e234c9f3b2 | |||
bcd0ae4a2a | |||
cebce2ce7f | |||
97e3fb68bf | |||
46a7add90d | |||
73de1e6ff9 | |||
826dea8054 | |||
fdea4b1cd0 | |||
823d86fd80 | |||
dd735032ea | |||
6dcc851d97 | |||
15b36fd052 | |||
a4024067c7 | |||
95cded4bf5 | |||
757a96ed9f | |||
b857736e10 | |||
1fccd51440 | |||
c07ddb0ccd | |||
58e99f95b2 | |||
227df6fe3d | |||
9451ec0cc4 | |||
fc797c2646 | |||
318e1d9af7 | |||
d47dc349bc | |||
c221c8499f | |||
71cf4b9feb | |||
c43cf9347f | |||
9990b1805e | |||
009b7c0316 | |||
779c0fe916 | |||
e448eabda6 | |||
9dfb3ed369 | |||
63353af38b | |||
68b6c9b18c | |||
fa6ee9a0af | |||
3601b02bc3 | |||
fdd0526c78 | |||
bdeec7d325 | |||
50ff17a0c1 | |||
87c2e3261d | |||
62a6f9c993 | |||
f7e4d6ff07 |
@ -1,2 +1,3 @@
|
||||
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/
|
||||
|
@ -1,2 +1,3 @@
|
||||
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/
|
||||
|
@ -1,2 +1,3 @@
|
||||
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
112
.vscode/launch.json
vendored
@ -1,67 +1,49 @@
|
||||
{
|
||||
"configurations": [
|
||||
|
||||
{
|
||||
|
||||
"name": "DEVELOPMENT",
|
||||
|
||||
"request": "launch",
|
||||
|
||||
"type": "dart",
|
||||
|
||||
"args": [
|
||||
"-d",
|
||||
"chrome",
|
||||
"--web-port",
|
||||
"3000",
|
||||
"-t",
|
||||
"lib/main_dev.dart",
|
||||
"--web-experimental-hot-reload",
|
||||
],
|
||||
|
||||
"flutterMode": "debug"
|
||||
|
||||
},{
|
||||
|
||||
"name": "STAGING",
|
||||
|
||||
"request": "launch",
|
||||
|
||||
"type": "dart",
|
||||
|
||||
"args": [
|
||||
"-d",
|
||||
"chrome",
|
||||
"--web-port",
|
||||
"3000",
|
||||
"-t",
|
||||
"lib/main_staging.dart",
|
||||
"--web-experimental-hot-reload",
|
||||
],
|
||||
|
||||
"flutterMode": "debug"
|
||||
|
||||
},{
|
||||
|
||||
"name": "PRODUCTION",
|
||||
|
||||
"request": "launch",
|
||||
|
||||
"type": "dart",
|
||||
|
||||
"args": [
|
||||
"-d",
|
||||
"chrome",
|
||||
"--web-port",
|
||||
"3000",
|
||||
"-t",
|
||||
"lib/main.dart",
|
||||
"--web-experimental-hot-reload",
|
||||
],
|
||||
|
||||
"flutterMode": "debug"
|
||||
|
||||
},
|
||||
|
||||
]
|
||||
"configurations": [
|
||||
{
|
||||
"name": "DEVELOPMENT",
|
||||
"request": "launch",
|
||||
"type": "dart",
|
||||
"args": [
|
||||
"-d",
|
||||
"chrome",
|
||||
"--web-port",
|
||||
"3000",
|
||||
"-t",
|
||||
"lib/main_dev.dart",
|
||||
"--web-experimental-hot-reload"
|
||||
],
|
||||
"flutterMode": "debug"
|
||||
},
|
||||
{
|
||||
"name": "STAGING",
|
||||
"request": "launch",
|
||||
"type": "dart",
|
||||
"args": [
|
||||
"-d",
|
||||
"chrome",
|
||||
"--web-port",
|
||||
"3000",
|
||||
"-t",
|
||||
"lib/main_staging.dart",
|
||||
"--web-experimental-hot-reload"
|
||||
],
|
||||
"flutterMode": "debug"
|
||||
},
|
||||
{
|
||||
"name": "PRODUCTION",
|
||||
"request": "launch",
|
||||
"type": "dart",
|
||||
"args": [
|
||||
"-d",
|
||||
"chrome",
|
||||
"--web-port",
|
||||
"3000",
|
||||
"-t",
|
||||
"lib/main.dart",
|
||||
"--web-experimental-hot-reload"
|
||||
],
|
||||
"flutterMode": "debug"
|
||||
}
|
||||
]
|
||||
}
|
@ -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
16
lib/firebase_options.dart
Normal 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',
|
||||
);
|
||||
}
|
@ -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',
|
||||
);
|
||||
}
|
@ -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',
|
||||
);
|
||||
}
|
@ -4,7 +4,7 @@ import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:flutter_dotenv/flutter_dotenv.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
import 'package:syncrow_web/firebase_options_prod.dart';
|
||||
import 'package:syncrow_web/firebase_options.dart';
|
||||
import 'package:syncrow_web/pages/auth/bloc/auth_bloc.dart';
|
||||
import 'package:syncrow_web/pages/home/bloc/home_bloc.dart';
|
||||
import 'package:syncrow_web/pages/home/bloc/home_event.dart';
|
||||
@ -27,7 +27,9 @@ Future<void> main() async {
|
||||
await dotenv.load(fileName: '.env.$environment');
|
||||
WidgetsFlutterBinding.ensureInitialized();
|
||||
await Firebase.initializeApp(
|
||||
options: DefaultFirebaseOptionsStaging.currentPlatform,
|
||||
options: DefaultFirebaseOptions(
|
||||
databaseUrl: dotenv.env['RTDB_URL']!,
|
||||
),
|
||||
);
|
||||
initialSetup();
|
||||
} catch (_) {}
|
||||
@ -59,7 +61,7 @@ class MyApp extends StatelessWidget {
|
||||
BlocProvider<CreateRoutineBloc>(
|
||||
create: (context) => CreateRoutineBloc(),
|
||||
),
|
||||
BlocProvider(create: (context) => HomeBloc()..add(FetchUserInfo())),
|
||||
BlocProvider(create: (context) => HomeBloc()..add(const FetchUserInfo())),
|
||||
BlocProvider<VisitorPasswordBloc>(
|
||||
create: (context) => VisitorPasswordBloc(),
|
||||
),
|
||||
|
@ -4,7 +4,7 @@ import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:flutter_dotenv/flutter_dotenv.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
import 'package:syncrow_web/firebase_options_dev.dart';
|
||||
import 'package:syncrow_web/firebase_options.dart';
|
||||
import 'package:syncrow_web/pages/auth/bloc/auth_bloc.dart';
|
||||
import 'package:syncrow_web/pages/home/bloc/home_bloc.dart';
|
||||
import 'package:syncrow_web/pages/home/bloc/home_event.dart';
|
||||
@ -27,7 +27,9 @@ Future<void> main() async {
|
||||
await dotenv.load(fileName: '.env.$environment');
|
||||
WidgetsFlutterBinding.ensureInitialized();
|
||||
await Firebase.initializeApp(
|
||||
options: DefaultFirebaseOptionsDev.currentPlatform,
|
||||
options: DefaultFirebaseOptions(
|
||||
databaseUrl: dotenv.env['RTDB_URL']!,
|
||||
),
|
||||
);
|
||||
initialSetup();
|
||||
} catch (_) {}
|
||||
@ -59,7 +61,7 @@ class MyApp extends StatelessWidget {
|
||||
BlocProvider<CreateRoutineBloc>(
|
||||
create: (context) => CreateRoutineBloc(),
|
||||
),
|
||||
BlocProvider(create: (context) => HomeBloc()..add(FetchUserInfo())),
|
||||
BlocProvider(create: (context) => HomeBloc()..add(const FetchUserInfo())),
|
||||
BlocProvider<VisitorPasswordBloc>(
|
||||
create: (context) => VisitorPasswordBloc(),
|
||||
),
|
||||
|
@ -4,7 +4,7 @@ import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:flutter_dotenv/flutter_dotenv.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
import 'package:syncrow_web/firebase_options_prod.dart';
|
||||
import 'package:syncrow_web/firebase_options.dart';
|
||||
import 'package:syncrow_web/pages/auth/bloc/auth_bloc.dart';
|
||||
import 'package:syncrow_web/pages/home/bloc/home_bloc.dart';
|
||||
import 'package:syncrow_web/pages/home/bloc/home_event.dart';
|
||||
@ -24,7 +24,9 @@ Future<void> main() async {
|
||||
await dotenv.load(fileName: '.env.$environment');
|
||||
WidgetsFlutterBinding.ensureInitialized();
|
||||
await Firebase.initializeApp(
|
||||
options: DefaultFirebaseOptionsStaging.currentPlatform,
|
||||
options: DefaultFirebaseOptions(
|
||||
databaseUrl: dotenv.env['RTDB_URL']!,
|
||||
),
|
||||
);
|
||||
initialSetup();
|
||||
} catch (_) {}
|
||||
@ -56,7 +58,7 @@ class MyApp extends StatelessWidget {
|
||||
BlocProvider<CreateRoutineBloc>(
|
||||
create: (context) => CreateRoutineBloc(),
|
||||
),
|
||||
BlocProvider(create: (context) => HomeBloc()..add(FetchUserInfo())),
|
||||
BlocProvider(create: (context) => HomeBloc()..add(const FetchUserInfo())),
|
||||
BlocProvider<VisitorPasswordBloc>(
|
||||
create: (context) => VisitorPasswordBloc(),
|
||||
),
|
||||
|
@ -2,7 +2,7 @@ import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:syncrow_web/pages/access_management/bloc/access_event.dart';
|
||||
import 'package:syncrow_web/pages/access_management/bloc/access_state.dart';
|
||||
import 'package:syncrow_web/pages/access_management/model/password_model.dart';
|
||||
import 'package:syncrow_web/pages/access_management/booking_system/presentation/model/password_model.dart';
|
||||
import 'package:syncrow_web/pages/common/bloc/project_manager.dart';
|
||||
import 'package:syncrow_web/pages/common/hour_picker_dialog.dart';
|
||||
import 'package:syncrow_web/services/access_mang_api.dart';
|
||||
|
@ -1,5 +1,5 @@
|
||||
import 'package:equatable/equatable.dart';
|
||||
import 'package:syncrow_web/pages/access_management/model/password_model.dart';
|
||||
import 'package:syncrow_web/pages/access_management/booking_system/presentation/model/password_model.dart';
|
||||
|
||||
abstract class AccessState extends Equatable {
|
||||
const AccessState();
|
||||
|
@ -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()}');
|
||||
}
|
||||
}
|
||||
}
|
@ -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()}');
|
||||
}
|
||||
}
|
||||
}
|
@ -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];
|
||||
}
|
@ -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,
|
||||
);
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
@ -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,
|
||||
);
|
||||
}
|
||||
}
|
@ -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,
|
||||
});
|
||||
}
|
@ -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,
|
||||
});
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
@ -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();
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
@ -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);
|
||||
}
|
@ -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));
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
@ -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,
|
||||
);
|
||||
}
|
||||
}
|
@ -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));
|
||||
});
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
@ -0,0 +1,9 @@
|
||||
part of 'selected_bookable_space_bloc.dart';
|
||||
|
||||
class SelectedBookableSpaceState {
|
||||
final BookableSpaceModel? selectedBookableSpace;
|
||||
|
||||
const SelectedBookableSpaceState(
|
||||
{ this.selectedBookableSpace,}
|
||||
);
|
||||
}
|
@ -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();
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
@ -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,
|
||||
);
|
||||
}
|
||||
}
|
@ -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,
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
@ -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,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
@ -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(),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
@ -20,13 +20,13 @@ class SvgTextButton extends StatelessWidget {
|
||||
required this.onPressed,
|
||||
this.backgroundColor = ColorsManager.circleRolesBackground,
|
||||
this.svgColor = const Color(0xFF496EFF),
|
||||
this.labelColor = Colors.black87,
|
||||
this.labelColor = Colors.black,
|
||||
this.borderRadius = 10.0,
|
||||
this.boxShadow = const [
|
||||
BoxShadow(
|
||||
color: ColorsManager.textGray,
|
||||
blurRadius: 12,
|
||||
offset: Offset(0, 4),
|
||||
color: ColorsManager.lightGrayColor,
|
||||
blurRadius: 4,
|
||||
offset: Offset(0, 1),
|
||||
),
|
||||
],
|
||||
this.svgSize = 24.0,
|
||||
@ -53,15 +53,14 @@ class SvgTextButton extends StatelessWidget {
|
||||
svgAsset,
|
||||
width: svgSize,
|
||||
height: svgSize,
|
||||
color: svgColor,
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Text(
|
||||
label,
|
||||
style: TextStyle(
|
||||
color: labelColor,
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.w500,
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.w400,
|
||||
),
|
||||
),
|
||||
],
|
@ -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,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
@ -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,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
@ -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,
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
@ -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';
|
||||
}
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
@ -1,52 +0,0 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:syncrow_web/pages/access_management/booking_system/view/widgets/icon_text_button.dart';
|
||||
import 'package:syncrow_web/utils/constants/assets.dart';
|
||||
|
||||
class BookingPage extends StatelessWidget {
|
||||
const BookingPage({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
child: Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: Container(
|
||||
color: Colors.blueGrey[100],
|
||||
child: const Center(
|
||||
child: Text(
|
||||
'Side bar',
|
||||
style: TextStyle(fontSize: 24, fontWeight: FontWeight.bold),
|
||||
),
|
||||
),
|
||||
)),
|
||||
Expanded(
|
||||
flex: 4,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(20.0),
|
||||
child: SizedBox(
|
||||
child: Column(
|
||||
children: [
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.start,
|
||||
children: [
|
||||
SvgTextButton(
|
||||
svgAsset: Assets.homeIcon,
|
||||
label: 'Manage Bookable Spaces',
|
||||
onPressed: () {}),
|
||||
SizedBox(width: 20),
|
||||
SvgTextButton(
|
||||
svgAsset: Assets.groupIcon,
|
||||
label: 'Manage Users',
|
||||
onPressed: () {})
|
||||
],
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
))
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
@ -2,7 +2,7 @@ import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:syncrow_web/pages/access_management/bloc/access_bloc.dart';
|
||||
import 'package:syncrow_web/pages/access_management/bloc/access_event.dart';
|
||||
import 'package:syncrow_web/pages/access_management/booking_system/view/booking_page.dart';
|
||||
import 'package:syncrow_web/pages/access_management/booking_system/presentation/view/booking_page.dart';
|
||||
import 'package:syncrow_web/pages/access_management/view/access_overview_content.dart';
|
||||
import 'package:syncrow_web/pages/device_managment/shared/navigate_home_grid_view.dart';
|
||||
import 'package:syncrow_web/utils/extension/build_context_x.dart';
|
||||
|
@ -105,7 +105,7 @@ class HomeBloc extends Bloc<HomeEvent, HomeState> {
|
||||
color: const Color(0xFF0026A2),
|
||||
),
|
||||
HomeItemModel(
|
||||
title: 'Devices Management',
|
||||
title: 'Device Management',
|
||||
icon: Assets.devicesIcon,
|
||||
active: true,
|
||||
onPress: (context) {
|
||||
|
@ -1,24 +1,39 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:syncrow_web/pages/space_management_v2/modules/communities/presentation/bloc/communities_bloc.dart';
|
||||
import 'package:syncrow_web/pages/space_management_v2/modules/communities/presentation/communities_tree_selection_bloc/communities_tree_selection_bloc.dart';
|
||||
import 'package:syncrow_web/pages/space_management_v2/modules/communities/domain/models/community_model.dart';
|
||||
import 'package:syncrow_web/pages/space_management_v2/modules/create_community/presentation/create_community_dialog.dart';
|
||||
import 'package:syncrow_web/pages/space_management_v2/modules/update_community/presentation/edit_community_dialog.dart';
|
||||
|
||||
abstract final class SpaceManagementCommunityDialogHelper {
|
||||
static void showCreateDialog(BuildContext context) {
|
||||
static void showCreateDialog(BuildContext context) => showDialog<void>(
|
||||
context: context,
|
||||
builder: (_) => const CreateCommunityDialog(),
|
||||
);
|
||||
|
||||
static void showEditDialog(
|
||||
BuildContext context,
|
||||
CommunityModel community,
|
||||
) {
|
||||
showDialog<void>(
|
||||
context: context,
|
||||
builder: (_) => CreateCommunityDialog(
|
||||
title: const SelectableText('Community Name'),
|
||||
onCreateCommunity: (community) {
|
||||
context.read<CommunitiesBloc>().add(
|
||||
InsertCommunity(community),
|
||||
);
|
||||
context.read<CommunitiesTreeSelectionBloc>().add(
|
||||
SelectCommunityEvent(community: community),
|
||||
);
|
||||
},
|
||||
builder: (_) => EditCommunityDialog(
|
||||
community: community,
|
||||
parentContext: context,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
static void showLoadingDialog(BuildContext context) => showDialog<void>(
|
||||
context: context,
|
||||
barrierDismissible: false,
|
||||
builder: (context) => const Center(
|
||||
child: CircularProgressIndicator(),
|
||||
),
|
||||
);
|
||||
|
||||
static void showSuccessSnackBar(BuildContext context, String message) =>
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(message),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
@ -1,28 +1,29 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:syncrow_web/pages/common/buttons/cancel_button.dart';
|
||||
import 'package:syncrow_web/pages/common/buttons/default_button.dart';
|
||||
import 'package:syncrow_web/pages/space_management_v2/modules/create_community/domain/param/create_community_param.dart';
|
||||
import 'package:syncrow_web/pages/space_management_v2/modules/create_community/presentation/bloc/create_community_bloc.dart';
|
||||
import 'package:syncrow_web/pages/space_management_v2/modules/create_community/presentation/create_community_name_text_field.dart';
|
||||
import 'package:syncrow_web/utils/color_manager.dart';
|
||||
import 'package:syncrow_web/utils/extension/build_context_x.dart';
|
||||
|
||||
class CreateCommunityDialogWidget extends StatefulWidget {
|
||||
class CommunityDialog extends StatefulWidget {
|
||||
final String? initialName;
|
||||
final Widget title;
|
||||
final void Function(String name) onSubmit;
|
||||
final String? errorMessage;
|
||||
|
||||
const CreateCommunityDialogWidget({
|
||||
super.key,
|
||||
const CommunityDialog({
|
||||
required this.title,
|
||||
required this.onSubmit,
|
||||
this.initialName,
|
||||
this.errorMessage,
|
||||
super.key,
|
||||
});
|
||||
|
||||
@override
|
||||
State<CreateCommunityDialogWidget> createState() =>
|
||||
_CreateCommunityDialogWidgetState();
|
||||
State<CommunityDialog> createState() => _CommunityDialogState();
|
||||
}
|
||||
|
||||
class _CreateCommunityDialogWidgetState extends State<CreateCommunityDialogWidget> {
|
||||
class _CommunityDialogState extends State<CommunityDialog> {
|
||||
late final TextEditingController _nameController;
|
||||
|
||||
@override
|
||||
@ -63,35 +64,20 @@ class _CreateCommunityDialogWidgetState extends State<CreateCommunityDialogWidge
|
||||
child: Form(
|
||||
key: _formKey,
|
||||
child: SingleChildScrollView(
|
||||
child: BlocBuilder<CreateCommunityBloc, CreateCommunityState>(
|
||||
builder: (context, state) {
|
||||
return Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
DefaultTextStyle(
|
||||
style: Theme.of(context).textTheme.headlineMedium!,
|
||||
child: widget.title,
|
||||
),
|
||||
const SizedBox(height: 18),
|
||||
CreateCommunityNameTextField(
|
||||
nameController: _nameController,
|
||||
),
|
||||
if (state case CreateCommunityFailure(:final message))
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(top: 18),
|
||||
child: SelectableText(
|
||||
'* $message',
|
||||
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
|
||||
color: Theme.of(context).colorScheme.error,
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
_buildActionButtons(context),
|
||||
],
|
||||
);
|
||||
},
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
DefaultTextStyle(
|
||||
style: Theme.of(context).textTheme.headlineMedium!,
|
||||
child: widget.title,
|
||||
),
|
||||
const SizedBox(height: 18),
|
||||
CreateCommunityNameTextField(nameController: _nameController),
|
||||
_buildErrorMessage(),
|
||||
const SizedBox(height: 24),
|
||||
_buildActionButtons(context),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
@ -132,13 +118,22 @@ class _CreateCommunityDialogWidgetState extends State<CreateCommunityDialogWidge
|
||||
|
||||
void _onSubmit(BuildContext context) {
|
||||
if (_formKey.currentState?.validate() ?? false) {
|
||||
context.read<CreateCommunityBloc>().add(
|
||||
CreateCommunity(
|
||||
CreateCommunityParam(
|
||||
name: _nameController.text.trim(),
|
||||
),
|
||||
),
|
||||
);
|
||||
widget.onSubmit.call(_nameController.text.trim());
|
||||
}
|
||||
}
|
||||
|
||||
Widget _buildErrorMessage() {
|
||||
return Visibility(
|
||||
visible: widget.errorMessage != null,
|
||||
child: Padding(
|
||||
padding: const EdgeInsetsDirectional.symmetric(vertical: 18),
|
||||
child: SelectableText(
|
||||
'* ${widget.errorMessage}',
|
||||
style: context.textTheme.bodyMedium?.copyWith(
|
||||
color: context.theme.colorScheme.error,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
@ -7,6 +7,11 @@ import 'package:syncrow_web/pages/space_management_v2/modules/communities/data/s
|
||||
import 'package:syncrow_web/pages/space_management_v2/modules/communities/domain/params/load_communities_param.dart';
|
||||
import 'package:syncrow_web/pages/space_management_v2/modules/communities/presentation/bloc/communities_bloc.dart';
|
||||
import 'package:syncrow_web/pages/space_management_v2/modules/communities/presentation/communities_tree_selection_bloc/communities_tree_selection_bloc.dart';
|
||||
import 'package:syncrow_web/pages/space_management_v2/modules/products/data/services/remote_products_service.dart';
|
||||
import 'package:syncrow_web/pages/space_management_v2/modules/products/presentation/bloc/products_bloc.dart';
|
||||
import 'package:syncrow_web/pages/space_management_v2/modules/space_details/data/services/remote_space_details_service.dart';
|
||||
import 'package:syncrow_web/pages/space_management_v2/modules/space_details/data/services/unique_subspaces_decorator.dart';
|
||||
import 'package:syncrow_web/pages/space_management_v2/modules/space_details/presentation/bloc/space_details_bloc.dart';
|
||||
import 'package:syncrow_web/services/api/http_service.dart';
|
||||
import 'package:syncrow_web/utils/theme/responsive_text_theme.dart';
|
||||
import 'package:syncrow_web/web_layout/web_scaffold.dart';
|
||||
@ -26,6 +31,18 @@ class SpaceManagementPage extends StatelessWidget {
|
||||
)..add(const LoadCommunities(LoadCommunitiesParam())),
|
||||
),
|
||||
BlocProvider(create: (context) => CommunitiesTreeSelectionBloc()),
|
||||
BlocProvider(
|
||||
create: (context) => SpaceDetailsBloc(
|
||||
UniqueSubspacesDecorator(
|
||||
RemoteSpaceDetailsService(httpService: HTTPService()),
|
||||
),
|
||||
),
|
||||
),
|
||||
BlocProvider(
|
||||
create: (context) => ProductsBloc(
|
||||
RemoteProductsService(HTTPService()),
|
||||
),
|
||||
),
|
||||
],
|
||||
child: WebScaffold(
|
||||
appBarTitle: Text(
|
||||
|
@ -0,0 +1,109 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:flutter_svg/svg.dart';
|
||||
import 'package:syncrow_web/pages/space_management_v2/main_module/shared/helpers/space_management_community_dialog_helper.dart';
|
||||
import 'package:syncrow_web/pages/space_management_v2/main_module/widgets/community_structure_header_action_buttons.dart';
|
||||
import 'package:syncrow_web/pages/space_management_v2/modules/communities/presentation/communities_tree_selection_bloc/communities_tree_selection_bloc.dart';
|
||||
import 'package:syncrow_web/pages/space_management_v2/modules/space_details/presentation/helpers/space_details_dialog_helper.dart';
|
||||
import 'package:syncrow_web/utils/color_manager.dart';
|
||||
import 'package:syncrow_web/utils/constants/assets.dart';
|
||||
|
||||
class CommunityStructureHeader extends StatelessWidget {
|
||||
const CommunityStructureHeader({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme = Theme.of(context);
|
||||
final screenWidth = MediaQuery.of(context).size.width;
|
||||
return Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 16.0),
|
||||
decoration: BoxDecoration(
|
||||
color: ColorsManager.whiteColors,
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: ColorsManager.shadowBlackColor,
|
||||
blurRadius: 8,
|
||||
offset: const Offset(0, 4),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Expanded(
|
||||
child: _buildCommunityInfo(context, theme, screenWidth),
|
||||
),
|
||||
const SizedBox(width: 16),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildCommunityInfo(
|
||||
BuildContext context, ThemeData theme, double screenWidth) {
|
||||
final selectedCommunity =
|
||||
context.watch<CommunitiesTreeSelectionBloc>().state.selectedCommunity;
|
||||
final selectedSpace =
|
||||
context.watch<CommunitiesTreeSelectionBloc>().state.selectedSpace;
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
'Community Structure',
|
||||
style: theme.textTheme.headlineLarge
|
||||
?.copyWith(color: ColorsManager.blackColor),
|
||||
),
|
||||
if (selectedCommunity != null)
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: Row(
|
||||
children: [
|
||||
Flexible(
|
||||
child: SelectableText(
|
||||
selectedCommunity.name,
|
||||
style: theme.textTheme.bodyLarge
|
||||
?.copyWith(color: ColorsManager.blackColor),
|
||||
maxLines: 1,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 2),
|
||||
GestureDetector(
|
||||
onTap: () {
|
||||
SpaceManagementCommunityDialogHelper.showEditDialog(
|
||||
context,
|
||||
selectedCommunity,
|
||||
);
|
||||
},
|
||||
child: SvgPicture.asset(
|
||||
Assets.iconEdit,
|
||||
width: 16,
|
||||
height: 16,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
CommunityStructureHeaderActionButtons(
|
||||
onDelete: (space) {},
|
||||
onDuplicate: (space) {},
|
||||
onEdit: (space) {
|
||||
SpaceDetailsDialogHelper.showEdit(
|
||||
context,
|
||||
spaceModel: selectedSpace!,
|
||||
);
|
||||
},
|
||||
selectedSpace: selectedSpace,
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
@ -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!),
|
||||
),
|
||||
],
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
@ -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,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
@ -1,6 +1,7 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:syncrow_web/pages/space_management_v2/main_module/widgets/community_structure_canvas.dart';
|
||||
import 'package:syncrow_web/pages/space_management_v2/main_module/widgets/community_structure_header.dart';
|
||||
import 'package:syncrow_web/pages/space_management_v2/main_module/widgets/create_space_button.dart';
|
||||
import 'package:syncrow_web/pages/space_management_v2/modules/communities/presentation/communities_tree_selection_bloc/communities_tree_selection_bloc.dart';
|
||||
|
||||
@ -18,9 +19,17 @@ class SpaceManagementCommunityStructure extends StatelessWidget {
|
||||
replacement: const Row(
|
||||
children: [spacer, Expanded(child: CreateSpaceButton()), spacer],
|
||||
),
|
||||
child: CommunityStructureCanvas(
|
||||
community: selectedCommunity,
|
||||
selectedSpace: selectedSpace,
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
const CommunityStructureHeader(),
|
||||
Expanded(
|
||||
child: CommunityStructureCanvas(
|
||||
community: selectedCommunity,
|
||||
selectedSpace: selectedSpace,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
@ -39,6 +39,26 @@ class CommunityModel extends Equatable {
|
||||
.toList();
|
||||
}
|
||||
|
||||
CommunityModel copyWith({
|
||||
String? uuid,
|
||||
String? name,
|
||||
DateTime? createdAt,
|
||||
DateTime? updatedAt,
|
||||
String? description,
|
||||
String? externalId,
|
||||
List<SpaceModel>? spaces,
|
||||
}) {
|
||||
return CommunityModel(
|
||||
uuid: uuid ?? this.uuid,
|
||||
name: name ?? this.name,
|
||||
createdAt: createdAt ?? this.createdAt,
|
||||
updatedAt: updatedAt ?? this.updatedAt,
|
||||
description: description ?? this.description,
|
||||
externalId: externalId ?? this.externalId,
|
||||
spaces: spaces ?? this.spaces,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
List<Object?> get props => [uuid, name, spaces];
|
||||
}
|
||||
|
@ -19,6 +19,16 @@ class SpaceModel extends Equatable {
|
||||
required this.parent,
|
||||
});
|
||||
|
||||
factory SpaceModel.empty() => const SpaceModel(
|
||||
uuid: '',
|
||||
createdAt: null,
|
||||
updatedAt: null,
|
||||
spaceName: '',
|
||||
icon: '',
|
||||
children: [],
|
||||
parent: null,
|
||||
);
|
||||
|
||||
factory SpaceModel.fromJson(Map<String, dynamic> json) {
|
||||
return SpaceModel(
|
||||
uuid: json['uuid'] as String? ?? '',
|
||||
|
@ -16,6 +16,7 @@ class CommunitiesBloc extends Bloc<CommunitiesEvent, CommunitiesState> {
|
||||
on<LoadCommunities>(_onLoadCommunities);
|
||||
on<LoadMoreCommunities>(_onLoadMoreCommunities);
|
||||
on<InsertCommunity>(_onInsertCommunity);
|
||||
on<CommunitiesUpdateCommunity>(_onCommunitiesUpdateCommunity);
|
||||
}
|
||||
|
||||
final CommunitiesService _communitiesService;
|
||||
@ -114,4 +115,18 @@ class CommunitiesBloc extends Bloc<CommunitiesEvent, CommunitiesState> {
|
||||
) {
|
||||
emit(state.copyWith(communities: [event.community, ...state.communities]));
|
||||
}
|
||||
|
||||
void _onCommunitiesUpdateCommunity(
|
||||
CommunitiesUpdateCommunity event,
|
||||
Emitter<CommunitiesState> emit,
|
||||
) {
|
||||
final updatedCommunities = state.communities
|
||||
.map((e) => e.uuid == event.community.uuid ? event.community : e)
|
||||
.toList();
|
||||
emit(
|
||||
state.copyWith(
|
||||
communities: updatedCommunities,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -31,3 +31,12 @@ final class InsertCommunity extends CommunitiesEvent {
|
||||
@override
|
||||
List<Object?> get props => [community];
|
||||
}
|
||||
|
||||
final class CommunitiesUpdateCommunity extends CommunitiesEvent {
|
||||
const CommunitiesUpdateCommunity(this.community);
|
||||
|
||||
final CommunityModel community;
|
||||
|
||||
@override
|
||||
List<Object?> get props => [community];
|
||||
}
|
||||
|
@ -1,57 +1,58 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:syncrow_web/pages/space_management_v2/modules/communities/domain/models/community_model.dart';
|
||||
import 'package:syncrow_web/pages/space_management_v2/main_module/shared/helpers/space_management_community_dialog_helper.dart';
|
||||
import 'package:syncrow_web/pages/space_management_v2/main_module/shared/widgets/community_dialog.dart';
|
||||
import 'package:syncrow_web/pages/space_management_v2/modules/communities/presentation/bloc/communities_bloc.dart';
|
||||
import 'package:syncrow_web/pages/space_management_v2/modules/communities/presentation/communities_tree_selection_bloc/communities_tree_selection_bloc.dart';
|
||||
import 'package:syncrow_web/pages/space_management_v2/modules/create_community/data/services/remote_create_community_service.dart';
|
||||
import 'package:syncrow_web/pages/space_management_v2/modules/create_community/domain/param/create_community_param.dart';
|
||||
import 'package:syncrow_web/pages/space_management_v2/modules/create_community/presentation/bloc/create_community_bloc.dart';
|
||||
import 'package:syncrow_web/pages/space_management_v2/modules/create_community/presentation/create_community_dialog_widget.dart';
|
||||
import 'package:syncrow_web/services/api/http_service.dart';
|
||||
|
||||
class CreateCommunityDialog extends StatelessWidget {
|
||||
final void Function(CommunityModel community) onCreateCommunity;
|
||||
final String? initialName;
|
||||
final Widget title;
|
||||
|
||||
const CreateCommunityDialog({
|
||||
super.key,
|
||||
required this.onCreateCommunity,
|
||||
required this.title,
|
||||
this.initialName,
|
||||
});
|
||||
const CreateCommunityDialog({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return BlocProvider(
|
||||
create: (_) => CreateCommunityBloc(RemoteCreateCommunityService(HTTPService())),
|
||||
child: BlocListener<CreateCommunityBloc, CreateCommunityState>(
|
||||
create: (_) => CreateCommunityBloc(
|
||||
RemoteCreateCommunityService(HTTPService()),
|
||||
),
|
||||
child: BlocConsumer<CreateCommunityBloc, CreateCommunityState>(
|
||||
listener: (context, state) {
|
||||
switch (state) {
|
||||
case CreateCommunityLoading():
|
||||
showDialog<void>(
|
||||
context: context,
|
||||
builder: (context) => const Center(
|
||||
child: CircularProgressIndicator(),
|
||||
),
|
||||
);
|
||||
case CreateCommunityLoading() || CreateCommunityInitial():
|
||||
SpaceManagementCommunityDialogHelper.showLoadingDialog(context);
|
||||
break;
|
||||
case CreateCommunitySuccess(:final community):
|
||||
Navigator.of(context).pop();
|
||||
Navigator.of(context).pop();
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(content: Text('Community created successfully')),
|
||||
SpaceManagementCommunityDialogHelper.showSuccessSnackBar(
|
||||
context,
|
||||
'${community.name} community created successfully',
|
||||
);
|
||||
onCreateCommunity.call(community);
|
||||
context.read<CommunitiesBloc>().add(
|
||||
InsertCommunity(community),
|
||||
);
|
||||
context.read<CommunitiesTreeSelectionBloc>().add(
|
||||
SelectCommunityEvent(community: community),
|
||||
);
|
||||
break;
|
||||
case CreateCommunityFailure():
|
||||
Navigator.of(context).pop();
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
},
|
||||
child: CreateCommunityDialogWidget(
|
||||
title: title,
|
||||
initialName: initialName,
|
||||
),
|
||||
builder: (BuildContext context, CreateCommunityState state) {
|
||||
return CommunityDialog(
|
||||
title: const Text('Create Community'),
|
||||
initialName: null,
|
||||
onSubmit: (name) => context.read<CreateCommunityBloc>().add(
|
||||
CreateCommunity(CreateCommunityParam(name: name)),
|
||||
),
|
||||
errorMessage: state is CreateCommunityFailure ? state.message : null,
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
@ -1,9 +1,9 @@
|
||||
import 'package:dio/dio.dart';
|
||||
import 'package:syncrow_web/pages/space_management_v2/modules/products/domain/models/product.dart';
|
||||
import 'package:syncrow_web/pages/space_management_v2/modules/products/domain/params/load_products_param.dart';
|
||||
import 'package:syncrow_web/pages/space_management_v2/modules/products/domain/services/products_service.dart';
|
||||
import 'package:syncrow_web/services/api/api_exception.dart';
|
||||
import 'package:syncrow_web/services/api/http_service.dart';
|
||||
import 'package:syncrow_web/utils/constants/api_const.dart';
|
||||
|
||||
class RemoteProductsService implements ProductsService {
|
||||
const RemoteProductsService(this._httpService);
|
||||
@ -13,17 +13,14 @@ class RemoteProductsService implements ProductsService {
|
||||
static const _defaultErrorMessage = 'Failed to load devices';
|
||||
|
||||
@override
|
||||
Future<List<Product>> getProducts(LoadProductsParam param) async {
|
||||
Future<List<Product>> getProducts() async {
|
||||
try {
|
||||
final response = await _httpService.get(
|
||||
path: 'devices',
|
||||
queryParameters: {
|
||||
'spaceUuid': param.spaceUuid,
|
||||
if (param.type != null) 'type': param.type,
|
||||
if (param.status != null) 'status': param.status,
|
||||
},
|
||||
path: ApiEndpoints.listProducts,
|
||||
expectedResponseModel: (data) {
|
||||
return (data as List)
|
||||
final json = data as Map<String, dynamic>;
|
||||
final products = json['data'] as List<dynamic>;
|
||||
return products
|
||||
.map((e) => Product.fromJson(e as Map<String, dynamic>))
|
||||
.toList();
|
||||
},
|
||||
|
@ -1,18 +1,24 @@
|
||||
import 'package:equatable/equatable.dart';
|
||||
import 'package:syncrow_web/utils/constants/assets.dart';
|
||||
|
||||
class Product extends Equatable {
|
||||
final String uuid;
|
||||
final String name;
|
||||
|
||||
const Product({
|
||||
required this.uuid,
|
||||
required this.name,
|
||||
required this.productType,
|
||||
});
|
||||
|
||||
final String uuid;
|
||||
final String name;
|
||||
final String productType;
|
||||
|
||||
String get icon => _mapIconToProduct(productType);
|
||||
|
||||
factory Product.fromJson(Map<String, dynamic> json) {
|
||||
return Product(
|
||||
uuid: json['uuid'] as String,
|
||||
name: json['name'] as String,
|
||||
uuid: json['uuid'] as String? ?? '',
|
||||
name: json['name'] as String? ?? '',
|
||||
productType: json['prodType'] as String? ?? '',
|
||||
);
|
||||
}
|
||||
|
||||
@ -20,9 +26,37 @@ class Product extends Equatable {
|
||||
return {
|
||||
'uuid': uuid,
|
||||
'name': name,
|
||||
'productType': productType,
|
||||
};
|
||||
}
|
||||
|
||||
static String _mapIconToProduct(String prodType) {
|
||||
const iconMapping = {
|
||||
'1G': Assets.Gang1SwitchIcon,
|
||||
'1GT': Assets.oneTouchSwitch,
|
||||
'2G': Assets.Gang2SwitchIcon,
|
||||
'2GT': Assets.twoTouchSwitch,
|
||||
'3G': Assets.Gang3SwitchIcon,
|
||||
'3GT': Assets.threeTouchSwitch,
|
||||
'CUR': Assets.curtain,
|
||||
'CUR_2': Assets.curtain,
|
||||
'GD': Assets.garageDoor,
|
||||
'GW': Assets.SmartGatewayIcon,
|
||||
'DL': Assets.DoorLockIcon,
|
||||
'WL': Assets.waterLeakSensor,
|
||||
'WH': Assets.waterHeater,
|
||||
'WM': Assets.waterLeakSensor,
|
||||
'SOS': Assets.sos,
|
||||
'AC': Assets.ac,
|
||||
'CPS': Assets.presenceSensor,
|
||||
'PC': Assets.powerClamp,
|
||||
'WPS': Assets.presenceSensor,
|
||||
'DS': Assets.doorSensor
|
||||
};
|
||||
|
||||
return iconMapping[prodType] ?? Assets.presenceSensor;
|
||||
}
|
||||
|
||||
@override
|
||||
List<Object?> get props => [uuid, name];
|
||||
List<Object?> get props => [uuid, name, productType];
|
||||
}
|
||||
|
@ -1,11 +0,0 @@
|
||||
class LoadProductsParam {
|
||||
final String spaceUuid;
|
||||
final String? type;
|
||||
final String? status;
|
||||
|
||||
const LoadProductsParam({
|
||||
required this.spaceUuid,
|
||||
this.type,
|
||||
this.status,
|
||||
});
|
||||
}
|
@ -1,6 +1,5 @@
|
||||
import 'package:syncrow_web/pages/space_management_v2/modules/products/domain/models/product.dart';
|
||||
import 'package:syncrow_web/pages/space_management_v2/modules/products/domain/params/load_products_param.dart';
|
||||
|
||||
abstract class ProductsService {
|
||||
Future<List<Product>> getProducts(LoadProductsParam param);
|
||||
Future<List<Product>> getProducts();
|
||||
}
|
||||
|
@ -1,7 +1,6 @@
|
||||
import 'package:bloc/bloc.dart';
|
||||
import 'package:equatable/equatable.dart';
|
||||
import 'package:syncrow_web/pages/space_management_v2/modules/products/domain/models/product.dart';
|
||||
import 'package:syncrow_web/pages/space_management_v2/modules/products/domain/params/load_products_param.dart';
|
||||
import 'package:syncrow_web/pages/space_management_v2/modules/products/domain/services/products_service.dart';
|
||||
import 'package:syncrow_web/services/api/api_exception.dart';
|
||||
|
||||
@ -9,20 +8,20 @@ part 'products_event.dart';
|
||||
part 'products_state.dart';
|
||||
|
||||
class ProductsBloc extends Bloc<ProductsEvent, ProductsState> {
|
||||
final ProductsService _deviceService;
|
||||
|
||||
ProductsBloc(this._deviceService) : super(ProductsInitial()) {
|
||||
ProductsBloc(this._productsService) : super(ProductsInitial()) {
|
||||
on<LoadProducts>(_onLoadProducts);
|
||||
}
|
||||
|
||||
final ProductsService _productsService;
|
||||
|
||||
Future<void> _onLoadProducts(
|
||||
LoadProducts event,
|
||||
Emitter<ProductsState> emit,
|
||||
) async {
|
||||
emit(ProductsLoading());
|
||||
try {
|
||||
final devices = await _deviceService.getProducts(event.param);
|
||||
emit(ProductsLoaded(devices));
|
||||
final products = await _productsService.getProducts();
|
||||
emit(ProductsLoaded(products));
|
||||
} on APIException catch (e) {
|
||||
emit(ProductsFailure(e.message));
|
||||
} catch (e) {
|
||||
|
@ -8,10 +8,5 @@ sealed class ProductsEvent extends Equatable {
|
||||
}
|
||||
|
||||
final class LoadProducts extends ProductsEvent {
|
||||
const LoadProducts(this.param);
|
||||
|
||||
final LoadProductsParam param;
|
||||
|
||||
@override
|
||||
List<Object> get props => [param];
|
||||
const LoadProducts();
|
||||
}
|
||||
|
@ -21,10 +21,10 @@ final class ProductsLoaded extends ProductsState {
|
||||
}
|
||||
|
||||
final class ProductsFailure extends ProductsState {
|
||||
final String message;
|
||||
final String errorMessage;
|
||||
|
||||
const ProductsFailure(this.message);
|
||||
const ProductsFailure(this.errorMessage);
|
||||
|
||||
@override
|
||||
List<Object> get props => [message];
|
||||
List<Object> get props => [errorMessage];
|
||||
}
|
||||
|
@ -1,6 +1,7 @@
|
||||
import 'package:dio/dio.dart';
|
||||
import 'package:syncrow_web/pages/common/bloc/project_manager.dart';
|
||||
import 'package:syncrow_web/pages/space_management_v2/modules/space_details/domain/models/space_details_model.dart';
|
||||
import 'package:syncrow_web/pages/space_management_v2/modules/space_details/domain/params/load_spaces_param.dart';
|
||||
import 'package:syncrow_web/pages/space_management_v2/modules/space_details/domain/params/load_space_details_param.dart';
|
||||
import 'package:syncrow_web/pages/space_management_v2/modules/space_details/domain/services/space_details_service.dart';
|
||||
import 'package:syncrow_web/services/api/api_exception.dart';
|
||||
import 'package:syncrow_web/services/api/http_service.dart';
|
||||
@ -15,12 +16,15 @@ class RemoteSpaceDetailsService implements SpaceDetailsService {
|
||||
static const _defaultErrorMessage = 'Failed to load space details';
|
||||
|
||||
@override
|
||||
Future<SpaceDetailsModel> getSpaceDetails(LoadSpacesParam param) async {
|
||||
Future<SpaceDetailsModel> getSpaceDetails(LoadSpaceDetailsParam param) async {
|
||||
try {
|
||||
final response = await _httpService.get(
|
||||
path: 'endpoint',
|
||||
path: await _makeEndpoint(param),
|
||||
expectedResponseModel: (data) {
|
||||
return SpaceDetailsModel.fromJson(data as Map<String, dynamic>);
|
||||
final response = data as Map<String, dynamic>;
|
||||
return SpaceDetailsModel.fromJson(
|
||||
response['data'] as Map<String, dynamic>,
|
||||
);
|
||||
},
|
||||
);
|
||||
return response;
|
||||
@ -37,4 +41,13 @@ class RemoteSpaceDetailsService implements SpaceDetailsService {
|
||||
throw APIException(formattedErrorMessage);
|
||||
}
|
||||
}
|
||||
|
||||
Future<String> _makeEndpoint(LoadSpaceDetailsParam param) async {
|
||||
final projectUuid = await ProjectManager.getProjectUUID();
|
||||
if (projectUuid == null || projectUuid.isEmpty) {
|
||||
throw APIException('Project UUID is not set');
|
||||
}
|
||||
|
||||
return '/projects/$projectUuid/communities/${param.communityUuid}/spaces/${param.spaceUuid}';
|
||||
}
|
||||
}
|
||||
|
@ -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(),
|
||||
);
|
||||
}
|
||||
}
|
@ -1,6 +1,8 @@
|
||||
import 'package:equatable/equatable.dart';
|
||||
import 'package:syncrow_web/pages/space_management_v2/modules/products/domain/models/product.dart';
|
||||
import 'package:syncrow_web/pages/space_management_v2/modules/tags/domain/models/tag.dart';
|
||||
import 'package:syncrow_web/utils/constants/assets.dart';
|
||||
import 'package:uuid/uuid.dart';
|
||||
|
||||
class SpaceDetailsModel extends Equatable {
|
||||
final String uuid;
|
||||
@ -17,6 +19,13 @@ class SpaceDetailsModel extends Equatable {
|
||||
required this.subspaces,
|
||||
});
|
||||
|
||||
factory SpaceDetailsModel.empty() => const SpaceDetailsModel(
|
||||
uuid: '',
|
||||
spaceName: '',
|
||||
icon: Assets.location,
|
||||
productAllocations: [],
|
||||
subspaces: [],
|
||||
);
|
||||
factory SpaceDetailsModel.fromJson(Map<String, dynamic> json) {
|
||||
return SpaceDetailsModel(
|
||||
uuid: json['uuid'] as String,
|
||||
@ -41,23 +50,40 @@ class SpaceDetailsModel extends Equatable {
|
||||
};
|
||||
}
|
||||
|
||||
SpaceDetailsModel copyWith({
|
||||
String? uuid,
|
||||
String? spaceName,
|
||||
String? icon,
|
||||
List<ProductAllocation>? productAllocations,
|
||||
List<Subspace>? subspaces,
|
||||
}) {
|
||||
return SpaceDetailsModel(
|
||||
uuid: uuid ?? this.uuid,
|
||||
spaceName: spaceName ?? this.spaceName,
|
||||
icon: icon ?? this.icon,
|
||||
productAllocations: productAllocations ?? this.productAllocations,
|
||||
subspaces: subspaces ?? this.subspaces,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
List<Object?> get props => [uuid, spaceName, icon, productAllocations, subspaces];
|
||||
}
|
||||
|
||||
class ProductAllocation extends Equatable {
|
||||
final String uuid;
|
||||
final Product product;
|
||||
final Tag tag;
|
||||
final String? location;
|
||||
|
||||
const ProductAllocation({
|
||||
required this.uuid,
|
||||
required this.product,
|
||||
required this.tag,
|
||||
this.location,
|
||||
});
|
||||
|
||||
factory ProductAllocation.fromJson(Map<String, dynamic> json) {
|
||||
return ProductAllocation(
|
||||
uuid: json['uuid'] as String? ?? const Uuid().v4(),
|
||||
product: Product.fromJson(json['product'] as Map<String, dynamic>),
|
||||
tag: Tag.fromJson(json['tag'] as Map<String, dynamic>),
|
||||
);
|
||||
@ -65,13 +91,26 @@ class ProductAllocation extends Equatable {
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
return {
|
||||
'uuid': uuid,
|
||||
'product': product.toJson(),
|
||||
'tag': tag.toJson(),
|
||||
};
|
||||
}
|
||||
|
||||
ProductAllocation copyWith({
|
||||
String? uuid,
|
||||
Product? product,
|
||||
Tag? tag,
|
||||
}) {
|
||||
return ProductAllocation(
|
||||
uuid: uuid ?? this.uuid,
|
||||
product: product ?? this.product,
|
||||
tag: tag ?? this.tag,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
List<Object?> get props => [product, tag];
|
||||
List<Object?> get props => [uuid, product, tag];
|
||||
}
|
||||
|
||||
class Subspace extends Equatable {
|
||||
@ -88,7 +127,7 @@ class Subspace extends Equatable {
|
||||
factory Subspace.fromJson(Map<String, dynamic> json) {
|
||||
return Subspace(
|
||||
uuid: json['uuid'] as String,
|
||||
name: json['name'] as String,
|
||||
name: json['subspaceName'] as String,
|
||||
productAllocations: (json['productAllocations'] as List)
|
||||
.map((e) => ProductAllocation.fromJson(e as Map<String, dynamic>))
|
||||
.toList(),
|
||||
@ -103,6 +142,18 @@ class Subspace extends Equatable {
|
||||
};
|
||||
}
|
||||
|
||||
Subspace copyWith({
|
||||
String? uuid,
|
||||
String? name,
|
||||
List<ProductAllocation>? productAllocations,
|
||||
}) {
|
||||
return Subspace(
|
||||
uuid: uuid ?? this.uuid,
|
||||
name: name ?? this.name,
|
||||
productAllocations: productAllocations ?? this.productAllocations,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
List<Object?> get props => [uuid, name, productAllocations];
|
||||
}
|
||||
|
@ -0,0 +1,9 @@
|
||||
class LoadSpaceDetailsParam {
|
||||
const LoadSpaceDetailsParam({
|
||||
required this.spaceUuid,
|
||||
required this.communityUuid,
|
||||
});
|
||||
|
||||
final String spaceUuid;
|
||||
final String communityUuid;
|
||||
}
|
@ -1,3 +0,0 @@
|
||||
class LoadSpacesParam {
|
||||
const LoadSpacesParam();
|
||||
}
|
@ -1,6 +1,6 @@
|
||||
import 'package:syncrow_web/pages/space_management_v2/modules/space_details/domain/models/space_details_model.dart';
|
||||
import 'package:syncrow_web/pages/space_management_v2/modules/space_details/domain/params/load_spaces_param.dart';
|
||||
import 'package:syncrow_web/pages/space_management_v2/modules/space_details/domain/params/load_space_details_param.dart';
|
||||
|
||||
abstract class SpaceDetailsService {
|
||||
Future<SpaceDetailsModel> getSpaceDetails(LoadSpacesParam param);
|
||||
Future<SpaceDetailsModel> getSpaceDetails(LoadSpaceDetailsParam param);
|
||||
}
|
||||
|
@ -1,7 +1,7 @@
|
||||
import 'package:bloc/bloc.dart';
|
||||
import 'package:equatable/equatable.dart';
|
||||
import 'package:syncrow_web/pages/space_management_v2/modules/space_details/domain/models/space_details_model.dart';
|
||||
import 'package:syncrow_web/pages/space_management_v2/modules/space_details/domain/params/load_spaces_param.dart';
|
||||
import 'package:syncrow_web/pages/space_management_v2/modules/space_details/domain/params/load_space_details_param.dart';
|
||||
import 'package:syncrow_web/pages/space_management_v2/modules/space_details/domain/services/space_details_service.dart';
|
||||
import 'package:syncrow_web/services/api/api_exception.dart';
|
||||
|
||||
@ -9,12 +9,13 @@ part 'space_details_event.dart';
|
||||
part 'space_details_state.dart';
|
||||
|
||||
class SpaceDetailsBloc extends Bloc<SpaceDetailsEvent, SpaceDetailsState> {
|
||||
final SpaceDetailsService _spaceDetailsService;
|
||||
|
||||
SpaceDetailsBloc(this._spaceDetailsService) : super(SpaceDetailsInitial()) {
|
||||
on<LoadSpaceDetails>(_onLoadSpaceDetails);
|
||||
on<ClearSpaceDetails>(_onClearSpaceDetails);
|
||||
}
|
||||
|
||||
final SpaceDetailsService _spaceDetailsService;
|
||||
|
||||
Future<void> _onLoadSpaceDetails(
|
||||
LoadSpaceDetails event,
|
||||
Emitter<SpaceDetailsState> emit,
|
||||
@ -31,4 +32,11 @@ class SpaceDetailsBloc extends Bloc<SpaceDetailsEvent, SpaceDetailsState> {
|
||||
emit(SpaceDetailsFailure(e.toString()));
|
||||
}
|
||||
}
|
||||
|
||||
void _onClearSpaceDetails(
|
||||
ClearSpaceDetails event,
|
||||
Emitter<SpaceDetailsState> emit,
|
||||
) {
|
||||
emit(SpaceDetailsInitial());
|
||||
}
|
||||
}
|
||||
|
@ -7,11 +7,18 @@ sealed class SpaceDetailsEvent extends Equatable {
|
||||
List<Object> get props => [];
|
||||
}
|
||||
|
||||
class LoadSpaceDetails extends SpaceDetailsEvent {
|
||||
final class LoadSpaceDetails extends SpaceDetailsEvent {
|
||||
const LoadSpaceDetails(this.param);
|
||||
|
||||
final LoadSpacesParam param;
|
||||
final LoadSpaceDetailsParam param;
|
||||
|
||||
@override
|
||||
List<Object> get props => [param];
|
||||
}
|
||||
|
||||
final class ClearSpaceDetails extends SpaceDetailsEvent {
|
||||
const ClearSpaceDetails();
|
||||
|
||||
@override
|
||||
List<Object> get props => [];
|
||||
}
|
@ -21,10 +21,10 @@ final class SpaceDetailsLoaded extends SpaceDetailsState {
|
||||
}
|
||||
|
||||
final class SpaceDetailsFailure extends SpaceDetailsState {
|
||||
final String message;
|
||||
final String errorMessage;
|
||||
|
||||
const SpaceDetailsFailure(this.message);
|
||||
const SpaceDetailsFailure(this.errorMessage);
|
||||
|
||||
@override
|
||||
List<Object> get props => [message];
|
||||
List<Object> get props => [errorMessage];
|
||||
}
|
||||
|
@ -1,11 +1,46 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:syncrow_web/pages/space_management_v2/modules/communities/domain/models/space_model.dart';
|
||||
import 'package:syncrow_web/pages/space_management_v2/modules/space_details/data/services/remote_space_details_service.dart';
|
||||
import 'package:syncrow_web/pages/space_management_v2/modules/space_details/presentation/bloc/space_details_bloc.dart';
|
||||
import 'package:syncrow_web/pages/space_management_v2/modules/space_details/presentation/widgets/space_details_dialog.dart';
|
||||
import 'package:syncrow_web/services/api/http_service.dart';
|
||||
|
||||
abstract final class SpaceDetailsDialogHelper {
|
||||
static void showCreate(BuildContext context) {
|
||||
showDialog<void>(
|
||||
context: context,
|
||||
builder: (context) => const SpaceDetailsDialog(),
|
||||
builder: (_) => BlocProvider(
|
||||
create: (context) => SpaceDetailsBloc(
|
||||
RemoteSpaceDetailsService(httpService: HTTPService()),
|
||||
),
|
||||
child: SpaceDetailsDialog(
|
||||
context: context,
|
||||
title: const SelectableText('Create Space'),
|
||||
spaceModel: SpaceModel.empty(),
|
||||
onSave: (space) {},
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
static void showEdit(
|
||||
BuildContext context, {
|
||||
required SpaceModel spaceModel,
|
||||
}) {
|
||||
showDialog<void>(
|
||||
context: context,
|
||||
builder: (_) => BlocProvider(
|
||||
create: (context) => SpaceDetailsBloc(
|
||||
RemoteSpaceDetailsService(httpService: HTTPService()),
|
||||
),
|
||||
child: SpaceDetailsDialog(
|
||||
context: context,
|
||||
title: const SelectableText('Edit Space'),
|
||||
spaceModel: spaceModel,
|
||||
onSave: (space) {},
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,60 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_svg/svg.dart';
|
||||
import 'package:syncrow_web/utils/color_manager.dart';
|
||||
|
||||
class ButtonContentWidget extends StatelessWidget {
|
||||
final String label;
|
||||
final String? svgAssets;
|
||||
final bool disabled;
|
||||
|
||||
const ButtonContentWidget({
|
||||
required this.label,
|
||||
this.svgAssets,
|
||||
this.disabled = false,
|
||||
super.key,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final screenWidth = MediaQuery.of(context).size.width;
|
||||
|
||||
return Opacity(
|
||||
opacity: disabled ? 0.5 : 1.0,
|
||||
child: Container(
|
||||
width: screenWidth * 0.25,
|
||||
decoration: BoxDecoration(
|
||||
color: ColorsManager.textFieldGreyColor,
|
||||
border: Border.all(
|
||||
color: ColorsManager.neutralGray,
|
||||
width: 3.0,
|
||||
),
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
),
|
||||
padding: const EdgeInsets.symmetric(vertical: 10.0, horizontal: 16.0),
|
||||
child: Row(
|
||||
children: [
|
||||
if (svgAssets != null)
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(left: 6.0),
|
||||
child: SvgPicture.asset(
|
||||
svgAssets!,
|
||||
width: screenWidth * 0.015,
|
||||
height: screenWidth * 0.015,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 10),
|
||||
Expanded(
|
||||
child: Text(
|
||||
label,
|
||||
style: const TextStyle(
|
||||
color: ColorsManager.blackColor,
|
||||
fontSize: 16,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
@ -0,0 +1,46 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:syncrow_web/pages/common/buttons/cancel_button.dart';
|
||||
import 'package:syncrow_web/pages/common/buttons/default_button.dart';
|
||||
import 'package:syncrow_web/utils/color_manager.dart';
|
||||
|
||||
class SpaceDetailsActionButtons extends StatelessWidget {
|
||||
const SpaceDetailsActionButtons({
|
||||
super.key,
|
||||
required this.onSave,
|
||||
required this.onCancel,
|
||||
this.saveButtonLabel = 'OK',
|
||||
this.cancelButtonLabel = 'Cancel',
|
||||
});
|
||||
|
||||
final VoidCallback onCancel;
|
||||
final VoidCallback? onSave;
|
||||
final String saveButtonLabel;
|
||||
final String cancelButtonLabel;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
spacing: 10,
|
||||
children: [
|
||||
Expanded(child: _buildCancelButton(context)),
|
||||
Expanded(child: _buildSaveButton()),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildCancelButton(BuildContext context) {
|
||||
return CancelButton(onPressed: onCancel, label: cancelButtonLabel);
|
||||
}
|
||||
|
||||
Widget _buildSaveButton() {
|
||||
return DefaultButton(
|
||||
onPressed: onSave,
|
||||
borderRadius: 10,
|
||||
backgroundColor: ColorsManager.secondaryColor,
|
||||
foregroundColor: ColorsManager.whiteColors,
|
||||
child: Text(saveButtonLabel),
|
||||
);
|
||||
}
|
||||
}
|
@ -0,0 +1,139 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:flutter_svg/svg.dart';
|
||||
import 'package:syncrow_web/common/edit_chip.dart';
|
||||
import 'package:syncrow_web/pages/space_management_v2/modules/space_details/domain/models/space_details_model.dart';
|
||||
import 'package:syncrow_web/pages/space_management_v2/modules/space_details/presentation/widgets/button_content_widget.dart';
|
||||
import 'package:syncrow_web/pages/space_management_v2/modules/tags/presentation/widgets/assign_tags_dialog.dart';
|
||||
import 'package:syncrow_web/pages/space_management_v2/modules/update_space/presentation/bloc/space_details_model_bloc/space_details_model_bloc.dart';
|
||||
import 'package:syncrow_web/utils/color_manager.dart';
|
||||
import 'package:syncrow_web/utils/constants/assets.dart';
|
||||
import 'package:syncrow_web/utils/enum/device_types.dart';
|
||||
import 'package:syncrow_web/utils/extension/build_context_x.dart';
|
||||
|
||||
class SpaceDetailsDevicesBox extends StatelessWidget {
|
||||
const SpaceDetailsDevicesBox({
|
||||
required this.space,
|
||||
super.key,
|
||||
});
|
||||
|
||||
final SpaceDetailsModel space;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final allAllocations = [
|
||||
...space.productAllocations,
|
||||
...space.subspaces.expand((s) => s.productAllocations),
|
||||
];
|
||||
|
||||
if (allAllocations.isNotEmpty) {
|
||||
final productCounts = <String, int>{};
|
||||
for (final allocation in allAllocations) {
|
||||
final productType = allocation.product.productType;
|
||||
productCounts[productType] = (productCounts[productType] ?? 0) + 1;
|
||||
}
|
||||
|
||||
return Container(
|
||||
width: double.infinity,
|
||||
padding: const EdgeInsets.all(8),
|
||||
decoration: BoxDecoration(
|
||||
color: ColorsManager.textFieldGreyColor,
|
||||
borderRadius: BorderRadius.circular(15),
|
||||
border: Border.all(
|
||||
color: ColorsManager.textFieldGreyColor,
|
||||
width: 3.0,
|
||||
),
|
||||
),
|
||||
child: Wrap(
|
||||
spacing: 8.0,
|
||||
runSpacing: 8.0,
|
||||
children: [
|
||||
...productCounts.entries.map((entry) {
|
||||
final productType = entry.key;
|
||||
final count = entry.value;
|
||||
return Chip(
|
||||
avatar: SizedBox(
|
||||
width: 24,
|
||||
height: 24,
|
||||
child: SvgPicture.asset(
|
||||
_getDeviceIcon(productType),
|
||||
fit: BoxFit.contain,
|
||||
),
|
||||
),
|
||||
label: Text(
|
||||
'x$count',
|
||||
style: context.textTheme.bodySmall?.copyWith(
|
||||
color: ColorsManager.spaceColor,
|
||||
),
|
||||
),
|
||||
backgroundColor: ColorsManager.whiteColors,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
side: const BorderSide(
|
||||
color: ColorsManager.spaceColor,
|
||||
),
|
||||
),
|
||||
);
|
||||
}),
|
||||
EditChip(onTap: () => _showAssignTagsDialog(context)),
|
||||
],
|
||||
),
|
||||
);
|
||||
} else {
|
||||
return TextButton(
|
||||
onPressed: () => _showAssignTagsDialog(context),
|
||||
style: TextButton.styleFrom(
|
||||
padding: EdgeInsets.zero,
|
||||
),
|
||||
child: const SizedBox(
|
||||
width: double.infinity,
|
||||
child: ButtonContentWidget(
|
||||
svgAssets: Assets.addIcon,
|
||||
label: 'Add Devices',
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
void _showAssignTagsDialog(BuildContext context) {
|
||||
showDialog<SpaceDetailsModel>(
|
||||
context: context,
|
||||
builder: (context) => AssignTagsDialog(space: space),
|
||||
).then((resultSpace) {
|
||||
if (resultSpace != null) {
|
||||
if (context.mounted) {
|
||||
context.read<SpaceDetailsModelBloc>().add(UpdateSpaceDetails(resultSpace));
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
String _getDeviceIcon(String productType) =>
|
||||
switch (devicesTypesMap[productType]) {
|
||||
DeviceType.LightBulb => Assets.lightBulb,
|
||||
DeviceType.CeilingSensor => Assets.sensors,
|
||||
DeviceType.AC => Assets.ac,
|
||||
DeviceType.DoorLock => Assets.doorLock,
|
||||
DeviceType.Curtain => Assets.curtain,
|
||||
DeviceType.ThreeGang => Assets.gangSwitch,
|
||||
DeviceType.Gateway => Assets.gateway,
|
||||
DeviceType.OneGang => Assets.oneGang,
|
||||
DeviceType.TwoGang => Assets.twoGang,
|
||||
DeviceType.WH => Assets.waterHeater,
|
||||
DeviceType.DoorSensor => Assets.openCloseDoor,
|
||||
DeviceType.GarageDoor => Assets.openedDoor,
|
||||
DeviceType.WaterLeak => Assets.waterLeakNormal,
|
||||
DeviceType.Curtain2 => Assets.curtainIcon,
|
||||
DeviceType.Blind => Assets.curtainIcon,
|
||||
DeviceType.WallSensor => Assets.sensors,
|
||||
DeviceType.DS => Assets.openCloseDoor,
|
||||
DeviceType.OneTouch => Assets.gangSwitch,
|
||||
DeviceType.TowTouch => Assets.gangSwitch,
|
||||
DeviceType.ThreeTouch => Assets.gangSwitch,
|
||||
DeviceType.NCPS => Assets.sensors,
|
||||
DeviceType.PC => Assets.powerClamp,
|
||||
DeviceType.Other => Assets.blackLogo,
|
||||
null => Assets.blackLogo,
|
||||
};
|
||||
}
|
@ -1,12 +1,105 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:syncrow_web/pages/space_management_v2/modules/communities/domain/models/space_model.dart';
|
||||
import 'package:syncrow_web/pages/space_management_v2/modules/communities/presentation/communities_tree_selection_bloc/communities_tree_selection_bloc.dart';
|
||||
import 'package:syncrow_web/pages/space_management_v2/modules/space_details/domain/models/space_details_model.dart';
|
||||
import 'package:syncrow_web/pages/space_management_v2/modules/space_details/domain/params/load_space_details_param.dart';
|
||||
import 'package:syncrow_web/pages/space_management_v2/modules/space_details/presentation/bloc/space_details_bloc.dart';
|
||||
import 'package:syncrow_web/pages/space_management_v2/modules/space_details/presentation/widgets/space_details_form.dart';
|
||||
import 'package:syncrow_web/utils/color_manager.dart';
|
||||
import 'package:syncrow_web/utils/extension/build_context_x.dart';
|
||||
|
||||
class SpaceDetailsDialog extends StatelessWidget {
|
||||
const SpaceDetailsDialog({super.key});
|
||||
class SpaceDetailsDialog extends StatefulWidget {
|
||||
const SpaceDetailsDialog({
|
||||
required this.title,
|
||||
required this.spaceModel,
|
||||
required this.onSave,
|
||||
required this.context,
|
||||
super.key,
|
||||
});
|
||||
|
||||
final Widget title;
|
||||
final SpaceModel spaceModel;
|
||||
final void Function(SpaceDetailsModel space) onSave;
|
||||
final BuildContext context;
|
||||
|
||||
@override
|
||||
State<SpaceDetailsDialog> createState() => _SpaceDetailsDialogState();
|
||||
}
|
||||
|
||||
class _SpaceDetailsDialogState extends State<SpaceDetailsDialog> {
|
||||
@override
|
||||
void initState() {
|
||||
final isCreateMode = widget.spaceModel.uuid.isEmpty;
|
||||
|
||||
if (!isCreateMode) {
|
||||
final param = LoadSpaceDetailsParam(
|
||||
spaceUuid: widget.spaceModel.uuid,
|
||||
communityUuid: widget.context
|
||||
.read<CommunitiesTreeSelectionBloc>()
|
||||
.state
|
||||
.selectedCommunity!
|
||||
.uuid,
|
||||
);
|
||||
widget.context.read<SpaceDetailsBloc>().add(LoadSpaceDetails(param));
|
||||
}
|
||||
super.initState();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return const Dialog(
|
||||
child: Text('Create Space'),
|
||||
final isCreateMode = widget.spaceModel.uuid.isEmpty;
|
||||
if (isCreateMode) {
|
||||
return SpaceDetailsForm(
|
||||
title: widget.title,
|
||||
space: SpaceDetailsModel.empty(),
|
||||
onSave: widget.onSave,
|
||||
);
|
||||
}
|
||||
|
||||
return BlocBuilder<SpaceDetailsBloc, SpaceDetailsState>(
|
||||
bloc: widget.context.read<SpaceDetailsBloc>(),
|
||||
builder: (context, state) => switch (state) {
|
||||
SpaceDetailsInitial() => _buildLoadingDialog(),
|
||||
SpaceDetailsLoading() => _buildLoadingDialog(),
|
||||
SpaceDetailsLoaded(:final spaceDetails) => SpaceDetailsForm(
|
||||
title: widget.title,
|
||||
space: spaceDetails,
|
||||
onSave: widget.onSave,
|
||||
),
|
||||
SpaceDetailsFailure(:final errorMessage) => _buildErrorDialog(
|
||||
errorMessage,
|
||||
),
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildLoadingDialog() {
|
||||
return AlertDialog(
|
||||
title: widget.title,
|
||||
backgroundColor: ColorsManager.whiteColors,
|
||||
content: SizedBox(
|
||||
height: context.screenHeight * 0.3,
|
||||
width: context.screenWidth * 0.5,
|
||||
child: const Center(child: CircularProgressIndicator()),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildErrorDialog(String errorMessage) {
|
||||
return AlertDialog(
|
||||
title: widget.title,
|
||||
backgroundColor: ColorsManager.whiteColors,
|
||||
content: Center(
|
||||
child: SelectableText(
|
||||
errorMessage,
|
||||
style: context.textTheme.bodyLarge?.copyWith(
|
||||
color: ColorsManager.red,
|
||||
fontWeight: FontWeight.w500,
|
||||
fontSize: 18,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,76 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:syncrow_web/pages/space_management_v2/modules/space_details/domain/models/space_details_model.dart';
|
||||
import 'package:syncrow_web/pages/space_management_v2/modules/space_details/presentation/widgets/space_details_action_buttons.dart';
|
||||
import 'package:syncrow_web/pages/space_management_v2/modules/space_details/presentation/widgets/space_details_devices_box.dart';
|
||||
import 'package:syncrow_web/pages/space_management_v2/modules/space_details/presentation/widgets/space_icon_picker.dart';
|
||||
import 'package:syncrow_web/pages/space_management_v2/modules/space_details/presentation/widgets/space_name_text_field.dart';
|
||||
import 'package:syncrow_web/pages/space_management_v2/modules/space_details/presentation/widgets/space_sub_spaces_box.dart';
|
||||
import 'package:syncrow_web/pages/space_management_v2/modules/update_space/presentation/bloc/space_details_model_bloc/space_details_model_bloc.dart';
|
||||
import 'package:syncrow_web/utils/color_manager.dart';
|
||||
import 'package:syncrow_web/utils/extension/build_context_x.dart';
|
||||
|
||||
class SpaceDetailsForm extends StatelessWidget {
|
||||
const SpaceDetailsForm({
|
||||
required this.title,
|
||||
required this.space,
|
||||
required this.onSave,
|
||||
super.key,
|
||||
});
|
||||
|
||||
final Widget title;
|
||||
final SpaceDetailsModel space;
|
||||
final void Function(SpaceDetailsModel space) onSave;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return BlocProvider(
|
||||
create: (context) => SpaceDetailsModelBloc(initialState: space),
|
||||
child: BlocBuilder<SpaceDetailsModelBloc, SpaceDetailsModel>(
|
||||
buildWhen: (previous, current) => previous != current,
|
||||
builder: (context, space) {
|
||||
return AlertDialog(
|
||||
title: title,
|
||||
backgroundColor: ColorsManager.whiteColors,
|
||||
content: SizedBox(
|
||||
height: context.screenHeight * 0.3,
|
||||
width: context.screenWidth * 0.5,
|
||||
child: Row(
|
||||
spacing: 20,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Expanded(child: SpaceIconPicker(iconPath: space.icon)),
|
||||
Expanded(
|
||||
flex: 2,
|
||||
child: ListView(
|
||||
shrinkWrap: true,
|
||||
children: [
|
||||
SpaceNameTextField(
|
||||
initialValue: space.spaceName,
|
||||
isNameFieldExist: (value) => space.subspaces.any(
|
||||
(subspace) => subspace.name == value,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 32),
|
||||
SpaceSubSpacesBox(
|
||||
subspaces: space.subspaces,
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
SpaceDetailsDevicesBox(space: space),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
actions: [
|
||||
SpaceDetailsActionButtons(
|
||||
onSave: () => onSave(space),
|
||||
onCancel: Navigator.of(context).pop,
|
||||
),
|
||||
],
|
||||
);
|
||||
}),
|
||||
);
|
||||
}
|
||||
}
|
@ -0,0 +1,76 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:flutter_svg/svg.dart';
|
||||
import 'package:syncrow_web/pages/space_management_v2/modules/space_details/presentation/widgets/space_icon_selection_dialog.dart';
|
||||
import 'package:syncrow_web/pages/space_management_v2/modules/update_space/presentation/bloc/space_details_model_bloc/space_details_model_bloc.dart';
|
||||
import 'package:syncrow_web/utils/color_manager.dart';
|
||||
import 'package:syncrow_web/utils/constants/assets.dart';
|
||||
import 'package:syncrow_web/utils/extension/build_context_x.dart';
|
||||
|
||||
class SpaceIconPicker extends StatelessWidget {
|
||||
const SpaceIconPicker({
|
||||
required this.iconPath,
|
||||
super.key,
|
||||
});
|
||||
|
||||
final String iconPath;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Center(
|
||||
child: Stack(
|
||||
|
||||
alignment: Alignment.center,
|
||||
children: [
|
||||
Container(
|
||||
width: context.screenWidth * 0.175,
|
||||
height: context.screenHeight * 0.175,
|
||||
decoration: const BoxDecoration(
|
||||
color: ColorsManager.boxColor,
|
||||
shape: BoxShape.circle,
|
||||
),
|
||||
padding: const EdgeInsets.all(24),
|
||||
child: SvgPicture.asset(
|
||||
iconPath,
|
||||
width: context.screenWidth * 0.08,
|
||||
height: context.screenHeight * 0.08,
|
||||
),
|
||||
),
|
||||
Positioned.directional(
|
||||
top: 12,
|
||||
start: context.screenHeight * 0.06,
|
||||
textDirection: Directionality.of(context),
|
||||
child: InkWell(
|
||||
onTap: () {
|
||||
showDialog<String?>(
|
||||
context: context,
|
||||
builder: (context) => SpaceIconSelectionDialog(
|
||||
selectedIcon: iconPath,
|
||||
),
|
||||
).then((value) {
|
||||
if (value != null) {
|
||||
if (context.mounted) {
|
||||
context.read<SpaceDetailsModelBloc>().add(
|
||||
UpdateSpaceDetailsIcon(value),
|
||||
);
|
||||
}
|
||||
}
|
||||
});
|
||||
},
|
||||
child: Container(
|
||||
decoration: const BoxDecoration(
|
||||
shape: BoxShape.circle,
|
||||
),
|
||||
child: SvgPicture.asset(
|
||||
Assets.iconEdit,
|
||||
width: 16,
|
||||
height: 16,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
@ -0,0 +1,75 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_svg/svg.dart';
|
||||
import 'package:syncrow_web/utils/color_manager.dart';
|
||||
import 'package:syncrow_web/utils/constants/assets.dart';
|
||||
import 'package:syncrow_web/utils/extension/build_context_x.dart';
|
||||
|
||||
class SpaceIconSelectionDialog extends StatelessWidget {
|
||||
const SpaceIconSelectionDialog({super.key, required this.selectedIcon});
|
||||
final String selectedIcon;
|
||||
|
||||
static const List<String> _icons = [
|
||||
Assets.location,
|
||||
Assets.villa,
|
||||
Assets.gym,
|
||||
Assets.sauna,
|
||||
Assets.bbq,
|
||||
Assets.building,
|
||||
Assets.desk,
|
||||
Assets.door,
|
||||
Assets.parking,
|
||||
Assets.pool,
|
||||
Assets.stair,
|
||||
Assets.steamRoom,
|
||||
Assets.street,
|
||||
Assets.unit,
|
||||
];
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return AlertDialog(
|
||||
title: SelectableText(
|
||||
'Space Icon',
|
||||
style: context.textTheme.headlineMedium,
|
||||
),
|
||||
backgroundColor: ColorsManager.whiteColors,
|
||||
content: Container(
|
||||
width: context.screenWidth * 0.45,
|
||||
height: context.screenHeight * 0.275,
|
||||
padding: const EdgeInsets.all(12),
|
||||
decoration: BoxDecoration(
|
||||
color: ColorsManager.boxColor,
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
child: GridView.builder(
|
||||
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
|
||||
crossAxisCount: 7,
|
||||
crossAxisSpacing: 8,
|
||||
mainAxisSpacing: 16,
|
||||
),
|
||||
itemCount: _icons.length,
|
||||
itemBuilder: (context, index) {
|
||||
final isSelected = selectedIcon == _icons[index];
|
||||
return Container(
|
||||
padding: const EdgeInsetsDirectional.all(2),
|
||||
decoration: BoxDecoration(
|
||||
shape: BoxShape.circle,
|
||||
border: isSelected
|
||||
? Border.all(color: ColorsManager.vividBlue, width: 2)
|
||||
: null,
|
||||
),
|
||||
child: IconButton(
|
||||
onPressed: () => Navigator.of(context).pop(_icons[index]),
|
||||
icon: SvgPicture.asset(
|
||||
_icons[index],
|
||||
width: context.screenWidth * 0.03,
|
||||
height: context.screenHeight * 0.08,
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
@ -0,0 +1,85 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:syncrow_web/pages/space_management_v2/modules/update_space/presentation/bloc/space_details_model_bloc/space_details_model_bloc.dart';
|
||||
import 'package:syncrow_web/utils/color_manager.dart';
|
||||
import 'package:syncrow_web/utils/extension/build_context_x.dart';
|
||||
|
||||
class SpaceNameTextField extends StatefulWidget {
|
||||
const SpaceNameTextField({
|
||||
required this.initialValue,
|
||||
required this.isNameFieldExist,
|
||||
super.key,
|
||||
});
|
||||
|
||||
final String? initialValue;
|
||||
final bool Function(String value) isNameFieldExist;
|
||||
|
||||
@override
|
||||
State<SpaceNameTextField> createState() => _SpaceNameTextFieldState();
|
||||
}
|
||||
|
||||
class _SpaceNameTextFieldState extends State<SpaceNameTextField> {
|
||||
late final TextEditingController _controller;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
_controller = TextEditingController(text: widget.initialValue);
|
||||
super.initState();
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_controller.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
final _formKey = GlobalKey<FormState>();
|
||||
|
||||
String? _validateName(String? value) {
|
||||
if (value == null || value.isEmpty) {
|
||||
return '*Space name should not be empty.';
|
||||
}
|
||||
if (widget.isNameFieldExist(value)) {
|
||||
return '*Name already exists';
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Form(
|
||||
key: _formKey,
|
||||
autovalidateMode: AutovalidateMode.onUserInteraction,
|
||||
child: TextFormField(
|
||||
controller: _controller,
|
||||
onChanged: (value) => context.read<SpaceDetailsModelBloc>().add(
|
||||
UpdateSpaceDetailsName(value),
|
||||
),
|
||||
validator: _validateName,
|
||||
style: context.textTheme.bodyMedium,
|
||||
decoration: InputDecoration(
|
||||
hintText: 'Please enter the name',
|
||||
hintStyle: context.textTheme.bodyMedium!.copyWith(
|
||||
color: ColorsManager.lightGrayColor,
|
||||
),
|
||||
filled: true,
|
||||
fillColor: ColorsManager.boxColor,
|
||||
enabledBorder: _buildBorder(context, ColorsManager.vividBlue),
|
||||
focusedBorder: _buildBorder(context, ColorsManager.primaryColor),
|
||||
errorBorder: _buildBorder(context, context.theme.colorScheme.error),
|
||||
focusedErrorBorder: _buildBorder(context, context.theme.colorScheme.error),
|
||||
errorStyle: context.textTheme.bodySmall?.copyWith(
|
||||
color: context.theme.colorScheme.error,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
OutlineInputBorder _buildBorder(BuildContext context, [Color? color]) {
|
||||
return OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
borderSide: BorderSide(width: 1, color: color ?? ColorsManager.boxColor),
|
||||
);
|
||||
}
|
||||
}
|
@ -0,0 +1,74 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:syncrow_web/common/edit_chip.dart';
|
||||
import 'package:syncrow_web/pages/space_management_v2/modules/space_details/domain/models/space_details_model.dart';
|
||||
import 'package:syncrow_web/pages/space_management_v2/modules/space_details/presentation/widgets/button_content_widget.dart';
|
||||
import 'package:syncrow_web/pages/space_management_v2/modules/space_details/presentation/widgets/space_sub_spaces_dialog.dart';
|
||||
import 'package:syncrow_web/pages/space_management_v2/modules/space_details/presentation/widgets/subspace_name_display_widget.dart';
|
||||
import 'package:syncrow_web/pages/space_management_v2/modules/update_space/presentation/bloc/space_details_model_bloc/space_details_model_bloc.dart';
|
||||
import 'package:syncrow_web/utils/color_manager.dart';
|
||||
import 'package:syncrow_web/utils/constants/assets.dart';
|
||||
|
||||
class SpaceSubSpacesBox extends StatelessWidget {
|
||||
const SpaceSubSpacesBox({super.key, required this.subspaces});
|
||||
|
||||
final List<Subspace> subspaces;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
if (subspaces.isEmpty) {
|
||||
return TextButton(
|
||||
style: TextButton.styleFrom(
|
||||
padding: EdgeInsets.zero,
|
||||
overlayColor: ColorsManager.transparentColor,
|
||||
),
|
||||
onPressed: () => _showSubSpacesDialog(context),
|
||||
child: const SizedBox(
|
||||
width: double.infinity,
|
||||
child: ButtonContentWidget(
|
||||
svgAssets: Assets.addIcon,
|
||||
label: 'Create Sub Spaces',
|
||||
),
|
||||
),
|
||||
);
|
||||
} else {
|
||||
return Container(
|
||||
padding: const EdgeInsets.all(8.0),
|
||||
width: double.infinity,
|
||||
decoration: BoxDecoration(
|
||||
color: ColorsManager.textFieldGreyColor,
|
||||
borderRadius: BorderRadius.circular(15),
|
||||
border: Border.all(
|
||||
color: ColorsManager.textFieldGreyColor,
|
||||
width: 3.0,
|
||||
),
|
||||
),
|
||||
child: Wrap(
|
||||
spacing: 8.0,
|
||||
runSpacing: 8.0,
|
||||
children: [
|
||||
...subspaces.map((e) => SubspaceNameDisplayWidget(subSpace: e)),
|
||||
EditChip(
|
||||
onTap: () => _showSubSpacesDialog(context),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
void _showSubSpacesDialog(BuildContext context) {
|
||||
showDialog<void>(
|
||||
context: context,
|
||||
barrierDismissible: false,
|
||||
builder: (_) => SpaceSubSpacesDialog(
|
||||
subspaces: subspaces,
|
||||
onSave: (subspaces) {
|
||||
context.read<SpaceDetailsModelBloc>().add(
|
||||
UpdateSpaceDetailsSubspaces(subspaces),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
@ -0,0 +1,90 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:syncrow_web/pages/space_management_v2/modules/space_details/domain/models/space_details_model.dart';
|
||||
import 'package:syncrow_web/pages/space_management_v2/modules/space_details/presentation/widgets/space_details_action_buttons.dart';
|
||||
import 'package:syncrow_web/pages/space_management_v2/modules/space_details/presentation/widgets/sub_spaces_input.dart';
|
||||
import 'package:uuid/uuid.dart';
|
||||
|
||||
class SpaceSubSpacesDialog extends StatefulWidget {
|
||||
const SpaceSubSpacesDialog({
|
||||
required this.subspaces,
|
||||
required this.onSave,
|
||||
super.key,
|
||||
});
|
||||
|
||||
final List<Subspace> subspaces;
|
||||
final void Function(List<Subspace> subspaces) onSave;
|
||||
|
||||
@override
|
||||
State<SpaceSubSpacesDialog> createState() => _SpaceSubSpacesDialogState();
|
||||
}
|
||||
|
||||
class _SpaceSubSpacesDialogState extends State<SpaceSubSpacesDialog> {
|
||||
late List<Subspace> _subspaces;
|
||||
|
||||
bool get _hasDuplicateNames =>
|
||||
_subspaces.map((subspace) => subspace.name.toLowerCase()).toSet().length !=
|
||||
_subspaces.length;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_subspaces = List.from(widget.subspaces);
|
||||
}
|
||||
|
||||
void _handleSubspaceAdded(String name) {
|
||||
setState(() {
|
||||
_subspaces = [
|
||||
..._subspaces,
|
||||
Subspace(
|
||||
name: name,
|
||||
uuid: const Uuid().v4(),
|
||||
productAllocations: const [],
|
||||
),
|
||||
];
|
||||
});
|
||||
}
|
||||
|
||||
void _handleSubspaceDeleted(String uuid) => setState(
|
||||
() => _subspaces = _subspaces.where((s) => s.uuid != uuid).toList(),
|
||||
);
|
||||
|
||||
void _handleSave() {
|
||||
widget.onSave(_subspaces);
|
||||
Navigator.of(context).pop();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return AlertDialog(
|
||||
title: const SelectableText('Create Sub Spaces'),
|
||||
content: Column(
|
||||
spacing: 12,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
SubSpacesInput(
|
||||
subSpaces: _subspaces,
|
||||
onSubspaceAdded: _handleSubspaceAdded,
|
||||
onSubspaceDeleted: _handleSubspaceDeleted,
|
||||
),
|
||||
AnimatedSwitcher(
|
||||
duration: const Duration(milliseconds: 100),
|
||||
child: Visibility(
|
||||
key: ValueKey(_hasDuplicateNames),
|
||||
visible: _hasDuplicateNames,
|
||||
child: const SelectableText(
|
||||
'Error: Duplicate subspace names are not allowed.',
|
||||
style: TextStyle(color: Colors.red),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
actions: [
|
||||
SpaceDetailsActionButtons(
|
||||
onSave: _hasDuplicateNames ? null : _handleSave,
|
||||
onCancel: Navigator.of(context).pop,
|
||||
)
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
@ -0,0 +1,107 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:syncrow_web/pages/space_management_v2/modules/space_details/domain/models/space_details_model.dart';
|
||||
import 'package:syncrow_web/pages/space_management_v2/modules/space_details/presentation/widgets/subspace_chip.dart';
|
||||
import 'package:syncrow_web/utils/color_manager.dart';
|
||||
import 'package:syncrow_web/utils/extension/build_context_x.dart';
|
||||
|
||||
class SubSpacesInput extends StatefulWidget {
|
||||
const SubSpacesInput({
|
||||
super.key,
|
||||
required this.subSpaces,
|
||||
required this.onSubspaceAdded,
|
||||
required this.onSubspaceDeleted,
|
||||
});
|
||||
|
||||
final List<Subspace> subSpaces;
|
||||
final void Function(String name) onSubspaceAdded;
|
||||
final void Function(String uuid) onSubspaceDeleted;
|
||||
|
||||
@override
|
||||
State<SubSpacesInput> createState() => _SubSpacesInputState();
|
||||
}
|
||||
|
||||
class _SubSpacesInputState extends State<SubSpacesInput> {
|
||||
late final TextEditingController _subspaceNameController;
|
||||
late final FocusNode _focusNode;
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_subspaceNameController = TextEditingController();
|
||||
_focusNode = FocusNode();
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_subspaceNameController.dispose();
|
||||
_focusNode.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
width: context.screenWidth * 0.35,
|
||||
padding: const EdgeInsets.symmetric(
|
||||
vertical: 10,
|
||||
horizontal: 16,
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
color: ColorsManager.boxColor,
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
),
|
||||
child: Wrap(
|
||||
spacing: 8,
|
||||
runSpacing: 8,
|
||||
alignment: WrapAlignment.start,
|
||||
crossAxisAlignment: WrapCrossAlignment.center,
|
||||
children: [
|
||||
...widget.subSpaces.asMap().entries.map(
|
||||
(entry) {
|
||||
final index = entry.key;
|
||||
final subSpace = entry.value;
|
||||
|
||||
final lowerName = subSpace.name.toLowerCase();
|
||||
|
||||
final duplicateIndices = widget.subSpaces
|
||||
.asMap()
|
||||
.entries
|
||||
.where((e) => e.value.name.toLowerCase() == lowerName)
|
||||
.map((e) => e.key)
|
||||
.toList();
|
||||
final isDuplicate = duplicateIndices.length > 1 &&
|
||||
duplicateIndices.indexOf(index) != 0;
|
||||
return SubspaceChip(
|
||||
subSpace: subSpace,
|
||||
isDuplicate: isDuplicate,
|
||||
onDeleted: () => widget.onSubspaceDeleted(subSpace.uuid),
|
||||
);
|
||||
},
|
||||
),
|
||||
SizedBox(
|
||||
width: 200,
|
||||
child: TextField(
|
||||
focusNode: _focusNode,
|
||||
controller: _subspaceNameController,
|
||||
decoration: InputDecoration(
|
||||
border: InputBorder.none,
|
||||
hintText: widget.subSpaces.isEmpty ? 'Please enter the name' : null,
|
||||
hintStyle: context.textTheme.bodySmall?.copyWith(
|
||||
color: ColorsManager.lightGrayColor,
|
||||
),
|
||||
),
|
||||
onSubmitted: (value) {
|
||||
final trimmedValue = value.trim();
|
||||
if (trimmedValue.isNotEmpty) {
|
||||
widget.onSubspaceAdded(trimmedValue);
|
||||
_subspaceNameController.clear();
|
||||
_focusNode.requestFocus();
|
||||
}
|
||||
},
|
||||
style: context.textTheme.bodyMedium,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
@ -0,0 +1,55 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:syncrow_web/pages/space_management_v2/modules/space_details/domain/models/space_details_model.dart';
|
||||
import 'package:syncrow_web/utils/color_manager.dart';
|
||||
import 'package:syncrow_web/utils/extension/build_context_x.dart';
|
||||
|
||||
class SubspaceChip extends StatelessWidget {
|
||||
const SubspaceChip({
|
||||
required this.subSpace,
|
||||
required this.isDuplicate,
|
||||
required this.onDeleted,
|
||||
super.key,
|
||||
});
|
||||
|
||||
final Subspace subSpace;
|
||||
final bool isDuplicate;
|
||||
final void Function() onDeleted;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Chip(
|
||||
label: Text(
|
||||
subSpace.name,
|
||||
style: context.textTheme.bodySmall?.copyWith(
|
||||
color: isDuplicate ? ColorsManager.red : ColorsManager.spaceColor,
|
||||
),
|
||||
),
|
||||
backgroundColor: ColorsManager.whiteColors,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
side: BorderSide(
|
||||
color: isDuplicate ? ColorsManager.red : ColorsManager.transparentColor,
|
||||
width: 0,
|
||||
),
|
||||
),
|
||||
deleteIcon: Container(
|
||||
padding: const EdgeInsetsDirectional.all(1),
|
||||
decoration: BoxDecoration(
|
||||
shape: BoxShape.circle,
|
||||
border: Border.all(
|
||||
color: ColorsManager.lightGrayColor,
|
||||
width: 1.5,
|
||||
),
|
||||
),
|
||||
child: const FittedBox(
|
||||
fit: BoxFit.scaleDown,
|
||||
child: Icon(
|
||||
Icons.close,
|
||||
color: ColorsManager.lightGrayColor,
|
||||
),
|
||||
),
|
||||
),
|
||||
onDeleted: onDeleted,
|
||||
);
|
||||
}
|
||||
}
|
@ -0,0 +1,171 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:syncrow_web/pages/space_management_v2/modules/space_details/domain/models/space_details_model.dart';
|
||||
import 'package:syncrow_web/pages/space_management_v2/modules/update_space/presentation/bloc/space_details_model_bloc/space_details_model_bloc.dart';
|
||||
import 'package:syncrow_web/utils/color_manager.dart';
|
||||
import 'package:syncrow_web/utils/extension/build_context_x.dart';
|
||||
|
||||
class SubspaceNameDisplayWidget extends StatefulWidget {
|
||||
const SubspaceNameDisplayWidget({super.key, required this.subSpace});
|
||||
|
||||
final Subspace subSpace;
|
||||
|
||||
@override
|
||||
State<SubspaceNameDisplayWidget> createState() =>
|
||||
_SubspaceNameDisplayWidgetState();
|
||||
}
|
||||
|
||||
class _SubspaceNameDisplayWidgetState extends State<SubspaceNameDisplayWidget> {
|
||||
late final TextEditingController _controller;
|
||||
late final FocusNode _focusNode;
|
||||
bool _isEditing = false;
|
||||
bool _hasDuplicateName = false;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
_controller = TextEditingController(text: widget.subSpace.name);
|
||||
_focusNode = FocusNode();
|
||||
super.initState();
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_controller.dispose();
|
||||
_focusNode.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
bool _checkForDuplicateName(String name) {
|
||||
final bloc = context.read<SpaceDetailsModelBloc>();
|
||||
return bloc.state.subspaces
|
||||
.where((s) => s.uuid != widget.subSpace.uuid)
|
||||
.any((s) => s.name.toLowerCase() == name.toLowerCase());
|
||||
}
|
||||
|
||||
void _handleNameChange(String value) {
|
||||
setState(() {
|
||||
_hasDuplicateName = _checkForDuplicateName(value);
|
||||
});
|
||||
}
|
||||
|
||||
void _tryToFinishEditing() {
|
||||
if (!_hasDuplicateName) {
|
||||
_onFinishEditing();
|
||||
}
|
||||
}
|
||||
|
||||
void _tryToSubmit(String value) {
|
||||
if (_hasDuplicateName) return;
|
||||
|
||||
final bloc = context.read<SpaceDetailsModelBloc>();
|
||||
bloc.add(
|
||||
UpdateSpaceDetailsSubspaces(
|
||||
bloc.state.subspaces
|
||||
.map(
|
||||
(e) => e.uuid == widget.subSpace.uuid ? e.copyWith(name: value) : e,
|
||||
)
|
||||
.toList(),
|
||||
),
|
||||
);
|
||||
_onFinishEditing();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final textStyle = context.textTheme.bodySmall?.copyWith(
|
||||
color: ColorsManager.spaceColor,
|
||||
);
|
||||
return InkWell(
|
||||
onTap: () {
|
||||
setState(() => _isEditing = true);
|
||||
_focusNode.requestFocus();
|
||||
},
|
||||
child: Chip(
|
||||
backgroundColor: ColorsManager.whiteColors,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
side: const BorderSide(color: ColorsManager.transparentColor),
|
||||
),
|
||||
onDeleted: () {
|
||||
final bloc = context.read<SpaceDetailsModelBloc>();
|
||||
bloc.add(
|
||||
UpdateSpaceDetailsSubspaces(
|
||||
bloc.state.subspaces
|
||||
.where((s) => s.uuid != widget.subSpace.uuid)
|
||||
.toList(),
|
||||
),
|
||||
);
|
||||
},
|
||||
deleteIcon: Container(
|
||||
padding: const EdgeInsetsDirectional.all(1),
|
||||
decoration: BoxDecoration(
|
||||
shape: BoxShape.circle,
|
||||
border: Border.all(
|
||||
color: ColorsManager.lightGrayColor,
|
||||
width: 1.5,
|
||||
),
|
||||
),
|
||||
child: const FittedBox(
|
||||
child: Icon(
|
||||
Icons.close,
|
||||
color: ColorsManager.lightGrayColor,
|
||||
),
|
||||
),
|
||||
),
|
||||
label: Visibility(
|
||||
visible: _isEditing,
|
||||
replacement: Text(
|
||||
widget.subSpace.name,
|
||||
style: textStyle,
|
||||
),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
SizedBox(
|
||||
width: context.screenWidth * 0.065,
|
||||
height: context.screenHeight * 0.025,
|
||||
child: TextField(
|
||||
focusNode: _focusNode,
|
||||
controller: _controller,
|
||||
style: textStyle?.copyWith(
|
||||
color: _hasDuplicateName ? Colors.red : null,
|
||||
),
|
||||
decoration: const InputDecoration.collapsed(
|
||||
hintText: '',
|
||||
),
|
||||
onChanged: _handleNameChange,
|
||||
onTapOutside: (_) => _tryToFinishEditing(),
|
||||
onSubmitted: _tryToSubmit,
|
||||
),
|
||||
),
|
||||
if (_hasDuplicateName)
|
||||
AnimatedSwitcher(
|
||||
duration: const Duration(milliseconds: 250),
|
||||
child: Visibility(
|
||||
key: ValueKey(_hasDuplicateName),
|
||||
visible: _hasDuplicateName,
|
||||
child: Text(
|
||||
'Name already exists',
|
||||
style: textStyle?.copyWith(
|
||||
color: Colors.red,
|
||||
fontSize: 8,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void _onFinishEditing() {
|
||||
setState(() {
|
||||
_isEditing = false;
|
||||
_hasDuplicateName = false;
|
||||
});
|
||||
_focusNode.unfocus();
|
||||
}
|
||||
}
|
@ -1,10 +1,9 @@
|
||||
import 'package:dio/dio.dart';
|
||||
import 'package:syncrow_web/pages/common/bloc/project_manager.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/params/load_tags_param.dart';
|
||||
import 'package:syncrow_web/pages/space_management_v2/modules/tags/domain/services/tags_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';
|
||||
|
||||
final class RemoteTagsService implements TagsService {
|
||||
const RemoteTagsService(this._httpService);
|
||||
@ -14,17 +13,10 @@ final class RemoteTagsService implements TagsService {
|
||||
static const _defaultErrorMessage = 'Failed to load tags';
|
||||
|
||||
@override
|
||||
Future<List<Tag>> loadTags(LoadTagsParam param) async {
|
||||
if (param.projectUuid == null) {
|
||||
throw Exception('Project UUID is required');
|
||||
}
|
||||
|
||||
Future<List<Tag>> loadTags() async {
|
||||
try {
|
||||
final response = await _httpService.get(
|
||||
path: ApiEndpoints.listTags.replaceAll(
|
||||
'{projectUuid}',
|
||||
param.projectUuid!,
|
||||
),
|
||||
path: await _makeUrl(),
|
||||
expectedResponseModel: (json) {
|
||||
final result = json as Map<String, dynamic>;
|
||||
final data = result['data'] as List<dynamic>;
|
||||
@ -46,4 +38,12 @@ final class RemoteTagsService implements TagsService {
|
||||
throw APIException(formattedErrorMessage);
|
||||
}
|
||||
}
|
||||
|
||||
Future<String> _makeUrl() async {
|
||||
final projectUuid = await ProjectManager.getProjectUUID();
|
||||
if (projectUuid == null || projectUuid.isEmpty) {
|
||||
throw APIException('Project UUID is required');
|
||||
}
|
||||
return '/projects/$projectUuid/tags';
|
||||
}
|
||||
}
|
||||
|
@ -13,6 +13,13 @@ class Tag extends Equatable {
|
||||
required this.updatedAt,
|
||||
});
|
||||
|
||||
factory Tag.empty() => const Tag(
|
||||
uuid: '',
|
||||
name: '',
|
||||
createdAt: '',
|
||||
updatedAt: '',
|
||||
);
|
||||
|
||||
factory Tag.fromJson(Map<String, dynamic> json) {
|
||||
return Tag(
|
||||
uuid: json['uuid'] as String,
|
||||
|
@ -1,6 +1,5 @@
|
||||
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/params/load_tags_param.dart';
|
||||
|
||||
abstract interface class TagsService {
|
||||
Future<List<Tag>> loadTags(LoadTagsParam param);
|
||||
Future<List<Tag>> loadTags();
|
||||
}
|
||||
|
@ -1,7 +1,6 @@
|
||||
import 'package:bloc/bloc.dart';
|
||||
import 'package:equatable/equatable.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/params/load_tags_param.dart';
|
||||
import 'package:syncrow_web/pages/space_management_v2/modules/tags/domain/services/tags_service.dart';
|
||||
import 'package:syncrow_web/services/api/api_exception.dart';
|
||||
|
||||
@ -21,7 +20,7 @@ class TagsBloc extends Bloc<TagsEvent, TagsState> {
|
||||
) async {
|
||||
emit(TagsLoading());
|
||||
try {
|
||||
final tags = await _tagsService.loadTags(event.param);
|
||||
final tags = await _tagsService.loadTags();
|
||||
emit(TagsLoaded(tags));
|
||||
} on APIException catch (e) {
|
||||
emit(TagsFailure(e.message));
|
||||
|
@ -8,10 +8,5 @@ abstract class TagsEvent extends Equatable {
|
||||
}
|
||||
|
||||
class LoadTags extends TagsEvent {
|
||||
final LoadTagsParam param;
|
||||
|
||||
const LoadTags(this.param);
|
||||
|
||||
@override
|
||||
List<Object?> get props => [param];
|
||||
const LoadTags();
|
||||
}
|
||||
|
@ -0,0 +1,100 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_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/domain/models/product.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/presentation/widgets/space_details_action_buttons.dart';
|
||||
import 'package:syncrow_web/pages/space_management_v2/modules/tags/presentation/widgets/products_grid.dart';
|
||||
import 'package:syncrow_web/services/api/http_service.dart';
|
||||
import 'package:syncrow_web/utils/color_manager.dart';
|
||||
import 'package:syncrow_web/utils/extension/build_context_x.dart';
|
||||
|
||||
class AddDeviceTypeWidget extends StatefulWidget {
|
||||
const AddDeviceTypeWidget({super.key});
|
||||
|
||||
@override
|
||||
State<AddDeviceTypeWidget> createState() => _AddDeviceTypeWidgetState();
|
||||
}
|
||||
|
||||
class _AddDeviceTypeWidgetState extends State<AddDeviceTypeWidget> {
|
||||
final Map<Product, int> _selectedProducts = {};
|
||||
|
||||
void _onIncrement(Product product) {
|
||||
setState(() {
|
||||
_selectedProducts[product] = (_selectedProducts[product] ?? 0) + 1;
|
||||
});
|
||||
}
|
||||
|
||||
void _onDecrement(Product product) {
|
||||
setState(() {
|
||||
if ((_selectedProducts[product] ?? 0) > 0) {
|
||||
_selectedProducts[product] = _selectedProducts[product]! - 1;
|
||||
if (_selectedProducts[product] == 0) {
|
||||
_selectedProducts.remove(product);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return BlocProvider(
|
||||
create: (_) => ProductsBloc(RemoteProductsService(HTTPService()))
|
||||
..add(const LoadProducts()),
|
||||
child: Builder(
|
||||
builder: (context) => AlertDialog(
|
||||
title: const SelectableText('Add Devices'),
|
||||
backgroundColor: ColorsManager.whiteColors,
|
||||
content: BlocBuilder<ProductsBloc, ProductsState>(
|
||||
builder: (context, state) => switch (state) {
|
||||
ProductsInitial() || ProductsLoading() => _buildLoading(context),
|
||||
ProductsLoaded(:final products) => ProductsGrid(
|
||||
products: products,
|
||||
selectedProducts: _selectedProducts,
|
||||
onIncrement: _onIncrement,
|
||||
onDecrement: _onDecrement,
|
||||
),
|
||||
ProductsFailure(:final errorMessage) => _buildFailure(
|
||||
context,
|
||||
errorMessage,
|
||||
),
|
||||
},
|
||||
),
|
||||
actions: [
|
||||
SpaceDetailsActionButtons(
|
||||
onSave: () {
|
||||
final result = _selectedProducts.entries
|
||||
.expand((entry) => List.generate(entry.value, (_) => entry.key))
|
||||
.toList();
|
||||
Navigator.of(context).pop(result);
|
||||
},
|
||||
onCancel: Navigator.of(context).pop,
|
||||
saveButtonLabel: 'Next',
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildLoading(BuildContext context) => SizedBox(
|
||||
width: context.screenWidth * 0.9,
|
||||
height: context.screenHeight * 0.65,
|
||||
child: const Center(child: CircularProgressIndicator()),
|
||||
);
|
||||
|
||||
Widget _buildFailure(BuildContext context, String errorMessage) {
|
||||
return SizedBox(
|
||||
width: context.screenWidth * 0.9,
|
||||
height: context.screenHeight * 0.65,
|
||||
child: Center(
|
||||
child: SelectableText(
|
||||
errorMessage,
|
||||
style: context.textTheme.bodyMedium?.copyWith(
|
||||
color: context.theme.colorScheme.error,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
@ -0,0 +1,231 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:syncrow_web/pages/space_management_v2/modules/products/domain/models/product.dart';
|
||||
import 'package:syncrow_web/pages/space_management_v2/modules/space_details/domain/models/space_details_model.dart';
|
||||
import 'package:syncrow_web/pages/space_management_v2/modules/space_details/presentation/widgets/space_details_action_buttons.dart';
|
||||
import 'package:syncrow_web/pages/space_management_v2/modules/tags/domain/models/tag.dart';
|
||||
import 'package:syncrow_web/pages/space_management_v2/modules/tags/presentation/widgets/add_device_type_widget.dart';
|
||||
import 'package:syncrow_web/pages/space_management_v2/modules/tags/presentation/widgets/assign_tags_error_messages.dart';
|
||||
import 'package:syncrow_web/pages/space_management_v2/modules/tags/presentation/widgets/assign_tags_table.dart';
|
||||
import 'package:syncrow_web/utils/extension/build_context_x.dart';
|
||||
import 'package:uuid/uuid.dart';
|
||||
|
||||
class AssignTagsDialog extends StatefulWidget {
|
||||
const AssignTagsDialog({required this.space, super.key});
|
||||
|
||||
final SpaceDetailsModel space;
|
||||
|
||||
@override
|
||||
State<AssignTagsDialog> createState() => _AssignTagsDialogState();
|
||||
}
|
||||
|
||||
class _AssignTagsDialogState extends State<AssignTagsDialog> {
|
||||
late SpaceDetailsModel _space;
|
||||
final Map<String, String> _validationErrors = {};
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_space = widget.space.copyWith(
|
||||
productAllocations:
|
||||
widget.space.productAllocations.map((e) => e.copyWith()).toList(),
|
||||
subspaces: widget.space.subspaces
|
||||
.map(
|
||||
(s) => s.copyWith(
|
||||
productAllocations:
|
||||
s.productAllocations.map((e) => e.copyWith()).toList(),
|
||||
),
|
||||
)
|
||||
.toList(),
|
||||
);
|
||||
_validateAllTags();
|
||||
}
|
||||
|
||||
void _validateAllTags() {
|
||||
final newErrors = <String, String>{};
|
||||
final allAllocations = [
|
||||
..._space.productAllocations,
|
||||
..._space.subspaces.expand((s) => s.productAllocations),
|
||||
];
|
||||
|
||||
final allocationsByProductType = <String, List<ProductAllocation>>{};
|
||||
for (final allocation in allAllocations) {
|
||||
(allocationsByProductType[allocation.product.productType] ??= [])
|
||||
.add(allocation);
|
||||
}
|
||||
|
||||
for (final productType in allocationsByProductType.keys) {
|
||||
final allocations = allocationsByProductType[productType]!;
|
||||
final tagCounts = <String, int>{};
|
||||
|
||||
for (final allocation in allocations) {
|
||||
final tagName = allocation.tag.name.trim().toLowerCase();
|
||||
if (tagName.isEmpty) {
|
||||
newErrors[allocation.uuid] =
|
||||
'Tag for ${allocation.product.name} cannot be empty.';
|
||||
} else {
|
||||
tagCounts[tagName] = (tagCounts[tagName] ?? 0) + 1;
|
||||
}
|
||||
}
|
||||
|
||||
for (final allocation in allocations) {
|
||||
final tagName = allocation.tag.name.trim().toLowerCase();
|
||||
if (tagName.isNotEmpty && (tagCounts[tagName] ?? 0) > 1) {
|
||||
newErrors[allocation.uuid] =
|
||||
'Tag "${allocation.tag.name}" is used by multiple $productType devices.';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
setState(() {
|
||||
_validationErrors
|
||||
..clear()
|
||||
..addAll(newErrors);
|
||||
});
|
||||
}
|
||||
|
||||
void _handleTagChange(String allocationUuid, Tag newTag) {
|
||||
setState(() {
|
||||
var index =
|
||||
_space.productAllocations.indexWhere((pa) => pa.uuid == allocationUuid);
|
||||
if (index != -1) {
|
||||
final allocation = _space.productAllocations[index];
|
||||
_space.productAllocations[index] = allocation.copyWith(tag: newTag);
|
||||
} else {
|
||||
for (final subspace in _space.subspaces) {
|
||||
index = subspace.productAllocations
|
||||
.indexWhere((pa) => pa.uuid == allocationUuid);
|
||||
if (index != -1) {
|
||||
final allocation = subspace.productAllocations[index];
|
||||
subspace.productAllocations[index] = allocation.copyWith(tag: newTag);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
_validateAllTags();
|
||||
}
|
||||
|
||||
void _handleLocationChange(String allocationUuid, String? newSubspaceUuid) {
|
||||
setState(() {
|
||||
ProductAllocation? allocationToMove;
|
||||
|
||||
var index =
|
||||
_space.productAllocations.indexWhere((pa) => pa.uuid == allocationUuid);
|
||||
if (index != -1) {
|
||||
allocationToMove = _space.productAllocations.removeAt(index);
|
||||
} else {
|
||||
for (final subspace in _space.subspaces) {
|
||||
index = subspace.productAllocations
|
||||
.indexWhere((pa) => pa.uuid == allocationUuid);
|
||||
if (index != -1) {
|
||||
allocationToMove = subspace.productAllocations.removeAt(index);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (allocationToMove == null) return;
|
||||
|
||||
if (newSubspaceUuid == null) {
|
||||
_space.productAllocations.add(allocationToMove);
|
||||
} else {
|
||||
_space.subspaces
|
||||
.firstWhere((s) => s.uuid == newSubspaceUuid)
|
||||
.productAllocations
|
||||
.add(allocationToMove);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
void _handleProductDelete(String allocationUuid) {
|
||||
setState(() {
|
||||
_space.productAllocations.removeWhere((pa) => pa.uuid == allocationUuid);
|
||||
|
||||
for (final subspace in _space.subspaces) {
|
||||
subspace.productAllocations.removeWhere(
|
||||
(pa) => pa.uuid == allocationUuid,
|
||||
);
|
||||
}
|
||||
});
|
||||
_validateAllTags();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final allProductAllocations = [
|
||||
..._space.productAllocations,
|
||||
..._space.subspaces.expand((s) => s.productAllocations),
|
||||
];
|
||||
|
||||
final productLocations = <String, String?>{};
|
||||
for (final pa in _space.productAllocations) {
|
||||
productLocations[pa.uuid] = null;
|
||||
}
|
||||
for (final subspace in _space.subspaces) {
|
||||
for (final pa in subspace.productAllocations) {
|
||||
productLocations[pa.uuid] = subspace.uuid;
|
||||
}
|
||||
}
|
||||
|
||||
final hasErrors = _validationErrors.isNotEmpty;
|
||||
|
||||
return AlertDialog(
|
||||
title: const SelectableText('Assign Tags'),
|
||||
content: ConstrainedBox(
|
||||
constraints: BoxConstraints(
|
||||
maxWidth: context.screenWidth * 0.6,
|
||||
minWidth: context.screenWidth * 0.6,
|
||||
maxHeight: context.screenHeight * 0.8,
|
||||
),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
SizedBox(
|
||||
width: double.infinity,
|
||||
child: AssignTagsTable(
|
||||
productAllocations: allProductAllocations,
|
||||
subspaces: _space.subspaces,
|
||||
productLocations: productLocations,
|
||||
onTagSelected: _handleTagChange,
|
||||
onLocationSelected: _handleLocationChange,
|
||||
onProductDeleted: _handleProductDelete,
|
||||
),
|
||||
),
|
||||
if (hasErrors)
|
||||
AssignTagsErrorMessages(
|
||||
errorMessages: _validationErrors.values.toSet().toList(),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
actions: [
|
||||
SpaceDetailsActionButtons(
|
||||
onSave: hasErrors ? null : () => Navigator.of(context).pop(_space),
|
||||
onCancel: () async {
|
||||
final newProducts = await showDialog<List<Product>>(
|
||||
context: context,
|
||||
builder: (context) => const AddDeviceTypeWidget(),
|
||||
);
|
||||
|
||||
if (newProducts == null || newProducts.isEmpty) return;
|
||||
|
||||
setState(() {
|
||||
for (final product in newProducts) {
|
||||
_space.productAllocations.add(
|
||||
ProductAllocation(
|
||||
uuid: const Uuid().v4(),
|
||||
product: product,
|
||||
tag: Tag.empty(),
|
||||
),
|
||||
);
|
||||
}
|
||||
});
|
||||
_validateAllTags();
|
||||
},
|
||||
cancelButtonLabel: 'Add New Device',
|
||||
)
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
@ -0,0 +1,29 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:syncrow_web/utils/extension/build_context_x.dart';
|
||||
|
||||
class AssignTagsErrorMessages extends StatelessWidget {
|
||||
const AssignTagsErrorMessages({super.key, required this.errorMessages});
|
||||
|
||||
final List<String> errorMessages;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.only(top: 16),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: errorMessages
|
||||
.map(
|
||||
(error) => Text(
|
||||
'- $error',
|
||||
style: context.textTheme.bodyMedium?.copyWith(
|
||||
color: context.theme.colorScheme.error,
|
||||
),
|
||||
),
|
||||
)
|
||||
.toList(),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
@ -0,0 +1,204 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:syncrow_web/common/dialog_dropdown.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/tags/data/services/remote_tags_service.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/presentation/bloc/tags_bloc.dart';
|
||||
import 'package:syncrow_web/pages/space_management_v2/modules/tags/presentation/widgets/product_tag_field.dart';
|
||||
import 'package:syncrow_web/services/api/http_service.dart';
|
||||
import 'package:syncrow_web/utils/color_manager.dart';
|
||||
import 'package:syncrow_web/utils/extension/build_context_x.dart';
|
||||
|
||||
class AssignTagsTable extends StatelessWidget {
|
||||
const AssignTagsTable({
|
||||
required this.productAllocations,
|
||||
required this.subspaces,
|
||||
required this.productLocations,
|
||||
required this.onTagSelected,
|
||||
required this.onLocationSelected,
|
||||
required this.onProductDeleted,
|
||||
super.key,
|
||||
});
|
||||
|
||||
final List<ProductAllocation> productAllocations;
|
||||
final List<Subspace> subspaces;
|
||||
final Map<String, String?> productLocations;
|
||||
final void Function(String, Tag) onTagSelected;
|
||||
final void Function(String, String?) onLocationSelected;
|
||||
final void Function(String) onProductDeleted;
|
||||
|
||||
DataColumn _buildDataColumn(BuildContext context, String label) {
|
||||
return DataColumn(
|
||||
label: SelectableText(label, style: context.textTheme.bodyMedium),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return BlocProvider<TagsBloc>(
|
||||
create: (BuildContext context) => TagsBloc(
|
||||
RemoteTagsService(HTTPService()),
|
||||
)..add(const LoadTags()),
|
||||
child: BlocBuilder<TagsBloc, TagsState>(
|
||||
builder: (context, state) {
|
||||
return switch (state) {
|
||||
TagsLoading() || TagsInitial() => const Center(
|
||||
child: CircularProgressIndicator(),
|
||||
),
|
||||
TagsFailure(:final message) => Center(
|
||||
child: Text(message),
|
||||
),
|
||||
TagsLoaded(:final tags) => ClipRRect(
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
child: DataTable(
|
||||
headingRowColor: WidgetStateProperty.all(
|
||||
ColorsManager.dataHeaderGrey,
|
||||
),
|
||||
key: ValueKey(productAllocations.length),
|
||||
border: TableBorder.all(
|
||||
color: ColorsManager.dataHeaderGrey,
|
||||
width: 1,
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
),
|
||||
columns: [
|
||||
_buildDataColumn(context, '#'),
|
||||
_buildDataColumn(context, 'Device'),
|
||||
_buildDataColumn(context, 'Tag'),
|
||||
_buildDataColumn(context, 'Location'),
|
||||
],
|
||||
rows: productAllocations.isEmpty
|
||||
? [
|
||||
DataRow(
|
||||
cells: [
|
||||
DataCell(
|
||||
Center(
|
||||
child: SelectableText(
|
||||
'No Devices Available',
|
||||
style: context.textTheme.bodyMedium?.copyWith(
|
||||
color: ColorsManager.lightGrayColor,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
DataCell.empty,
|
||||
DataCell.empty,
|
||||
DataCell.empty,
|
||||
],
|
||||
),
|
||||
]
|
||||
: List.generate(productAllocations.length, (index) {
|
||||
final productAllocation = productAllocations[index];
|
||||
final allocationUuid = productAllocation.uuid;
|
||||
|
||||
final availableTags = tags
|
||||
.where(
|
||||
(tag) =>
|
||||
!productAllocations
|
||||
.where((p) =>
|
||||
p.product.productType ==
|
||||
productAllocation.product.productType)
|
||||
.map((p) => p.tag.name.toLowerCase())
|
||||
.contains(tag.name.toLowerCase()) ||
|
||||
tag.uuid == productAllocation.tag.uuid,
|
||||
)
|
||||
.toList();
|
||||
|
||||
final currentLocationUuid =
|
||||
productLocations[allocationUuid];
|
||||
final currentLocationName = currentLocationUuid == null
|
||||
? 'Main Space'
|
||||
: subspaces
|
||||
.firstWhere((s) => s.uuid == currentLocationUuid)
|
||||
.name;
|
||||
|
||||
return DataRow(
|
||||
key: ValueKey(allocationUuid),
|
||||
cells: [
|
||||
DataCell(Text((index + 1).toString())),
|
||||
DataCell(
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Expanded(
|
||||
child: Text(
|
||||
productAllocation.product.name,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
)),
|
||||
const SizedBox(width: 10),
|
||||
Container(
|
||||
width: 20,
|
||||
height: 20,
|
||||
decoration: BoxDecoration(
|
||||
shape: BoxShape.circle,
|
||||
border: Border.all(
|
||||
color: ColorsManager.lightGrayColor,
|
||||
width: 1,
|
||||
),
|
||||
),
|
||||
child: IconButton(
|
||||
icon: const Icon(
|
||||
Icons.close,
|
||||
color: ColorsManager.lightGreyColor,
|
||||
size: 16,
|
||||
),
|
||||
onPressed: () {
|
||||
onProductDeleted(allocationUuid);
|
||||
},
|
||||
tooltip: 'Delete Tag',
|
||||
padding: EdgeInsets.zero,
|
||||
constraints: const BoxConstraints(),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
DataCell(
|
||||
Container(
|
||||
alignment: Alignment.centerLeft,
|
||||
width: double.infinity,
|
||||
child: ProductTagField(
|
||||
key: ValueKey('dropdown_$allocationUuid'),
|
||||
productName: productAllocation.product.uuid,
|
||||
initialValue: productAllocation.tag,
|
||||
onSelected: (newTag) {
|
||||
onTagSelected(allocationUuid, newTag);
|
||||
},
|
||||
items: availableTags,
|
||||
),
|
||||
),
|
||||
),
|
||||
DataCell(
|
||||
SizedBox(
|
||||
width: double.infinity,
|
||||
child: DialogDropdown(
|
||||
items: [
|
||||
'Main Space',
|
||||
...subspaces.map((s) => s.name)
|
||||
],
|
||||
selectedValue: currentLocationName,
|
||||
onSelected: (newLocationName) {
|
||||
final newSubspaceUuid = newLocationName ==
|
||||
'Main Space'
|
||||
? null
|
||||
: subspaces
|
||||
.firstWhere(
|
||||
(s) => s.name == newLocationName)
|
||||
.uuid;
|
||||
onLocationSelected(
|
||||
allocationUuid, newSubspaceUuid);
|
||||
},
|
||||
)),
|
||||
),
|
||||
],
|
||||
);
|
||||
}),
|
||||
),
|
||||
),
|
||||
_ => const SizedBox.shrink(),
|
||||
};
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user