mirror of
https://github.com/SyncrowIOT/web.git
synced 2025-07-14 17:25:50 +00:00
Compare commits
64 Commits
SP-1716-FE
...
SP-1721-FE
Author | SHA1 | Date | |
---|---|---|---|
de5d8df01c | |||
5218641705 | |||
ab6a6851f2 | |||
beb33e37fa | |||
3bee17c574 | |||
f4b5c6fb52 | |||
086f3cedf8 | |||
559091faa0 | |||
035c03c6b2 | |||
eba351c9be | |||
cf1b34ee0a | |||
c112cde634 | |||
5663e2084e | |||
3cd0125310 | |||
e0980b324c | |||
65d541d594 | |||
7331c8440b | |||
a409e34643 | |||
7cc59e43df | |||
21f8b2962c | |||
645a07287e | |||
df29aab111 | |||
e55e793081 | |||
885ef61114 | |||
bfd6b5c3a0 | |||
2b638940ae | |||
3e95bf4473 | |||
2d16bda61d | |||
5c90d5f6b9 | |||
d6a48850a7 | |||
6cac94a1c4 | |||
9f28e1ccef | |||
83202204b0 | |||
d87739f1fd | |||
5cd083a37b | |||
6534bfae5b | |||
2b8d987c69 | |||
707cb4791f | |||
03c45ed8d0 | |||
9e0ea4ad6f | |||
9e6b14737f | |||
7c2aed2d58 | |||
bcf62027bc | |||
b001713ce4 | |||
bab3226c73 | |||
fa1eaa570c | |||
4cfb984d2c | |||
4c06479469 | |||
3101960201 | |||
ddfd4ee153 | |||
7f0484eec6 | |||
dc7064d142 | |||
e523a83912 | |||
e917225c3d | |||
66ed30b50c | |||
47bd6ff89e | |||
138390496c | |||
df87e41d61 | |||
f0bfe085a4 | |||
bb846f797f | |||
e234c9f3b2 | |||
bcd0ae4a2a | |||
cebce2ce7f | |||
97e3fb68bf |
@ -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"
|
||||
}
|
||||
]
|
||||
}
|
5
assets/icons/x_delete.svg
Normal file
5
assets/icons/x_delete.svg
Normal file
@ -0,0 +1,5 @@
|
||||
<svg width="35" height="35" viewBox="0 0 35 35" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M17.5 34.9999C27.1649 34.9999 34.9999 27.1649 34.9999 17.4999C34.9999 7.83499 27.1649 0 17.5 0C7.83499 0 0 7.83499 0 17.5C0 27.1651 7.83499 34.9999 17.5 34.9999Z" fill="#FF6465"/>
|
||||
<path opacity="0.1" d="M4.70804 17.5C4.70804 8.63343 11.3024 1.30805 19.854 0.158115C19.0839 0.0545507 18.2984 0 17.5 0C7.835 0 0 7.835 0 17.5C0 27.1651 7.83499 35 17.4999 35C18.2983 35 19.0839 34.9455 19.8539 34.8419C11.3024 33.6919 4.70804 26.3665 4.70804 17.5Z" fill="black"/>
|
||||
<path d="M21.4229 17.5003L26.0301 12.8931C26.365 12.5582 26.365 12.0152 26.0301 11.6804L23.3197 8.96992C22.9848 8.63503 22.4418 8.63503 22.107 8.96992L17.4997 13.5772L12.8924 8.96992C12.5576 8.63503 12.0146 8.63503 11.6798 8.96992L8.96931 11.6804C8.63442 12.0153 8.63442 12.5582 8.96931 12.8931L13.5766 17.5003L8.96931 22.1076C8.63442 22.4425 8.63442 22.9855 8.96931 23.3204L11.6798 26.0308C12.0146 26.3657 12.5576 26.3657 12.8924 26.0308L17.4997 21.4235L22.1071 26.0308C22.442 26.3657 22.9849 26.3657 23.3198 26.0308L26.0302 23.3204C26.3651 22.9855 26.3651 22.4425 26.0302 22.1076L21.4229 17.5003Z" fill="white"/>
|
||||
</svg>
|
After Width: | Height: | Size: 1.2 KiB |
@ -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';
|
||||
|
@ -0,0 +1,41 @@
|
||||
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/domain/models/space_details_model.dart';
|
||||
|
||||
abstract final class SpacesRecursiveHelper {
|
||||
static List<SpaceModel> recusrivelyUpdate(
|
||||
List<SpaceModel> spaces,
|
||||
SpaceDetailsModel updatedSpace,
|
||||
) {
|
||||
return spaces.map((space) {
|
||||
if (space.uuid == updatedSpace.uuid) {
|
||||
return space.copyWith(
|
||||
spaceName: updatedSpace.spaceName,
|
||||
icon: updatedSpace.icon,
|
||||
);
|
||||
}
|
||||
if (space.children.isNotEmpty) {
|
||||
return space.copyWith(
|
||||
children: recusrivelyUpdate(space.children, updatedSpace),
|
||||
);
|
||||
}
|
||||
return space;
|
||||
}).toList();
|
||||
}
|
||||
|
||||
static List<SpaceModel> recusrivelyDelete(
|
||||
List<SpaceModel> spaces,
|
||||
String spaceUuid,
|
||||
) {
|
||||
final s = spaces.map((space) {
|
||||
if (space.uuid == spaceUuid) return null;
|
||||
if (space.children.isNotEmpty) {
|
||||
return space.copyWith(
|
||||
children: recusrivelyDelete(space.children, spaceUuid),
|
||||
);
|
||||
}
|
||||
return space;
|
||||
}).toList();
|
||||
|
||||
return s.whereType<SpaceModel>().toList();
|
||||
}
|
||||
}
|
@ -0,0 +1,14 @@
|
||||
import 'package:syncrow_web/pages/space_management_v2/modules/communities/domain/models/community_model.dart';
|
||||
import 'package:syncrow_web/pages/space_management_v2/modules/communities/domain/models/space_model.dart';
|
||||
|
||||
class SpaceReorderDataModel {
|
||||
const SpaceReorderDataModel({
|
||||
required this.space,
|
||||
this.parent,
|
||||
this.community,
|
||||
});
|
||||
|
||||
final SpaceModel space;
|
||||
final SpaceModel? parent;
|
||||
final CommunityModel? community;
|
||||
}
|
@ -37,8 +37,8 @@ class SpacesConnectionsArrowPainter extends CustomPainter {
|
||||
|
||||
final path = Path()..moveTo(startPoint.dx, startPoint.dy);
|
||||
|
||||
final controlPoint1 = Offset(startPoint.dx, startPoint.dy + 20);
|
||||
final controlPoint2 = Offset(endPoint.dx, endPoint.dy - 60);
|
||||
final controlPoint1 = Offset(startPoint.dx, startPoint.dy + 100);
|
||||
final controlPoint2 = Offset(endPoint.dx, endPoint.dy - 100);
|
||||
|
||||
path.cubicTo(controlPoint1.dx, controlPoint1.dy, controlPoint2.dx,
|
||||
controlPoint2.dy, endPoint.dx, endPoint.dy);
|
||||
|
@ -7,6 +7,8 @@ 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';
|
||||
@ -14,21 +16,37 @@ import 'package:syncrow_web/services/api/http_service.dart';
|
||||
import 'package:syncrow_web/utils/theme/responsive_text_theme.dart';
|
||||
import 'package:syncrow_web/web_layout/web_scaffold.dart';
|
||||
|
||||
class SpaceManagementPage extends StatelessWidget {
|
||||
class SpaceManagementPage extends StatefulWidget {
|
||||
const SpaceManagementPage({super.key});
|
||||
|
||||
@override
|
||||
State<SpaceManagementPage> createState() => _SpaceManagementPageState();
|
||||
}
|
||||
|
||||
class _SpaceManagementPageState extends State<SpaceManagementPage> {
|
||||
late final CommunitiesBloc communitiesBloc;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
communitiesBloc = CommunitiesBloc(
|
||||
communitiesService: DebouncedCommunitiesService(
|
||||
RemoteCommunitiesService(HTTPService()),
|
||||
),
|
||||
)..add(const LoadCommunities(LoadCommunitiesParam()));
|
||||
|
||||
super.initState();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return MultiBlocProvider(
|
||||
providers: [
|
||||
BlocProvider.value(value: communitiesBloc),
|
||||
BlocProvider(
|
||||
create: (context) => CommunitiesBloc(
|
||||
communitiesService: DebouncedCommunitiesService(
|
||||
RemoteCommunitiesService(HTTPService()),
|
||||
),
|
||||
)..add(const LoadCommunities(LoadCommunitiesParam())),
|
||||
create: (context) => CommunitiesTreeSelectionBloc(
|
||||
communitiesBloc: communitiesBloc,
|
||||
),
|
||||
),
|
||||
BlocProvider(create: (context) => CommunitiesTreeSelectionBloc()),
|
||||
BlocProvider(
|
||||
create: (context) => SpaceDetailsBloc(
|
||||
UniqueSubspacesDecorator(
|
||||
@ -36,6 +54,11 @@ class SpaceManagementPage extends StatelessWidget {
|
||||
),
|
||||
),
|
||||
),
|
||||
BlocProvider(
|
||||
create: (context) => ProductsBloc(
|
||||
RemoteProductsService(HTTPService()),
|
||||
),
|
||||
),
|
||||
],
|
||||
child: WebScaffold(
|
||||
appBarTitle: Text(
|
||||
|
@ -1,13 +1,17 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:syncrow_web/pages/space_management_v2/main_module/models/space_connection_model.dart';
|
||||
import 'package:syncrow_web/pages/space_management_v2/main_module/models/space_reorder_data_model.dart';
|
||||
import 'package:syncrow_web/pages/space_management_v2/main_module/painters/spaces_connections_arrow_painter.dart';
|
||||
import 'package:syncrow_web/pages/space_management_v2/main_module/widgets/create_space_button.dart';
|
||||
import 'package:syncrow_web/pages/space_management_v2/main_module/widgets/space_card_widget.dart';
|
||||
import 'package:syncrow_web/pages/space_management_v2/main_module/widgets/space_cell.dart';
|
||||
import 'package:syncrow_web/pages/space_management_v2/modules/communities/domain/models/community_model.dart';
|
||||
import 'package:syncrow_web/pages/space_management_v2/modules/communities/domain/models/space_model.dart';
|
||||
import 'package:syncrow_web/pages/space_management_v2/modules/communities/presentation/bloc/communities_bloc.dart';
|
||||
import 'package:syncrow_web/pages/space_management_v2/modules/communities/presentation/communities_tree_selection_bloc/communities_tree_selection_bloc.dart';
|
||||
import 'package:syncrow_web/pages/space_management_v2/modules/space_details/presentation/helpers/space_details_dialog_helper.dart';
|
||||
import 'package:syncrow_web/utils/extension/build_context_x.dart';
|
||||
|
||||
class CommunityStructureCanvas extends StatefulWidget {
|
||||
const CommunityStructureCanvas({
|
||||
@ -31,8 +35,9 @@ class _CommunityStructureCanvasState extends State<CommunityStructureCanvas>
|
||||
final double _horizontalSpacing = 150.0;
|
||||
final double _verticalSpacing = 120.0;
|
||||
|
||||
late TransformationController _transformationController;
|
||||
late AnimationController _animationController;
|
||||
late final TransformationController _transformationController;
|
||||
late final AnimationController _animationController;
|
||||
SpaceReorderDataModel? _draggedData;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
@ -46,7 +51,6 @@ class _CommunityStructureCanvasState extends State<CommunityStructureCanvas>
|
||||
|
||||
@override
|
||||
void didUpdateWidget(covariant CommunityStructureCanvas oldWidget) {
|
||||
super.didUpdateWidget(oldWidget);
|
||||
if (widget.selectedSpace?.uuid != oldWidget.selectedSpace?.uuid) {
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
if (mounted) {
|
||||
@ -54,6 +58,7 @@ class _CommunityStructureCanvasState extends State<CommunityStructureCanvas>
|
||||
}
|
||||
});
|
||||
}
|
||||
super.didUpdateWidget(oldWidget);
|
||||
}
|
||||
|
||||
@override
|
||||
@ -97,7 +102,7 @@ class _CommunityStructureCanvasState extends State<CommunityStructureCanvas>
|
||||
final position = _positions[space.uuid];
|
||||
if (position == null) return;
|
||||
|
||||
const scale = 1.5;
|
||||
const scale = 1;
|
||||
final viewSize = context.size;
|
||||
if (viewSize == null) return;
|
||||
|
||||
@ -112,16 +117,33 @@ class _CommunityStructureCanvasState extends State<CommunityStructureCanvas>
|
||||
_runAnimation(matrix);
|
||||
}
|
||||
|
||||
void _onReorder(SpaceReorderDataModel data, int newIndex) {
|
||||
final newCommunity = widget.community.copyWith();
|
||||
final children = data.parent?.children ?? newCommunity.spaces;
|
||||
final oldIndex = children.indexWhere((s) => s.uuid == data.space.uuid);
|
||||
if (oldIndex != -1) {
|
||||
final item = children.removeAt(oldIndex);
|
||||
if (newIndex > oldIndex) {
|
||||
children.insert(newIndex - 1, item);
|
||||
} else {
|
||||
children.insert(newIndex, item);
|
||||
}
|
||||
}
|
||||
context.read<CommunitiesBloc>().add(
|
||||
CommunitiesUpdateCommunity(newCommunity),
|
||||
);
|
||||
}
|
||||
|
||||
void _onSpaceTapped(SpaceModel? space) {
|
||||
context.read<CommunitiesTreeSelectionBloc>().add(
|
||||
SelectSpaceEvent(community: widget.community, space: space),
|
||||
);
|
||||
}
|
||||
|
||||
void _resetSelectionAndZoom() {
|
||||
void _resetSelectionAndZoom([CommunityModel? community]) {
|
||||
context.read<CommunitiesTreeSelectionBloc>().add(
|
||||
SelectSpaceEvent(
|
||||
community: widget.community,
|
||||
community: community ?? widget.community,
|
||||
space: null,
|
||||
),
|
||||
);
|
||||
@ -182,7 +204,8 @@ class _CommunityStructureCanvasState extends State<CommunityStructureCanvas>
|
||||
_positions.clear();
|
||||
final community = widget.community;
|
||||
|
||||
_calculateLayout(community.spaces, 0, {});
|
||||
final levelXOffset = <int, double>{};
|
||||
_calculateLayout(community.spaces, 0, levelXOffset);
|
||||
|
||||
final selectedSpace = widget.selectedSpace;
|
||||
final highlightedUuids = <String>{};
|
||||
@ -193,7 +216,24 @@ class _CommunityStructureCanvasState extends State<CommunityStructureCanvas>
|
||||
|
||||
final widgets = <Widget>[];
|
||||
final connections = <SpaceConnectionModel>[];
|
||||
_generateWidgets(community.spaces, widgets, connections, highlightedUuids);
|
||||
_generateWidgets(
|
||||
widget.community.spaces,
|
||||
widgets,
|
||||
connections,
|
||||
highlightedUuids,
|
||||
community: widget.community,
|
||||
);
|
||||
|
||||
final createButtonX = levelXOffset[0] ?? 0.0;
|
||||
const createButtonY = 0.0;
|
||||
|
||||
widgets.add(
|
||||
Positioned(
|
||||
left: createButtonX,
|
||||
top: createButtonY,
|
||||
child: CreateSpaceButton(communityUuid: widget.community.uuid),
|
||||
),
|
||||
);
|
||||
|
||||
return [
|
||||
CustomPaint(
|
||||
@ -211,58 +251,178 @@ class _CommunityStructureCanvasState extends State<CommunityStructureCanvas>
|
||||
List<SpaceModel> spaces,
|
||||
List<Widget> widgets,
|
||||
List<SpaceConnectionModel> connections,
|
||||
Set<String> highlightedUuids,
|
||||
) {
|
||||
for (final space in spaces) {
|
||||
Set<String> highlightedUuids, {
|
||||
CommunityModel? community,
|
||||
SpaceModel? parent,
|
||||
}) {
|
||||
if (spaces.isNotEmpty) {
|
||||
final firstChildPos = _positions[spaces.first.uuid]!;
|
||||
final targetPos = Offset(
|
||||
firstChildPos.dx - (_horizontalSpacing / 4),
|
||||
firstChildPos.dy,
|
||||
);
|
||||
widgets.add(_buildDropTarget(parent, community, 0, targetPos));
|
||||
}
|
||||
|
||||
for (var i = 0; i < spaces.length; i++) {
|
||||
final space = spaces[i];
|
||||
final position = _positions[space.uuid];
|
||||
if (position == null) continue;
|
||||
if (position == null) {
|
||||
continue;
|
||||
}
|
||||
|
||||
final isHighlighted = highlightedUuids.contains(space.uuid);
|
||||
final hasNoSelectedSpace = widget.selectedSpace == null;
|
||||
|
||||
final spaceCard = SpaceCardWidget(
|
||||
buildSpaceContainer: () {
|
||||
return Opacity(
|
||||
opacity: hasNoSelectedSpace || isHighlighted ? 1.0 : 0.5,
|
||||
child: Tooltip(
|
||||
message: space.spaceName,
|
||||
preferBelow: false,
|
||||
child: SpaceCell(
|
||||
onTap: () => _onSpaceTapped(space),
|
||||
icon: space.icon,
|
||||
name: space.spaceName,
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
onTap: () => SpaceDetailsDialogHelper.showCreate(
|
||||
context,
|
||||
communityUuid: widget.community.uuid,
|
||||
),
|
||||
);
|
||||
|
||||
final reorderData = SpaceReorderDataModel(
|
||||
space: space,
|
||||
parent: parent,
|
||||
community: community,
|
||||
);
|
||||
|
||||
widgets.add(
|
||||
Positioned(
|
||||
left: position.dx,
|
||||
top: position.dy,
|
||||
width: _cardWidth,
|
||||
height: _cardHeight,
|
||||
child: SpaceCardWidget(
|
||||
buildSpaceContainer: () {
|
||||
return Opacity(
|
||||
opacity: hasNoSelectedSpace || isHighlighted ? 1.0 : 0.5,
|
||||
child: Tooltip(
|
||||
message: space.spaceName,
|
||||
preferBelow: false,
|
||||
child: SpaceCell(
|
||||
onTap: () => _onSpaceTapped(space),
|
||||
icon: space.icon,
|
||||
name: space.spaceName,
|
||||
),
|
||||
child: Draggable<SpaceReorderDataModel>(
|
||||
data: reorderData,
|
||||
feedback: Material(
|
||||
color: Colors.transparent,
|
||||
child: Opacity(
|
||||
opacity: 0.2,
|
||||
child: SizedBox(
|
||||
width: _cardWidth,
|
||||
height: _cardHeight,
|
||||
child: spaceCard,
|
||||
),
|
||||
);
|
||||
},
|
||||
onTap: () => SpaceDetailsDialogHelper.showCreate(context),
|
||||
),
|
||||
),
|
||||
onDragStarted: () => setState(() => _draggedData = reorderData),
|
||||
onDragEnd: (_) => setState(() => _draggedData = null),
|
||||
onDraggableCanceled: (_, __) => setState(() => _draggedData = null),
|
||||
childWhenDragging: Opacity(opacity: 0.4, child: spaceCard),
|
||||
child: spaceCard,
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
final targetPos = Offset(
|
||||
position.dx + _cardWidth + (_horizontalSpacing / 4) - 20,
|
||||
position.dy,
|
||||
);
|
||||
widgets.add(_buildDropTarget(parent, community, i + 1, targetPos));
|
||||
|
||||
for (final child in space.children) {
|
||||
connections.add(
|
||||
SpaceConnectionModel(from: space.uuid, to: child.uuid),
|
||||
connections.add(SpaceConnectionModel(from: space.uuid, to: child.uuid));
|
||||
}
|
||||
|
||||
if (space.children.isNotEmpty) {
|
||||
_generateWidgets(
|
||||
space.children,
|
||||
widgets,
|
||||
connections,
|
||||
highlightedUuids,
|
||||
parent: space,
|
||||
);
|
||||
}
|
||||
_generateWidgets(space.children, widgets, connections, highlightedUuids);
|
||||
}
|
||||
}
|
||||
|
||||
Widget _buildDropTarget(
|
||||
SpaceModel? parent,
|
||||
CommunityModel? community,
|
||||
int index,
|
||||
Offset position,
|
||||
) {
|
||||
return Positioned(
|
||||
left: position.dx,
|
||||
top: position.dy,
|
||||
width: 40,
|
||||
height: _cardHeight,
|
||||
child: DragTarget<SpaceReorderDataModel>(
|
||||
builder: (context, candidateData, rejectedData) {
|
||||
if (_draggedData == null) {
|
||||
return const SizedBox();
|
||||
}
|
||||
|
||||
final isTargetForDragged = (_draggedData?.parent?.uuid == parent?.uuid &&
|
||||
_draggedData?.community == null) ||
|
||||
(_draggedData?.community?.uuid == community?.uuid &&
|
||||
_draggedData?.parent == null);
|
||||
|
||||
if (!isTargetForDragged) {
|
||||
return const SizedBox();
|
||||
}
|
||||
|
||||
return Container(
|
||||
width: 40,
|
||||
height: _cardHeight,
|
||||
decoration: BoxDecoration(
|
||||
color: context.theme.colorScheme.primary.withValues(
|
||||
alpha: candidateData.isNotEmpty ? 0.7 : 0.3,
|
||||
),
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
child: Icon(
|
||||
Icons.add,
|
||||
color: context.theme.colorScheme.onPrimary,
|
||||
),
|
||||
);
|
||||
},
|
||||
onWillAcceptWithDetails: (data) {
|
||||
final children = parent?.children ?? community?.spaces ?? [];
|
||||
final isSameParent = (data.data.parent?.uuid == parent?.uuid &&
|
||||
data.data.community == null) ||
|
||||
(data.data.community?.uuid == community?.uuid &&
|
||||
data.data.parent == null);
|
||||
|
||||
if (!isSameParent) {
|
||||
return false;
|
||||
}
|
||||
|
||||
final oldIndex =
|
||||
children.indexWhere((s) => s.uuid == data.data.space.uuid);
|
||||
if (oldIndex == index || oldIndex == index - 1) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
},
|
||||
onAcceptWithDetails: (data) => _onReorder(data.data, index),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final treeWidgets = _buildTreeWidgets();
|
||||
return InteractiveViewer(
|
||||
transformationController: _transformationController,
|
||||
boundaryMargin: EdgeInsets.symmetric(
|
||||
horizontal: MediaQuery.sizeOf(context).width * 0.3,
|
||||
vertical: MediaQuery.sizeOf(context).height * 0.3,
|
||||
horizontal: context.screenWidth * 0.3,
|
||||
vertical: context.screenHeight * 0.3,
|
||||
),
|
||||
minScale: 0.5,
|
||||
maxScale: 3.0,
|
||||
@ -270,8 +430,8 @@ class _CommunityStructureCanvasState extends State<CommunityStructureCanvas>
|
||||
child: GestureDetector(
|
||||
onTap: _resetSelectionAndZoom,
|
||||
child: SizedBox(
|
||||
width: MediaQuery.sizeOf(context).width * 5,
|
||||
height: MediaQuery.sizeOf(context).height * 5,
|
||||
width: context.screenWidth * 5,
|
||||
height: context.screenHeight * 5,
|
||||
child: Stack(children: treeWidgets),
|
||||
),
|
||||
),
|
||||
|
@ -2,18 +2,17 @@ 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/main_module/widgets/community_structure_header_action_buttons_composer.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';
|
||||
import 'package:syncrow_web/utils/extension/build_context_x.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),
|
||||
@ -34,7 +33,7 @@ class CommunityStructureHeader extends StatelessWidget {
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Expanded(
|
||||
child: _buildCommunityInfo(context, theme, screenWidth),
|
||||
child: _buildCommunityInfo(context, screenWidth),
|
||||
),
|
||||
const SizedBox(width: 16),
|
||||
],
|
||||
@ -44,8 +43,7 @@ class CommunityStructureHeader extends StatelessWidget {
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildCommunityInfo(
|
||||
BuildContext context, ThemeData theme, double screenWidth) {
|
||||
Widget _buildCommunityInfo(BuildContext context, double screenWidth) {
|
||||
final selectedCommunity =
|
||||
context.watch<CommunitiesTreeSelectionBloc>().state.selectedCommunity;
|
||||
final selectedSpace =
|
||||
@ -55,8 +53,9 @@ class CommunityStructureHeader extends StatelessWidget {
|
||||
children: [
|
||||
Text(
|
||||
'Community Structure',
|
||||
style: theme.textTheme.headlineLarge
|
||||
?.copyWith(color: ColorsManager.blackColor),
|
||||
style: context.textTheme.headlineLarge?.copyWith(
|
||||
color: ColorsManager.blackColor,
|
||||
),
|
||||
),
|
||||
if (selectedCommunity != null)
|
||||
Row(
|
||||
@ -67,8 +66,9 @@ class CommunityStructureHeader extends StatelessWidget {
|
||||
Flexible(
|
||||
child: SelectableText(
|
||||
selectedCommunity.name,
|
||||
style: theme.textTheme.bodyLarge
|
||||
?.copyWith(color: ColorsManager.blackColor),
|
||||
style: context.textTheme.bodyLarge?.copyWith(
|
||||
color: ColorsManager.blackColor,
|
||||
),
|
||||
maxLines: 1,
|
||||
),
|
||||
),
|
||||
@ -90,15 +90,8 @@ class CommunityStructureHeader extends StatelessWidget {
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
CommunityStructureHeaderActionButtons(
|
||||
onDelete: (space) {},
|
||||
onDuplicate: (space) {},
|
||||
onEdit: (space) {
|
||||
SpaceDetailsDialogHelper.showEdit(
|
||||
context,
|
||||
spaceModel: selectedSpace!,
|
||||
);
|
||||
},
|
||||
CommunityStructureHeaderActionButtonsComposer(
|
||||
selectedCommunity: selectedCommunity,
|
||||
selectedSpace: selectedSpace,
|
||||
),
|
||||
],
|
||||
|
@ -0,0 +1,69 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:syncrow_web/pages/space_management_v2/main_module/helpers/spaces_recursive_helper.dart';
|
||||
import 'package:syncrow_web/pages/space_management_v2/main_module/widgets/community_structure_header_action_buttons.dart';
|
||||
import 'package:syncrow_web/pages/space_management_v2/modules/communities/domain/models/community_model.dart';
|
||||
import 'package:syncrow_web/pages/space_management_v2/modules/communities/domain/models/space_model.dart';
|
||||
import 'package:syncrow_web/pages/space_management_v2/modules/communities/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/delete_space/presentation/widgets/delete_space_dialog.dart';
|
||||
import 'package:syncrow_web/pages/space_management_v2/modules/space_details/presentation/helpers/space_details_dialog_helper.dart';
|
||||
|
||||
class CommunityStructureHeaderActionButtonsComposer extends StatelessWidget {
|
||||
const CommunityStructureHeaderActionButtonsComposer({
|
||||
required this.selectedCommunity,
|
||||
required this.selectedSpace,
|
||||
super.key,
|
||||
});
|
||||
final CommunityModel selectedCommunity;
|
||||
final SpaceModel? selectedSpace;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return CommunityStructureHeaderActionButtons(
|
||||
onDelete: (space) => showDialog<void>(
|
||||
context: context,
|
||||
barrierDismissible: false,
|
||||
builder: (_) => DeleteSpaceDialog(
|
||||
space: space,
|
||||
community: selectedCommunity,
|
||||
onSuccess: () {
|
||||
final updatedSpaces = SpacesRecursiveHelper.recusrivelyDelete(
|
||||
selectedCommunity.spaces,
|
||||
space.uuid,
|
||||
);
|
||||
final community = selectedCommunity.copyWith(
|
||||
spaces: updatedSpaces,
|
||||
);
|
||||
context.read<CommunitiesBloc>().add(
|
||||
CommunitiesUpdateCommunity(community),
|
||||
);
|
||||
context.read<CommunitiesTreeSelectionBloc>().add(
|
||||
SelectCommunityEvent(community: selectedCommunity),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
onDuplicate: (space) {},
|
||||
onEdit: (space) => SpaceDetailsDialogHelper.showEdit(
|
||||
context,
|
||||
spaceModel: selectedSpace!,
|
||||
communityUuid: selectedCommunity.uuid,
|
||||
onSuccess: (updatedSpaceDetails) {
|
||||
final communitiesBloc = context.read<CommunitiesBloc>();
|
||||
final updatedSpaces = SpacesRecursiveHelper.recusrivelyUpdate(
|
||||
selectedCommunity.spaces,
|
||||
updatedSpaceDetails,
|
||||
);
|
||||
|
||||
final community = selectedCommunity.copyWith(
|
||||
spaces: updatedSpaces,
|
||||
);
|
||||
|
||||
communitiesBloc.add(CommunitiesUpdateCommunity(community));
|
||||
},
|
||||
),
|
||||
selectedSpace: selectedSpace,
|
||||
);
|
||||
}
|
||||
}
|
@ -2,38 +2,66 @@ import 'package:flutter/material.dart';
|
||||
import 'package:syncrow_web/pages/space_management_v2/modules/space_details/presentation/helpers/space_details_dialog_helper.dart';
|
||||
import 'package:syncrow_web/utils/color_manager.dart';
|
||||
|
||||
class CreateSpaceButton extends StatelessWidget {
|
||||
const CreateSpaceButton({super.key});
|
||||
class CreateSpaceButton extends StatefulWidget {
|
||||
const CreateSpaceButton({
|
||||
required this.communityUuid,
|
||||
super.key,
|
||||
});
|
||||
|
||||
final String communityUuid;
|
||||
|
||||
@override
|
||||
State<CreateSpaceButton> createState() => _CreateSpaceButtonState();
|
||||
}
|
||||
|
||||
class _CreateSpaceButtonState extends State<CreateSpaceButton> {
|
||||
bool _isHovered = false;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return GestureDetector(
|
||||
onTap: () => SpaceDetailsDialogHelper.showCreate(context),
|
||||
child: Container(
|
||||
height: 60,
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.grey.withValues(alpha: 0.5),
|
||||
spreadRadius: 5,
|
||||
blurRadius: 7,
|
||||
offset: const Offset(0, 3),
|
||||
),
|
||||
],
|
||||
return Tooltip(
|
||||
margin: const EdgeInsets.symmetric(vertical: 24),
|
||||
message: 'Create a new space',
|
||||
child: InkWell(
|
||||
onTap: () => SpaceDetailsDialogHelper.showCreate(
|
||||
context,
|
||||
communityUuid: widget.communityUuid,
|
||||
),
|
||||
child: Center(
|
||||
child: Container(
|
||||
width: 40,
|
||||
height: 40,
|
||||
decoration: const BoxDecoration(
|
||||
color: ColorsManager.boxColor,
|
||||
shape: BoxShape.circle,
|
||||
),
|
||||
child: const Icon(
|
||||
Icons.add,
|
||||
color: Colors.blue,
|
||||
child: MouseRegion(
|
||||
onEnter: (_) => setState(() => _isHovered = true),
|
||||
onExit: (_) => setState(() => _isHovered = false),
|
||||
child: AnimatedOpacity(
|
||||
duration: const Duration(milliseconds: 100),
|
||||
opacity: _isHovered ? 1.0 : 0.45,
|
||||
child: Container(
|
||||
width: 150,
|
||||
height: 90,
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.grey.withValues(alpha: 0.2),
|
||||
spreadRadius: 3,
|
||||
blurRadius: 8,
|
||||
offset: const Offset(0, 4),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: Container(
|
||||
margin: const EdgeInsets.symmetric(vertical: 20),
|
||||
decoration: BoxDecoration(
|
||||
border: Border.all(color: ColorsManager.borderColor, width: 2),
|
||||
color: ColorsManager.boxColor,
|
||||
shape: BoxShape.circle,
|
||||
),
|
||||
child: const Center(
|
||||
child: Icon(
|
||||
Icons.add,
|
||||
color: Colors.blue,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
@ -22,22 +22,20 @@ class _SpaceCardWidgetState extends State<SpaceCardWidget> {
|
||||
return MouseRegion(
|
||||
onEnter: (_) => setState(() => isHovered = true),
|
||||
onExit: (_) => setState(() => isHovered = false),
|
||||
child: SizedBox(
|
||||
child: Stack(
|
||||
clipBehavior: Clip.none,
|
||||
alignment: Alignment.center,
|
||||
children: [
|
||||
widget.buildSpaceContainer(),
|
||||
if (isHovered)
|
||||
Positioned(
|
||||
bottom: 0,
|
||||
child: PlusButtonWidget(
|
||||
offset: Offset.zero,
|
||||
onButtonTap: widget.onTap,
|
||||
),
|
||||
child: Stack(
|
||||
clipBehavior: Clip.none,
|
||||
alignment: Alignment.center,
|
||||
children: [
|
||||
widget.buildSpaceContainer(),
|
||||
if (isHovered)
|
||||
Positioned(
|
||||
bottom: 0,
|
||||
child: PlusButtonWidget(
|
||||
offset: Offset.zero,
|
||||
onButtonTap: widget.onTap,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
@ -17,7 +17,7 @@ class SpaceCell extends StatelessWidget {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return GestureDetector(
|
||||
return InkWell(
|
||||
onTap: onTap,
|
||||
child: Container(
|
||||
width: 150,
|
||||
@ -74,9 +74,8 @@ class SpaceCell extends StatelessWidget {
|
||||
borderRadius: BorderRadius.circular(15),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: ColorsManager.lightGrayColor.withValues(alpha: 0.5),
|
||||
spreadRadius: 2,
|
||||
blurRadius: 5,
|
||||
color: ColorsManager.lightGrayColor.withValues(alpha: 0.25),
|
||||
blurRadius: 7,
|
||||
offset: const Offset(0, 3),
|
||||
),
|
||||
],
|
||||
|
@ -13,11 +13,17 @@ class SpaceManagementCommunityStructure extends StatelessWidget {
|
||||
final selectionBloc = context.watch<CommunitiesTreeSelectionBloc>().state;
|
||||
final selectedCommunity = selectionBloc.selectedCommunity;
|
||||
final selectedSpace = selectionBloc.selectedSpace;
|
||||
const spacer = Spacer(flex: 10);
|
||||
const spacer = Spacer(flex: 6);
|
||||
return Visibility(
|
||||
visible: selectedCommunity!.spaces.isNotEmpty,
|
||||
replacement: const Row(
|
||||
children: [spacer, Expanded(child: CreateSpaceButton()), spacer],
|
||||
replacement: Row(
|
||||
children: [
|
||||
spacer,
|
||||
Expanded(
|
||||
child: CreateSpaceButton(communityUuid: selectedCommunity.uuid),
|
||||
),
|
||||
spacer
|
||||
],
|
||||
),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
|
@ -46,6 +46,25 @@ class SpaceModel extends Equatable {
|
||||
);
|
||||
}
|
||||
|
||||
SpaceModel copyWith({
|
||||
String? uuid,
|
||||
DateTime? createdAt,
|
||||
DateTime? updatedAt,
|
||||
String? spaceName,
|
||||
String? icon,
|
||||
List<SpaceModel>? children,
|
||||
}) {
|
||||
return SpaceModel(
|
||||
uuid: uuid ?? this.uuid,
|
||||
createdAt: createdAt ?? this.createdAt,
|
||||
updatedAt: updatedAt ?? this.updatedAt,
|
||||
spaceName: spaceName ?? this.spaceName,
|
||||
icon: icon ?? this.icon,
|
||||
children: children ?? this.children,
|
||||
parent: parent,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
List<Object?> get props => [uuid, spaceName, icon, children];
|
||||
}
|
||||
|
@ -1,17 +1,39 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:bloc/bloc.dart';
|
||||
import 'package:equatable/equatable.dart';
|
||||
import 'package:syncrow_web/pages/space_management_v2/modules/communities/domain/models/community_model.dart';
|
||||
import 'package:syncrow_web/pages/space_management_v2/modules/communities/domain/models/space_model.dart';
|
||||
import 'package:syncrow_web/pages/space_management_v2/modules/communities/presentation/bloc/communities_bloc.dart';
|
||||
|
||||
part 'communities_tree_selection_event.dart';
|
||||
part 'communities_tree_selection_state.dart';
|
||||
|
||||
class CommunitiesTreeSelectionBloc
|
||||
extends Bloc<CommunitiesTreeSelectionEvent, CommunitiesTreeSelectionState> {
|
||||
CommunitiesTreeSelectionBloc() : super(const CommunitiesTreeSelectionState()) {
|
||||
CommunitiesTreeSelectionBloc({
|
||||
required CommunitiesBloc communitiesBloc,
|
||||
}) : _communitiesBloc = communitiesBloc,
|
||||
super(const CommunitiesTreeSelectionState()) {
|
||||
on<SelectCommunityEvent>(_onSelectCommunity);
|
||||
on<SelectSpaceEvent>(_onSelectSpace);
|
||||
on<ClearCommunitiesTreeSelectionEvent>(_onClearSelection);
|
||||
on<_CommunitiesStateUpdated>(_onCommunitiesStateUpdated);
|
||||
|
||||
_communitiesSubscription = _communitiesBloc.stream.listen((communitiesState) {
|
||||
if (state.selectedCommunity != null) {
|
||||
add(_CommunitiesStateUpdated(communitiesState));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
final CommunitiesBloc _communitiesBloc;
|
||||
late final StreamSubscription<CommunitiesState> _communitiesSubscription;
|
||||
|
||||
@override
|
||||
Future<void> close() {
|
||||
_communitiesSubscription.cancel();
|
||||
return super.close();
|
||||
}
|
||||
|
||||
void _onSelectCommunity(
|
||||
@ -44,4 +66,59 @@ class CommunitiesTreeSelectionBloc
|
||||
) {
|
||||
emit(const CommunitiesTreeSelectionState());
|
||||
}
|
||||
|
||||
void _onCommunitiesStateUpdated(
|
||||
_CommunitiesStateUpdated event,
|
||||
Emitter<CommunitiesTreeSelectionState> emit,
|
||||
) {
|
||||
if (state.selectedCommunity == null) return;
|
||||
|
||||
final communities = event.communitiesState.communities;
|
||||
try {
|
||||
final updatedCommunity = communities.firstWhere(
|
||||
(c) => c.uuid == state.selectedCommunity!.uuid,
|
||||
);
|
||||
|
||||
var updatedSelectedSpace = state.selectedSpace;
|
||||
if (state.selectedSpace != null) {
|
||||
updatedSelectedSpace = _findSpaceInCommunity(
|
||||
updatedCommunity,
|
||||
state.selectedSpace!.uuid,
|
||||
);
|
||||
}
|
||||
emit(
|
||||
state.copyWith(
|
||||
selectedCommunity: updatedCommunity,
|
||||
selectedSpace: updatedSelectedSpace,
|
||||
clearSelectedSpace: updatedSelectedSpace == null,
|
||||
),
|
||||
);
|
||||
} catch (_) {
|
||||
add(const ClearCommunitiesTreeSelectionEvent());
|
||||
}
|
||||
}
|
||||
|
||||
SpaceModel? _findSpaceInCommunity(CommunityModel community, String spaceUuid) {
|
||||
try {
|
||||
return _findSpaceRecursive(community.spaces, spaceUuid);
|
||||
} catch (_) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
SpaceModel _findSpaceRecursive(List<SpaceModel> spaces, String spaceUuid) {
|
||||
for (final space in spaces) {
|
||||
if (space.uuid == spaceUuid) {
|
||||
return space;
|
||||
}
|
||||
if (space.children.isNotEmpty) {
|
||||
try {
|
||||
return _findSpaceRecursive(space.children, spaceUuid);
|
||||
} catch (_) {
|
||||
// not found in this branch
|
||||
}
|
||||
}
|
||||
}
|
||||
throw Exception('Space not found');
|
||||
}
|
||||
}
|
||||
|
@ -29,3 +29,12 @@ final class ClearCommunitiesTreeSelectionEvent
|
||||
extends CommunitiesTreeSelectionEvent {
|
||||
const ClearCommunitiesTreeSelectionEvent();
|
||||
}
|
||||
|
||||
final class _CommunitiesStateUpdated extends CommunitiesTreeSelectionEvent {
|
||||
const _CommunitiesStateUpdated(this.communitiesState);
|
||||
|
||||
final CommunitiesState communitiesState;
|
||||
|
||||
@override
|
||||
List<Object> get props => [communitiesState];
|
||||
}
|
||||
|
@ -12,18 +12,14 @@ final class CommunitiesTreeSelectionState extends Equatable {
|
||||
CommunitiesTreeSelectionState copyWith({
|
||||
CommunityModel? selectedCommunity,
|
||||
SpaceModel? selectedSpace,
|
||||
List<CommunityModel>? expandedCommunities,
|
||||
List<SpaceModel>? expandedSpaces,
|
||||
bool clearSelectedSpace = false,
|
||||
}) {
|
||||
return CommunitiesTreeSelectionState(
|
||||
selectedCommunity: selectedCommunity ?? this.selectedCommunity,
|
||||
selectedSpace: selectedSpace ?? this.selectedSpace,
|
||||
selectedSpace: clearSelectedSpace ? null : selectedSpace ?? this.selectedSpace,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
List<Object?> get props => [
|
||||
selectedCommunity,
|
||||
selectedSpace,
|
||||
];
|
||||
}
|
||||
List<Object?> get props => [selectedCommunity, selectedSpace];
|
||||
}
|
||||
|
@ -13,14 +13,14 @@ class CommunitiesTreeFailureWidget extends StatelessWidget {
|
||||
return Expanded(
|
||||
child: Center(
|
||||
child: Column(
|
||||
spacing: 16,
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Text(
|
||||
SelectableText(
|
||||
errorMessage ?? 'Something went wrong',
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
ElevatedButton(
|
||||
FilledButton(
|
||||
onPressed: () => context.read<CommunitiesBloc>().add(
|
||||
LoadCommunities(
|
||||
LoadCommunitiesParam(
|
||||
|
@ -0,0 +1,61 @@
|
||||
import 'package:dio/dio.dart';
|
||||
import 'package:syncrow_web/pages/common/bloc/project_manager.dart';
|
||||
import 'package:syncrow_web/pages/space_management_v2/modules/delete_space/domain/params/delete_space_param.dart';
|
||||
import 'package:syncrow_web/pages/space_management_v2/modules/delete_space/domain/services/delete_space_service.dart';
|
||||
import 'package:syncrow_web/services/api/api_exception.dart';
|
||||
import 'package:syncrow_web/services/api/http_service.dart';
|
||||
|
||||
final class RemoteDeleteSpaceService implements DeleteSpaceService {
|
||||
RemoteDeleteSpaceService({
|
||||
required this.httpService,
|
||||
});
|
||||
|
||||
final HTTPService httpService;
|
||||
|
||||
@override
|
||||
Future<void> delete(DeleteSpaceParam param) async {
|
||||
try {
|
||||
await httpService.delete(
|
||||
path: await _makeUrl(param),
|
||||
expectedResponseModel: (json) {
|
||||
final response = json as Map<String, dynamic>;
|
||||
final hasSuccessfullyDeletedSpace = response['success'] as bool? ?? false;
|
||||
|
||||
if (!hasSuccessfullyDeletedSpace) {
|
||||
throw APIException('Failed to delete space');
|
||||
}
|
||||
|
||||
return hasSuccessfullyDeletedSpace;
|
||||
},
|
||||
);
|
||||
} on DioException catch (e) {
|
||||
final message = e.response?.data as Map<String, dynamic>?;
|
||||
throw APIException(_getErrorMessageFromBody(message));
|
||||
} catch (e) {
|
||||
throw APIException(e.toString());
|
||||
}
|
||||
}
|
||||
|
||||
String _getErrorMessageFromBody(Map<String, dynamic>? body) {
|
||||
if (body == null) return 'Failed to delete space';
|
||||
final error = body['error'] as Map<String, dynamic>?;
|
||||
final errorMessage = error?['message'] as String? ?? '';
|
||||
return errorMessage;
|
||||
}
|
||||
|
||||
Future<String> _makeUrl(DeleteSpaceParam param) async {
|
||||
final projectUuid = await ProjectManager.getProjectUUID();
|
||||
if (projectUuid == null) {
|
||||
throw APIException('Project UUID is not set');
|
||||
}
|
||||
|
||||
if (param.communityUuid.isEmpty) {
|
||||
throw APIException('Community UUID is not set');
|
||||
}
|
||||
|
||||
if (param.spaceUuid.isEmpty) {
|
||||
throw APIException('Space UUID is not set');
|
||||
}
|
||||
return '/projects/$projectUuid/communities/${param.communityUuid}/spaces/${param.spaceUuid}';
|
||||
}
|
||||
}
|
@ -0,0 +1,9 @@
|
||||
class DeleteSpaceParam {
|
||||
const DeleteSpaceParam({
|
||||
required this.spaceUuid,
|
||||
required this.communityUuid,
|
||||
});
|
||||
|
||||
final String spaceUuid;
|
||||
final String communityUuid;
|
||||
}
|
@ -0,0 +1,5 @@
|
||||
import 'package:syncrow_web/pages/space_management_v2/modules/delete_space/domain/params/delete_space_param.dart';
|
||||
|
||||
abstract interface class DeleteSpaceService {
|
||||
Future<void> delete(DeleteSpaceParam param);
|
||||
}
|
@ -0,0 +1,31 @@
|
||||
import 'package:bloc/bloc.dart';
|
||||
import 'package:equatable/equatable.dart';
|
||||
import 'package:syncrow_web/pages/space_management_v2/modules/delete_space/domain/params/delete_space_param.dart';
|
||||
import 'package:syncrow_web/pages/space_management_v2/modules/delete_space/domain/services/delete_space_service.dart';
|
||||
import 'package:syncrow_web/services/api/api_exception.dart';
|
||||
|
||||
part 'delete_space_event.dart';
|
||||
part 'delete_space_state.dart';
|
||||
|
||||
class DeleteSpaceBloc extends Bloc<DeleteSpaceEvent, DeleteSpaceState> {
|
||||
DeleteSpaceBloc(this._deleteSpaceService) : super(DeleteSpaceInitial()) {
|
||||
on<DeleteSpace>(_onDeleteSpace);
|
||||
}
|
||||
|
||||
final DeleteSpaceService _deleteSpaceService;
|
||||
|
||||
Future<void> _onDeleteSpace(
|
||||
DeleteSpace event,
|
||||
Emitter<DeleteSpaceState> emit,
|
||||
) async {
|
||||
emit(DeleteSpaceLoading());
|
||||
try {
|
||||
await _deleteSpaceService.delete(event.param);
|
||||
emit(const DeleteSpaceSuccess('Space deleted successfully'));
|
||||
} on APIException catch (e) {
|
||||
emit(DeleteSpaceFailure(e.message));
|
||||
} catch (e) {
|
||||
emit(DeleteSpaceFailure(e.toString()));
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,17 @@
|
||||
part of 'delete_space_bloc.dart';
|
||||
|
||||
sealed class DeleteSpaceEvent extends Equatable {
|
||||
const DeleteSpaceEvent();
|
||||
|
||||
@override
|
||||
List<Object> get props => [];
|
||||
}
|
||||
|
||||
final class DeleteSpace extends DeleteSpaceEvent {
|
||||
const DeleteSpace(this.param);
|
||||
|
||||
final DeleteSpaceParam param;
|
||||
|
||||
@override
|
||||
List<Object> get props => [param];
|
||||
}
|
@ -0,0 +1,30 @@
|
||||
part of 'delete_space_bloc.dart';
|
||||
|
||||
sealed class DeleteSpaceState extends Equatable {
|
||||
const DeleteSpaceState();
|
||||
|
||||
@override
|
||||
List<Object> get props => [];
|
||||
}
|
||||
|
||||
final class DeleteSpaceInitial extends DeleteSpaceState {}
|
||||
|
||||
final class DeleteSpaceLoading extends DeleteSpaceState {}
|
||||
|
||||
final class DeleteSpaceSuccess extends DeleteSpaceState {
|
||||
const DeleteSpaceSuccess(this.successMessage);
|
||||
|
||||
final String successMessage;
|
||||
|
||||
@override
|
||||
List<Object> get props => [successMessage];
|
||||
}
|
||||
|
||||
final class DeleteSpaceFailure extends DeleteSpaceState {
|
||||
const DeleteSpaceFailure(this.errorMessage);
|
||||
|
||||
final String errorMessage;
|
||||
|
||||
@override
|
||||
List<Object> get props => [errorMessage];
|
||||
}
|
@ -0,0 +1,118 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:syncrow_web/pages/space_management_v2/modules/communities/domain/models/community_model.dart';
|
||||
import 'package:syncrow_web/pages/space_management_v2/modules/communities/domain/models/space_model.dart';
|
||||
import 'package:syncrow_web/pages/space_management_v2/modules/delete_space/data/remote_delete_space_service.dart';
|
||||
import 'package:syncrow_web/pages/space_management_v2/modules/delete_space/presentation/bloc/delete_space_bloc.dart';
|
||||
import 'package:syncrow_web/pages/space_management_v2/modules/delete_space/presentation/widgets/delete_space_dialog_form.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 DeleteSpaceDialog extends StatelessWidget {
|
||||
const DeleteSpaceDialog({
|
||||
required this.space,
|
||||
required this.community,
|
||||
required this.onSuccess,
|
||||
super.key,
|
||||
});
|
||||
|
||||
final SpaceModel space;
|
||||
final CommunityModel community;
|
||||
final void Function() onSuccess;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return BlocProvider(
|
||||
create: (context) => DeleteSpaceBloc(
|
||||
RemoteDeleteSpaceService(httpService: HTTPService()),
|
||||
),
|
||||
child: Builder(
|
||||
builder: (context) => Dialog(
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
|
||||
child: Container(
|
||||
padding: const EdgeInsetsDirectional.all(32),
|
||||
constraints: BoxConstraints(
|
||||
maxWidth: context.screenWidth * 0.2,
|
||||
),
|
||||
child: BlocConsumer<DeleteSpaceBloc, DeleteSpaceState>(
|
||||
listener: (context, state) {
|
||||
if (state case DeleteSpaceSuccess()) onSuccess();
|
||||
},
|
||||
builder: (context, state) => switch (state) {
|
||||
DeleteSpaceInitial() => DeleteSpaceDialogForm(
|
||||
space: space,
|
||||
communityUuid: community.uuid,
|
||||
),
|
||||
DeleteSpaceLoading() => const DeleteSpaceLoadingWidget(),
|
||||
DeleteSpaceSuccess(:final successMessage) => DeleteSpaceStatusWidget(
|
||||
message: successMessage,
|
||||
icon: const Icon(
|
||||
Icons.check_circle,
|
||||
size: 92,
|
||||
color: ColorsManager.goodGreen,
|
||||
),
|
||||
),
|
||||
DeleteSpaceFailure() => DeleteSpaceStatusWidget(
|
||||
message: state.errorMessage,
|
||||
icon: const Icon(
|
||||
Icons.error,
|
||||
size: 92,
|
||||
color: ColorsManager.red,
|
||||
),
|
||||
),
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class DeleteSpaceLoadingWidget extends StatelessWidget {
|
||||
const DeleteSpaceLoadingWidget({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return const SizedBox.square(
|
||||
dimension: 32,
|
||||
child: Center(child: CircularProgressIndicator()),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class DeleteSpaceStatusWidget extends StatelessWidget {
|
||||
const DeleteSpaceStatusWidget({
|
||||
required this.message,
|
||||
required this.icon,
|
||||
super.key,
|
||||
});
|
||||
|
||||
final String message;
|
||||
final Widget icon;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
spacing: 16,
|
||||
children: [
|
||||
icon,
|
||||
SelectableText(
|
||||
message,
|
||||
style: context.textTheme.bodyMedium?.copyWith(
|
||||
color: ColorsManager.blackColor,
|
||||
fontSize: 22,
|
||||
),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
FilledButton(
|
||||
onPressed: Navigator.of(context).pop,
|
||||
child: const Text('Close'),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
@ -0,0 +1,106 @@
|
||||
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/communities/domain/models/space_model.dart';
|
||||
import 'package:syncrow_web/pages/space_management_v2/modules/delete_space/domain/params/delete_space_param.dart';
|
||||
import 'package:syncrow_web/pages/space_management_v2/modules/delete_space/presentation/bloc/delete_space_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 DeleteSpaceDialogForm extends StatelessWidget {
|
||||
const DeleteSpaceDialogForm({
|
||||
required this.space,
|
||||
required this.communityUuid,
|
||||
super.key,
|
||||
});
|
||||
|
||||
final SpaceModel space;
|
||||
final String communityUuid;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
SvgPicture.asset(Assets.xDelete, width: 36, height: 36),
|
||||
const SizedBox(height: 16),
|
||||
SelectableText(
|
||||
'Delete Space',
|
||||
textAlign: TextAlign.center,
|
||||
style: context.textTheme.titleLarge?.copyWith(
|
||||
color: ColorsManager.blackColor,
|
||||
fontWeight: FontWeight.w400,
|
||||
fontSize: 24,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
SelectableText(
|
||||
'Are you sure you want to delete this space? This action is irreversible',
|
||||
textAlign: TextAlign.center,
|
||||
style: context.textTheme.bodyLarge?.copyWith(
|
||||
color: ColorsManager.lightGreyColor,
|
||||
fontWeight: FontWeight.w400,
|
||||
fontSize: 14,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: FilledButton(
|
||||
style: _buildButtonStyle(
|
||||
context,
|
||||
color: ColorsManager.grey25,
|
||||
textColor: ColorsManager.blackColor,
|
||||
),
|
||||
onPressed: Navigator.of(context).pop,
|
||||
child: const Text('Cancel'),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 16),
|
||||
Expanded(
|
||||
child: FilledButton(
|
||||
style: _buildButtonStyle(
|
||||
context,
|
||||
color: ColorsManager.semiTransparentRed,
|
||||
textColor: ColorsManager.whiteColors,
|
||||
),
|
||||
onPressed: () {
|
||||
context.read<DeleteSpaceBloc>().add(
|
||||
DeleteSpace(
|
||||
DeleteSpaceParam(
|
||||
spaceUuid: space.uuid,
|
||||
communityUuid: communityUuid,
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
child: const Text('Delete'),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
ButtonStyle _buildButtonStyle(
|
||||
BuildContext context, {
|
||||
required Color color,
|
||||
required Color textColor,
|
||||
}) {
|
||||
return FilledButton.styleFrom(
|
||||
padding: const EdgeInsets.symmetric(vertical: 12),
|
||||
backgroundColor: color,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
foregroundColor: textColor,
|
||||
textStyle: context.textTheme.bodyLarge?.copyWith(
|
||||
fontWeight: FontWeight.w400,
|
||||
fontSize: 14,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
@ -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];
|
||||
}
|
||||
|
@ -2,6 +2,7 @@ import 'package:equatable/equatable.dart';
|
||||
import 'package:syncrow_web/pages/space_management_v2/modules/products/domain/models/product.dart';
|
||||
import 'package:syncrow_web/pages/space_management_v2/modules/tags/domain/models/tag.dart';
|
||||
import 'package:syncrow_web/utils/constants/assets.dart';
|
||||
import 'package:uuid/uuid.dart';
|
||||
|
||||
class SpaceDetailsModel extends Equatable {
|
||||
final String uuid;
|
||||
@ -21,7 +22,7 @@ class SpaceDetailsModel extends Equatable {
|
||||
factory SpaceDetailsModel.empty() => const SpaceDetailsModel(
|
||||
uuid: '',
|
||||
spaceName: '',
|
||||
icon: Assets.villa,
|
||||
icon: Assets.location,
|
||||
productAllocations: [],
|
||||
subspaces: [],
|
||||
);
|
||||
@ -31,24 +32,14 @@ class SpaceDetailsModel extends Equatable {
|
||||
spaceName: json['spaceName'] as String,
|
||||
icon: json['icon'] as String,
|
||||
productAllocations: (json['productAllocations'] as List)
|
||||
.map((e) => ProductAllocation.fromJson(e as Map<String, dynamic>))
|
||||
.toList(),
|
||||
.map((e) => ProductAllocation.fromJson(e as Map<String, dynamic>))
|
||||
.toList(),
|
||||
subspaces: (json['subspaces'] as List)
|
||||
.map((e) => Subspace.fromJson(e as Map<String, dynamic>))
|
||||
.toList(),
|
||||
);
|
||||
}
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
return {
|
||||
'uuid': uuid,
|
||||
'spaceName': spaceName,
|
||||
'icon': icon,
|
||||
'productAllocations': productAllocations.map((e) => e.toJson()).toList(),
|
||||
'subspaces': subspaces.map((e) => e.toJson()).toList(),
|
||||
};
|
||||
}
|
||||
|
||||
SpaceDetailsModel copyWith({
|
||||
String? uuid,
|
||||
String? spaceName,
|
||||
@ -70,40 +61,38 @@ class SpaceDetailsModel extends Equatable {
|
||||
}
|
||||
|
||||
class ProductAllocation extends Equatable {
|
||||
final String uuid;
|
||||
final Product product;
|
||||
final Tag tag;
|
||||
|
||||
const ProductAllocation({
|
||||
required this.uuid,
|
||||
required this.product,
|
||||
required this.tag,
|
||||
});
|
||||
|
||||
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>),
|
||||
);
|
||||
}
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
return {
|
||||
'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 {
|
||||
@ -127,14 +116,6 @@ class Subspace extends Equatable {
|
||||
);
|
||||
}
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
return {
|
||||
'uuid': uuid,
|
||||
'name': name,
|
||||
'productAllocations': productAllocations.map((e) => e.toJson()).toList(),
|
||||
};
|
||||
}
|
||||
|
||||
Subspace copyWith({
|
||||
String? uuid,
|
||||
String? name,
|
||||
|
@ -2,23 +2,37 @@ 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/domain/models/space_details_model.dart';
|
||||
import 'package:syncrow_web/pages/space_management_v2/modules/space_details/presentation/bloc/space_details_bloc.dart';
|
||||
import 'package:syncrow_web/pages/space_management_v2/modules/space_details/presentation/widgets/space_details_dialog.dart';
|
||||
import 'package:syncrow_web/pages/space_management_v2/modules/update_space/data/services/remote_update_space_service.dart';
|
||||
import 'package:syncrow_web/pages/space_management_v2/modules/update_space/domain/params/update_space_param.dart';
|
||||
import 'package:syncrow_web/pages/space_management_v2/modules/update_space/presentation/bloc/update_space_bloc.dart';
|
||||
import 'package:syncrow_web/services/api/http_service.dart';
|
||||
|
||||
abstract final class SpaceDetailsDialogHelper {
|
||||
static void showCreate(BuildContext context) {
|
||||
static void showCreate(
|
||||
BuildContext context, {
|
||||
required String communityUuid,
|
||||
}) {
|
||||
showDialog<void>(
|
||||
context: context,
|
||||
builder: (_) => BlocProvider(
|
||||
create: (context) => SpaceDetailsBloc(
|
||||
RemoteSpaceDetailsService(httpService: HTTPService()),
|
||||
),
|
||||
child: SpaceDetailsDialog(
|
||||
context: context,
|
||||
title: const SelectableText('Create Space'),
|
||||
spaceModel: SpaceModel.empty(),
|
||||
onSave: print,
|
||||
builder: (_) => MultiBlocProvider(
|
||||
providers: [
|
||||
BlocProvider(
|
||||
create: (context) => SpaceDetailsBloc(
|
||||
RemoteSpaceDetailsService(httpService: HTTPService()),
|
||||
),
|
||||
),
|
||||
],
|
||||
child: Builder(
|
||||
builder: (context) => SpaceDetailsDialog(
|
||||
context: context,
|
||||
title: const SelectableText('Create Space'),
|
||||
spaceModel: SpaceModel.empty(),
|
||||
onSave: (space) {},
|
||||
communityUuid: communityUuid,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
@ -27,20 +41,98 @@ abstract final class SpaceDetailsDialogHelper {
|
||||
static void showEdit(
|
||||
BuildContext context, {
|
||||
required SpaceModel spaceModel,
|
||||
required String communityUuid,
|
||||
required void Function(SpaceDetailsModel updatedSpaceDetails)? onSuccess,
|
||||
}) {
|
||||
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) {},
|
||||
builder: (_) => MultiBlocProvider(
|
||||
providers: [
|
||||
BlocProvider(
|
||||
create: (context) => SpaceDetailsBloc(
|
||||
RemoteSpaceDetailsService(httpService: HTTPService()),
|
||||
),
|
||||
),
|
||||
BlocProvider(
|
||||
create: (context) => UpdateSpaceBloc(
|
||||
RemoteUpdateSpaceService(HTTPService()),
|
||||
),
|
||||
),
|
||||
],
|
||||
child: Builder(
|
||||
builder: (context) => BlocListener<UpdateSpaceBloc, UpdateSpaceState>(
|
||||
listener: (context, state) => _updateListener(
|
||||
context,
|
||||
state,
|
||||
onSuccess,
|
||||
),
|
||||
child: SpaceDetailsDialog(
|
||||
context: context,
|
||||
title: const SelectableText('Edit Space'),
|
||||
spaceModel: spaceModel,
|
||||
onSave: (space) => context.read<UpdateSpaceBloc>().add(
|
||||
UpdateSpace(
|
||||
UpdateSpaceParam(
|
||||
communityUuid: communityUuid,
|
||||
space: space,
|
||||
),
|
||||
),
|
||||
),
|
||||
communityUuid: communityUuid,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
static void _updateListener(
|
||||
BuildContext context,
|
||||
UpdateSpaceState state,
|
||||
void Function(SpaceDetailsModel updatedSpaceDetails)? onSuccess,
|
||||
) {
|
||||
return switch (state) {
|
||||
UpdateSpaceInitial() => null,
|
||||
UpdateSpaceLoading() => _onLoading(context),
|
||||
UpdateSpaceSuccess(:final space) =>
|
||||
_onUpdateSuccess(context, space, onSuccess),
|
||||
UpdateSpaceFailure(:final errorMessage) => _onError(context, errorMessage),
|
||||
};
|
||||
}
|
||||
|
||||
static void _onUpdateSuccess(
|
||||
BuildContext context,
|
||||
SpaceDetailsModel space,
|
||||
void Function(SpaceDetailsModel updatedSpaceDetails)? onSuccess,
|
||||
) {
|
||||
Navigator.of(context).pop();
|
||||
Navigator.of(context).pop();
|
||||
onSuccess?.call(space);
|
||||
}
|
||||
|
||||
static void _onLoading(BuildContext context) {
|
||||
showDialog<void>(
|
||||
context: context,
|
||||
barrierDismissible: false,
|
||||
builder: (_) => const Center(child: CircularProgressIndicator()),
|
||||
);
|
||||
}
|
||||
|
||||
static void _onError(BuildContext context, String errorMessage) {
|
||||
Navigator.of(context).pop();
|
||||
showDialog<void>(
|
||||
context: context,
|
||||
barrierDismissible: false,
|
||||
builder: (_) => AlertDialog(
|
||||
title: const Text('Error'),
|
||||
content: Text(errorMessage),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: Navigator.of(context).pop,
|
||||
child: const Text('OK'),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -8,10 +8,14 @@ class SpaceDetailsActionButtons extends StatelessWidget {
|
||||
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) {
|
||||
@ -27,10 +31,7 @@ class SpaceDetailsActionButtons extends StatelessWidget {
|
||||
}
|
||||
|
||||
Widget _buildCancelButton(BuildContext context) {
|
||||
return CancelButton(
|
||||
onPressed: onCancel,
|
||||
label: 'Cancel',
|
||||
);
|
||||
return CancelButton(onPressed: onCancel, label: cancelButtonLabel);
|
||||
}
|
||||
|
||||
Widget _buildSaveButton() {
|
||||
@ -39,7 +40,7 @@ class SpaceDetailsActionButtons extends StatelessWidget {
|
||||
borderRadius: 10,
|
||||
backgroundColor: ColorsManager.secondaryColor,
|
||||
foregroundColor: ColorsManager.whiteColors,
|
||||
child: const Text('OK'),
|
||||
child: Text(saveButtonLabel),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -1,9 +1,15 @@
|
||||
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({
|
||||
@ -15,11 +21,18 @@ class SpaceDetailsDevicesBox extends StatelessWidget {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final productAllocations = space.productAllocations;
|
||||
final subspaces = space.subspaces;
|
||||
final isAnySubspaceHasProductAllocations =
|
||||
subspaces.any((subspace) => subspace.productAllocations.isNotEmpty);
|
||||
if (productAllocations.isNotEmpty || isAnySubspaceHasProductAllocations) {
|
||||
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),
|
||||
@ -35,46 +48,40 @@ class SpaceDetailsDevicesBox extends StatelessWidget {
|
||||
spacing: 8.0,
|
||||
runSpacing: 8.0,
|
||||
children: [
|
||||
// Combine tags from spaceModel and subspaces
|
||||
// ...TagHelper.groupTags([
|
||||
// ...?tags,
|
||||
// ...?subspaces?.expand((subspace) => subspace.tags ?? [])
|
||||
// ]).entries.map(
|
||||
// (entry) => Chip(
|
||||
// avatar: SizedBox(
|
||||
// width: 24,
|
||||
// height: 24,
|
||||
// child: SvgPicture.asset(
|
||||
// entry.key.icon ?? 'assets/icons/gateway.svg',
|
||||
// fit: BoxFit.contain,
|
||||
// ),
|
||||
// ),
|
||||
// label: Text(
|
||||
// 'x${entry.value}',
|
||||
// style: Theme.of(context)
|
||||
// .textTheme
|
||||
// .bodySmall
|
||||
// ?.copyWith(color: ColorsManager.spaceColor),
|
||||
// ),
|
||||
// backgroundColor: ColorsManager.whiteColors,
|
||||
// shape: RoundedRectangleBorder(
|
||||
// borderRadius: BorderRadius.circular(16),
|
||||
// side: const BorderSide(
|
||||
// color: ColorsManager.spaceColor,
|
||||
// ),
|
||||
// ),
|
||||
// ),
|
||||
// ),
|
||||
|
||||
EditChip(
|
||||
onTap: () {},
|
||||
),
|
||||
...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: () {},
|
||||
onPressed: () => _showAssignTagsDialog(context),
|
||||
style: TextButton.styleFrom(
|
||||
padding: EdgeInsets.zero,
|
||||
),
|
||||
@ -83,10 +90,50 @@ class SpaceDetailsDevicesBox extends StatelessWidget {
|
||||
child: ButtonContentWidget(
|
||||
svgAssets: Assets.addIcon,
|
||||
label: 'Add Devices',
|
||||
// disabled: isTagsAndSubspaceModelDisabled,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
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,7 +1,6 @@
|
||||
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';
|
||||
@ -15,6 +14,7 @@ class SpaceDetailsDialog extends StatefulWidget {
|
||||
required this.spaceModel,
|
||||
required this.onSave,
|
||||
required this.context,
|
||||
required this.communityUuid,
|
||||
super.key,
|
||||
});
|
||||
|
||||
@ -22,6 +22,7 @@ class SpaceDetailsDialog extends StatefulWidget {
|
||||
final SpaceModel spaceModel;
|
||||
final void Function(SpaceDetailsModel space) onSave;
|
||||
final BuildContext context;
|
||||
final String communityUuid;
|
||||
|
||||
@override
|
||||
State<SpaceDetailsDialog> createState() => _SpaceDetailsDialogState();
|
||||
@ -35,11 +36,7 @@ class _SpaceDetailsDialogState extends State<SpaceDetailsDialog> {
|
||||
if (!isCreateMode) {
|
||||
final param = LoadSpaceDetailsParam(
|
||||
spaceUuid: widget.spaceModel.uuid,
|
||||
communityUuid: widget.context
|
||||
.read<CommunitiesTreeSelectionBloc>()
|
||||
.state
|
||||
.selectedCommunity!
|
||||
.uuid,
|
||||
communityUuid: widget.communityUuid,
|
||||
);
|
||||
widget.context.read<SpaceDetailsBloc>().add(LoadSpaceDetails(param));
|
||||
}
|
||||
|
@ -42,9 +42,8 @@ class SpaceDetailsForm extends StatelessWidget {
|
||||
Expanded(child: SpaceIconPicker(iconPath: space.icon)),
|
||||
Expanded(
|
||||
flex: 2,
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
child: ListView(
|
||||
shrinkWrap: true,
|
||||
children: [
|
||||
SpaceNameTextField(
|
||||
initialValue: space.spaceName,
|
||||
@ -52,7 +51,7 @@ class SpaceDetailsForm extends StatelessWidget {
|
||||
(subspace) => subspace.name == value,
|
||||
),
|
||||
),
|
||||
const Spacer(),
|
||||
const SizedBox(height: 32),
|
||||
SpaceSubSpacesBox(
|
||||
subspaces: space.subspaces,
|
||||
),
|
||||
|
@ -37,7 +37,7 @@ class _SpaceSubSpacesDialogState extends State<SpaceSubSpacesDialog> {
|
||||
..._subspaces,
|
||||
Subspace(
|
||||
name: name,
|
||||
uuid: const Uuid().v4(),
|
||||
uuid: '${const Uuid().v4()}-NewTag',
|
||||
productAllocations: const [],
|
||||
),
|
||||
];
|
||||
|
@ -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';
|
||||
}
|
||||
}
|
||||
|
@ -3,34 +3,19 @@ import 'package:equatable/equatable.dart';
|
||||
class Tag extends Equatable {
|
||||
final String uuid;
|
||||
final String name;
|
||||
final String createdAt;
|
||||
final String updatedAt;
|
||||
|
||||
const Tag({
|
||||
required this.uuid,
|
||||
required this.name,
|
||||
required this.createdAt,
|
||||
required this.updatedAt,
|
||||
});
|
||||
|
||||
factory Tag.fromJson(Map<String, dynamic> json) {
|
||||
return Tag(
|
||||
uuid: json['uuid'] as String,
|
||||
name: json['name'] as String,
|
||||
createdAt: json['createdAt'] as String,
|
||||
updatedAt: json['updatedAt'] as String,
|
||||
);
|
||||
}
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
return {
|
||||
'uuid': uuid,
|
||||
'name': name,
|
||||
'createdAt': createdAt,
|
||||
'updatedAt': updatedAt,
|
||||
};
|
||||
}
|
||||
|
||||
@override
|
||||
List<Object?> get props => [uuid, name, createdAt, updatedAt];
|
||||
List<Object?> get props => [uuid, name];
|
||||
}
|
||||
|
@ -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,234 @@
|
||||
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()}-NewProductUuid',
|
||||
product: product,
|
||||
tag: Tag(
|
||||
uuid: '${const Uuid().v4()}-NewTag',
|
||||
name: '',
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
});
|
||||
_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(),
|
||||
};
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
@ -0,0 +1,182 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:syncrow_web/pages/space_management_v2/modules/tags/domain/models/tag.dart';
|
||||
import 'package:syncrow_web/utils/color_manager.dart';
|
||||
import 'package:syncrow_web/utils/extension/build_context_x.dart';
|
||||
import 'package:uuid/uuid.dart';
|
||||
|
||||
class ProductTagField extends StatefulWidget {
|
||||
final List<Tag> items;
|
||||
final ValueChanged<Tag> onSelected;
|
||||
final Tag? initialValue;
|
||||
final String productName;
|
||||
|
||||
const ProductTagField({
|
||||
super.key,
|
||||
required this.items,
|
||||
required this.onSelected,
|
||||
this.initialValue,
|
||||
required this.productName,
|
||||
});
|
||||
|
||||
@override
|
||||
State<ProductTagField> createState() => _ProductTagFieldState();
|
||||
}
|
||||
|
||||
class _ProductTagFieldState extends State<ProductTagField> {
|
||||
bool _isOpen = false;
|
||||
OverlayEntry? _overlayEntry;
|
||||
final TextEditingController _controller = TextEditingController();
|
||||
final FocusNode _focusNode = FocusNode();
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_controller.text = widget.initialValue?.name ?? '';
|
||||
_focusNode.addListener(_handleFocusChange);
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_focusNode.removeListener(_handleFocusChange);
|
||||
_controller.dispose();
|
||||
_focusNode.dispose();
|
||||
_overlayEntry?.remove();
|
||||
_overlayEntry = null;
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
void _handleFocusChange() {
|
||||
if (!_focusNode.hasFocus) {
|
||||
_submit(_controller.text);
|
||||
}
|
||||
}
|
||||
|
||||
void _submit(String value) {
|
||||
final lowerCaseValue = value.toLowerCase();
|
||||
final selectedTag = widget.items.firstWhere(
|
||||
(e) => e.name.toLowerCase() == lowerCaseValue,
|
||||
orElse: () => Tag(uuid: '${const Uuid().v4()}-NewTag', name: value),
|
||||
);
|
||||
widget.onSelected(selectedTag);
|
||||
_closeDropdown();
|
||||
}
|
||||
|
||||
void _toggleDropdown() {
|
||||
if (_isOpen) {
|
||||
_closeDropdown();
|
||||
} else {
|
||||
_openDropdown();
|
||||
}
|
||||
}
|
||||
|
||||
void _openDropdown() {
|
||||
_overlayEntry = _createOverlayEntry();
|
||||
Overlay.of(context).insert(_overlayEntry!);
|
||||
setState(() => _isOpen = true);
|
||||
}
|
||||
|
||||
void _closeDropdown() {
|
||||
if (_isOpen) {
|
||||
_overlayEntry?.remove();
|
||||
_overlayEntry = null;
|
||||
setState(() => _isOpen = false);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return GestureDetector(
|
||||
onTap: () => FocusScope.of(context).unfocus(),
|
||||
behavior: HitTestBehavior.opaque,
|
||||
child: Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12.0, vertical: 8.0),
|
||||
decoration: BoxDecoration(
|
||||
border: Border.all(color: ColorsManager.transparentColor),
|
||||
borderRadius: BorderRadius.circular(8.0),
|
||||
),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Expanded(
|
||||
child: TextFormField(
|
||||
controller: _controller,
|
||||
focusNode: _focusNode,
|
||||
onFieldSubmitted: _submit,
|
||||
style: context.textTheme.bodyMedium,
|
||||
decoration: const InputDecoration(
|
||||
hintText: 'Enter or Select a tag',
|
||||
border: InputBorder.none,
|
||||
),
|
||||
),
|
||||
),
|
||||
GestureDetector(
|
||||
onTap: _toggleDropdown,
|
||||
child: const Icon(Icons.arrow_drop_down),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
OverlayEntry _createOverlayEntry() {
|
||||
final renderBox = context.findRenderObject()! as RenderBox;
|
||||
final size = renderBox.size;
|
||||
final offset = renderBox.localToGlobal(Offset.zero);
|
||||
|
||||
return OverlayEntry(
|
||||
builder: (context) {
|
||||
return GestureDetector(
|
||||
onTap: _closeDropdown,
|
||||
behavior: HitTestBehavior.translucent,
|
||||
child: Stack(
|
||||
children: [
|
||||
Positioned(
|
||||
left: offset.dx,
|
||||
top: offset.dy + size.height,
|
||||
width: size.width,
|
||||
child: Material(
|
||||
elevation: 4.0,
|
||||
child: Container(
|
||||
color: ColorsManager.whiteColors,
|
||||
constraints: const BoxConstraints(maxHeight: 200.0),
|
||||
child: ListView.builder(
|
||||
shrinkWrap: true,
|
||||
itemCount: widget.items.length,
|
||||
itemBuilder: (context, index) {
|
||||
final tag = widget.items[index];
|
||||
return Container(
|
||||
decoration: const BoxDecoration(
|
||||
border: Border(
|
||||
bottom: BorderSide(
|
||||
color: ColorsManager.lightGrayBorderColor,
|
||||
width: 1.0,
|
||||
),
|
||||
),
|
||||
),
|
||||
child: ListTile(
|
||||
title: Text(
|
||||
tag.name,
|
||||
style: context.textTheme.bodyMedium?.copyWith(
|
||||
color: ColorsManager.textPrimaryColor,
|
||||
),
|
||||
),
|
||||
onTap: () {
|
||||
_controller.text = tag.name;
|
||||
_submit(tag.name);
|
||||
_closeDropdown();
|
||||
},
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
@ -0,0 +1,67 @@
|
||||
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/tags/presentation/widgets/product_type_card_counter.dart';
|
||||
import 'package:syncrow_web/pages/spaces_management/tag_model/widgets/device_icon_widget.dart';
|
||||
import 'package:syncrow_web/utils/color_manager.dart';
|
||||
import 'package:syncrow_web/utils/extension/build_context_x.dart';
|
||||
|
||||
class ProductTypeCard extends StatelessWidget {
|
||||
const ProductTypeCard({
|
||||
required this.product,
|
||||
required this.count,
|
||||
required this.onIncrement,
|
||||
required this.onDecrement,
|
||||
super.key,
|
||||
});
|
||||
|
||||
final Product product;
|
||||
final int count;
|
||||
final void Function() onIncrement;
|
||||
final void Function() onDecrement;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Card(
|
||||
elevation: 2,
|
||||
color: ColorsManager.whiteColors,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(4),
|
||||
child: Column(
|
||||
spacing: 8,
|
||||
mainAxisAlignment: MainAxisAlignment.start,
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
Expanded(child: DeviceIconWidget(icon: product.icon)),
|
||||
_buildName(context, product.name),
|
||||
ProductTypeCardCounter(
|
||||
onIncrement: onIncrement,
|
||||
onDecrement: onDecrement,
|
||||
count: count,
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildName(BuildContext context, String name) {
|
||||
return Expanded(
|
||||
child: SizedBox(
|
||||
height: 35,
|
||||
child: Text(
|
||||
name,
|
||||
style: context.textTheme.bodySmall?.copyWith(
|
||||
color: ColorsManager.blackColor,
|
||||
),
|
||||
textAlign: TextAlign.center,
|
||||
maxLines: 2,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
@ -0,0 +1,58 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:syncrow_web/utils/color_manager.dart';
|
||||
import 'package:syncrow_web/utils/extension/build_context_x.dart';
|
||||
|
||||
class ProductTypeCardCounter extends StatelessWidget {
|
||||
const ProductTypeCardCounter({
|
||||
super.key,
|
||||
required this.onIncrement,
|
||||
required this.onDecrement,
|
||||
required this.count,
|
||||
});
|
||||
|
||||
final int count;
|
||||
final void Function() onIncrement;
|
||||
final void Function() onDecrement;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 5),
|
||||
decoration: BoxDecoration(
|
||||
color: ColorsManager.counterBackgroundColor,
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
),
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
spacing: 8,
|
||||
children: [
|
||||
_buildCounterButton(
|
||||
Icons.remove,
|
||||
onDecrement,
|
||||
),
|
||||
Text(
|
||||
count.toString(),
|
||||
style: context.textTheme.bodyLarge?.copyWith(
|
||||
color: ColorsManager.spaceColor,
|
||||
),
|
||||
),
|
||||
_buildCounterButton(Icons.add, onIncrement),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildCounterButton(
|
||||
IconData icon,
|
||||
VoidCallback onPressed,
|
||||
) {
|
||||
return GestureDetector(
|
||||
onTap: onPressed,
|
||||
child: Icon(
|
||||
icon,
|
||||
color: ColorsManager.spaceColor.withValues(alpha: 0.3),
|
||||
size: 18,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
@ -0,0 +1,62 @@
|
||||
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/tags/presentation/widgets/product_type_card.dart';
|
||||
import 'package:syncrow_web/utils/color_manager.dart';
|
||||
import 'package:syncrow_web/utils/extension/build_context_x.dart';
|
||||
|
||||
class ProductsGrid extends StatelessWidget {
|
||||
const ProductsGrid({
|
||||
required this.products,
|
||||
required this.selectedProducts,
|
||||
required this.onIncrement,
|
||||
required this.onDecrement,
|
||||
super.key,
|
||||
});
|
||||
|
||||
final List<Product> products;
|
||||
final Map<Product, int> selectedProducts;
|
||||
final void Function(Product) onIncrement;
|
||||
final void Function(Product) onDecrement;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final size = MediaQuery.of(context).size;
|
||||
final crossAxisCount = switch (context.screenWidth) {
|
||||
> 1200 => 8,
|
||||
> 800 => 5,
|
||||
_ => 3,
|
||||
};
|
||||
return SingleChildScrollView(
|
||||
child: Container(
|
||||
width: size.width * 0.9,
|
||||
height: size.height * 0.65,
|
||||
decoration: BoxDecoration(
|
||||
color: ColorsManager.textFieldGreyColor,
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
child: GridView.builder(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 20,
|
||||
),
|
||||
shrinkWrap: true,
|
||||
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
|
||||
crossAxisCount: crossAxisCount,
|
||||
mainAxisSpacing: 6,
|
||||
crossAxisSpacing: 4,
|
||||
childAspectRatio: 0.8,
|
||||
),
|
||||
itemCount: products.length,
|
||||
itemBuilder: (context, index) {
|
||||
final product = products[index];
|
||||
return ProductTypeCard(
|
||||
product: product,
|
||||
count: selectedProducts[product] ?? 0,
|
||||
onIncrement: () => onIncrement(product),
|
||||
onDecrement: () => onDecrement(product),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user