mirror of
https://github.com/SyncrowIOT/web.git
synced 2025-07-14 17:25:50 +00:00
Compare commits
127 Commits
analytics-
...
dev
Author | SHA1 | Date | |
---|---|---|---|
5532935a3a | |||
249cbfc242 | |||
8167926620 | |||
559091faa0 | |||
eba351c9be | |||
c112cde634 | |||
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 | |||
46a7add90d | |||
73de1e6ff9 | |||
826dea8054 | |||
fdea4b1cd0 | |||
823d86fd80 | |||
dd735032ea | |||
6dcc851d97 | |||
15b36fd052 | |||
a4024067c7 | |||
95cded4bf5 | |||
757a96ed9f | |||
b857736e10 | |||
1fccd51440 | |||
c07ddb0ccd | |||
58e99f95b2 | |||
227df6fe3d | |||
9451ec0cc4 | |||
fc797c2646 | |||
318e1d9af7 | |||
d47dc349bc | |||
c221c8499f | |||
71cf4b9feb | |||
c43cf9347f | |||
9990b1805e | |||
50f8158830 | |||
009b7c0316 | |||
72af55ef98 | |||
779c0fe916 | |||
e448eabda6 | |||
9dfb3ed369 | |||
63353af38b | |||
68b6c9b18c | |||
fa6ee9a0af | |||
3601b02bc3 | |||
fdd0526c78 | |||
b888f516e2 | |||
bdeec7d325 | |||
50ff17a0c1 | |||
87c2e3261d | |||
62a6f9c993 | |||
c1e61ee61d | |||
7750290be4 | |||
f7e4d6ff07 | |||
7f26c773a7 | |||
1adbae6735 | |||
ede2da6632 | |||
b06e4bd2ba | |||
0847cb8a41 | |||
818bdee745 | |||
0a022d8a8d | |||
f33b3e8bd2 | |||
8f0eb88567 | |||
19739c6e4d | |||
9f86b8d638 | |||
95907661d2 | |||
9c9b7d99dc | |||
037895844a | |||
c07bae5cbc | |||
e6fe9f35b0 | |||
8cb6c13cd5 | |||
949c27938a | |||
4c582b865d | |||
d7467adeda | |||
5486f0832d | |||
fd239a3907 | |||
e2d6f5eea8 | |||
289922071a | |||
8594168548 | |||
bd9a74b380 | |||
15ee79688d | |||
e5e88385e9 | |||
62d5bbce7e |
@ -1,2 +1,3 @@
|
|||||||
ENV_NAME=development
|
ENV_NAME=development
|
||||||
BASE_URL=https://syncrow-dev.azurewebsites.net
|
BASE_URL=https://syncrow-dev.azurewebsites.net
|
||||||
|
RTDB_URL=https://syncrow-dev-79446.asia-southeast1.firebasedatabase.app/
|
||||||
|
@ -1,2 +1,3 @@
|
|||||||
ENV_NAME=production
|
ENV_NAME=production
|
||||||
BASE_URL=https://syncrow-staging.azurewebsites.net
|
BASE_URL=https://syncrow-staging.azurewebsites.net
|
||||||
|
RTDB_URL=https://syncrow-prod-79446.asia-southeast1.firebasedatabase.app/
|
||||||
|
@ -1,2 +1,3 @@
|
|||||||
ENV_NAME=staging
|
ENV_NAME=staging
|
||||||
BASE_URL=https://syncrow-staging.azurewebsites.net
|
BASE_URL=https://syncrow-staging.azurewebsites.net
|
||||||
|
RTDB_URL=https://syncrow-staging-79446.asia-southeast1.firebasedatabase.app/
|
||||||
|
34
.vscode/launch.json
vendored
34
.vscode/launch.json
vendored
@ -1,14 +1,9 @@
|
|||||||
{
|
{
|
||||||
"configurations": [
|
"configurations": [
|
||||||
|
|
||||||
{
|
{
|
||||||
|
|
||||||
"name": "DEVELOPMENT",
|
"name": "DEVELOPMENT",
|
||||||
|
|
||||||
"request": "launch",
|
"request": "launch",
|
||||||
|
|
||||||
"type": "dart",
|
"type": "dart",
|
||||||
|
|
||||||
"args": [
|
"args": [
|
||||||
"-d",
|
"-d",
|
||||||
"chrome",
|
"chrome",
|
||||||
@ -16,19 +11,14 @@
|
|||||||
"3000",
|
"3000",
|
||||||
"-t",
|
"-t",
|
||||||
"lib/main_dev.dart",
|
"lib/main_dev.dart",
|
||||||
"--web-experimental-hot-reload",
|
"--web-experimental-hot-reload"
|
||||||
],
|
],
|
||||||
|
|
||||||
"flutterMode": "debug"
|
"flutterMode": "debug"
|
||||||
|
},
|
||||||
},{
|
{
|
||||||
|
|
||||||
"name": "STAGING",
|
"name": "STAGING",
|
||||||
|
|
||||||
"request": "launch",
|
"request": "launch",
|
||||||
|
|
||||||
"type": "dart",
|
"type": "dart",
|
||||||
|
|
||||||
"args": [
|
"args": [
|
||||||
"-d",
|
"-d",
|
||||||
"chrome",
|
"chrome",
|
||||||
@ -36,19 +26,14 @@
|
|||||||
"3000",
|
"3000",
|
||||||
"-t",
|
"-t",
|
||||||
"lib/main_staging.dart",
|
"lib/main_staging.dart",
|
||||||
"--web-experimental-hot-reload",
|
"--web-experimental-hot-reload"
|
||||||
],
|
],
|
||||||
|
|
||||||
"flutterMode": "debug"
|
"flutterMode": "debug"
|
||||||
|
},
|
||||||
},{
|
{
|
||||||
|
|
||||||
"name": "PRODUCTION",
|
"name": "PRODUCTION",
|
||||||
|
|
||||||
"request": "launch",
|
"request": "launch",
|
||||||
|
|
||||||
"type": "dart",
|
"type": "dart",
|
||||||
|
|
||||||
"args": [
|
"args": [
|
||||||
"-d",
|
"-d",
|
||||||
"chrome",
|
"chrome",
|
||||||
@ -56,12 +41,9 @@
|
|||||||
"3000",
|
"3000",
|
||||||
"-t",
|
"-t",
|
||||||
"lib/main.dart",
|
"lib/main.dart",
|
||||||
"--web-experimental-hot-reload",
|
"--web-experimental-hot-reload"
|
||||||
],
|
],
|
||||||
|
|
||||||
"flutterMode": "debug"
|
"flutterMode": "debug"
|
||||||
|
}
|
||||||
},
|
|
||||||
|
|
||||||
]
|
]
|
||||||
}
|
}
|
15
assets/icons/group_icon.svg
Normal file
15
assets/icons/group_icon.svg
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<g clip-path="url(#clip0_9717_7433)">
|
||||||
|
<path d="M17.1131 10.6766H15.5664C15.7241 11.1083 15.8102 11.5741 15.8102 12.0596V17.9053C15.8102 18.1077 15.775 18.302 15.7109 18.4827H18.2679C19.2231 18.4827 20.0002 17.7056 20.0002 16.7505V13.5637C20.0002 11.9718 18.7051 10.6766 17.1131 10.6766Z" fill="#023DFE" fill-opacity="0.7"/>
|
||||||
|
<path d="M4.19005 12.0596C4.19005 11.5741 4.27618 11.1083 4.43384 10.6766H2.88712C1.29516 10.6766 0 11.9718 0 13.5637V16.7505C0 17.7057 0.777072 18.4828 1.73227 18.4828H4.28938C4.22528 18.302 4.19005 18.1077 4.19005 17.9053V12.0596Z" fill="#023DFE" fill-opacity="0.7"/>
|
||||||
|
<path d="M11.7679 9.17249H8.23184C6.63989 9.17249 5.34473 10.4676 5.34473 12.0596V17.9053C5.34473 18.2242 5.60324 18.4827 5.92215 18.4827H14.0776C14.3965 18.4827 14.655 18.2242 14.655 17.9053V12.0596C14.655 10.4676 13.3598 9.17249 11.7679 9.17249Z" fill="#023DFE" fill-opacity="0.7"/>
|
||||||
|
<path d="M9.99995 1.51721C8.08541 1.51721 6.52783 3.07479 6.52783 4.98937C6.52783 6.288 7.24459 7.42218 8.30311 8.01765C8.80518 8.30008 9.38401 8.46148 9.99995 8.46148C10.6159 8.46148 11.1947 8.30008 11.6968 8.01765C12.7553 7.42218 13.4721 6.28796 13.4721 4.98937C13.4721 3.07483 11.9145 1.51721 9.99995 1.51721Z" fill="#023DFE" fill-opacity="0.7"/>
|
||||||
|
<path d="M3.90284 4.75354C2.471 4.75354 1.30615 5.91839 1.30615 7.35022C1.30615 8.78206 2.471 9.94691 3.90284 9.94691C4.26604 9.94691 4.6119 9.87168 4.92608 9.73644C5.46929 9.50257 5.91718 9.08859 6.19433 8.57003C6.38886 8.20609 6.49952 7.79089 6.49952 7.35022C6.49952 5.91843 5.33468 4.75354 3.90284 4.75354Z" fill="#023DFE" fill-opacity="0.7"/>
|
||||||
|
<path d="M16.0972 4.75354C14.6653 4.75354 13.5005 5.91839 13.5005 7.35022C13.5005 7.79093 13.6112 8.20612 13.8057 8.57003C14.0828 9.08863 14.5307 9.50261 15.0739 9.73644C15.3881 9.87168 15.734 9.94691 16.0972 9.94691C17.529 9.94691 18.6939 8.78206 18.6939 7.35022C18.6939 5.91839 17.529 4.75354 16.0972 4.75354Z" fill="#023DFE" fill-opacity="0.7"/>
|
||||||
|
</g>
|
||||||
|
<defs>
|
||||||
|
<clipPath id="clip0_9717_7433">
|
||||||
|
<rect width="20" height="20" fill="white"/>
|
||||||
|
</clipPath>
|
||||||
|
</defs>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 2.1 KiB |
4
assets/icons/home_icon.svg
Normal file
4
assets/icons/home_icon.svg
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path d="M10.0002 5.97498L3.12109 11.2683V18.3601H8.64871V13.163H11.5852V18.3601H16.8794V11.2683L10.0002 5.97498Z" fill="#023DFE" fill-opacity="0.7"/>
|
||||||
|
<path d="M17.1673 7.15356V3.52759H14.2702V4.92485L10 1.63989L0 9.33274L1.38043 11.1271L10 4.49458L18.6196 11.1272L20 9.33278L17.1673 7.15356Z" fill="#023DFE" fill-opacity="0.7"/>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 433 B |
@ -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_bloc/flutter_bloc.dart';
|
||||||
import 'package:flutter_dotenv/flutter_dotenv.dart';
|
import 'package:flutter_dotenv/flutter_dotenv.dart';
|
||||||
import 'package:go_router/go_router.dart';
|
import 'package:go_router/go_router.dart';
|
||||||
import 'package:syncrow_web/firebase_options_prod.dart';
|
import 'package:syncrow_web/firebase_options.dart';
|
||||||
import 'package:syncrow_web/pages/auth/bloc/auth_bloc.dart';
|
import 'package:syncrow_web/pages/auth/bloc/auth_bloc.dart';
|
||||||
import 'package:syncrow_web/pages/home/bloc/home_bloc.dart';
|
import 'package:syncrow_web/pages/home/bloc/home_bloc.dart';
|
||||||
import 'package:syncrow_web/pages/home/bloc/home_event.dart';
|
import 'package:syncrow_web/pages/home/bloc/home_event.dart';
|
||||||
@ -27,7 +27,9 @@ Future<void> main() async {
|
|||||||
await dotenv.load(fileName: '.env.$environment');
|
await dotenv.load(fileName: '.env.$environment');
|
||||||
WidgetsFlutterBinding.ensureInitialized();
|
WidgetsFlutterBinding.ensureInitialized();
|
||||||
await Firebase.initializeApp(
|
await Firebase.initializeApp(
|
||||||
options: DefaultFirebaseOptionsStaging.currentPlatform,
|
options: DefaultFirebaseOptions(
|
||||||
|
databaseUrl: dotenv.env['RTDB_URL']!,
|
||||||
|
),
|
||||||
);
|
);
|
||||||
initialSetup();
|
initialSetup();
|
||||||
} catch (_) {}
|
} catch (_) {}
|
||||||
@ -59,7 +61,7 @@ class MyApp extends StatelessWidget {
|
|||||||
BlocProvider<CreateRoutineBloc>(
|
BlocProvider<CreateRoutineBloc>(
|
||||||
create: (context) => CreateRoutineBloc(),
|
create: (context) => CreateRoutineBloc(),
|
||||||
),
|
),
|
||||||
BlocProvider(create: (context) => HomeBloc()..add(FetchUserInfo())),
|
BlocProvider(create: (context) => HomeBloc()..add(const FetchUserInfo())),
|
||||||
BlocProvider<VisitorPasswordBloc>(
|
BlocProvider<VisitorPasswordBloc>(
|
||||||
create: (context) => VisitorPasswordBloc(),
|
create: (context) => VisitorPasswordBloc(),
|
||||||
),
|
),
|
||||||
|
@ -4,7 +4,7 @@ import 'package:flutter/material.dart';
|
|||||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||||
import 'package:flutter_dotenv/flutter_dotenv.dart';
|
import 'package:flutter_dotenv/flutter_dotenv.dart';
|
||||||
import 'package:go_router/go_router.dart';
|
import 'package:go_router/go_router.dart';
|
||||||
import 'package:syncrow_web/firebase_options_dev.dart';
|
import 'package:syncrow_web/firebase_options.dart';
|
||||||
import 'package:syncrow_web/pages/auth/bloc/auth_bloc.dart';
|
import 'package:syncrow_web/pages/auth/bloc/auth_bloc.dart';
|
||||||
import 'package:syncrow_web/pages/home/bloc/home_bloc.dart';
|
import 'package:syncrow_web/pages/home/bloc/home_bloc.dart';
|
||||||
import 'package:syncrow_web/pages/home/bloc/home_event.dart';
|
import 'package:syncrow_web/pages/home/bloc/home_event.dart';
|
||||||
@ -27,7 +27,9 @@ Future<void> main() async {
|
|||||||
await dotenv.load(fileName: '.env.$environment');
|
await dotenv.load(fileName: '.env.$environment');
|
||||||
WidgetsFlutterBinding.ensureInitialized();
|
WidgetsFlutterBinding.ensureInitialized();
|
||||||
await Firebase.initializeApp(
|
await Firebase.initializeApp(
|
||||||
options: DefaultFirebaseOptionsDev.currentPlatform,
|
options: DefaultFirebaseOptions(
|
||||||
|
databaseUrl: dotenv.env['RTDB_URL']!,
|
||||||
|
),
|
||||||
);
|
);
|
||||||
initialSetup();
|
initialSetup();
|
||||||
} catch (_) {}
|
} catch (_) {}
|
||||||
@ -59,7 +61,7 @@ class MyApp extends StatelessWidget {
|
|||||||
BlocProvider<CreateRoutineBloc>(
|
BlocProvider<CreateRoutineBloc>(
|
||||||
create: (context) => CreateRoutineBloc(),
|
create: (context) => CreateRoutineBloc(),
|
||||||
),
|
),
|
||||||
BlocProvider(create: (context) => HomeBloc()..add(FetchUserInfo())),
|
BlocProvider(create: (context) => HomeBloc()..add(const FetchUserInfo())),
|
||||||
BlocProvider<VisitorPasswordBloc>(
|
BlocProvider<VisitorPasswordBloc>(
|
||||||
create: (context) => VisitorPasswordBloc(),
|
create: (context) => VisitorPasswordBloc(),
|
||||||
),
|
),
|
||||||
|
@ -4,7 +4,7 @@ import 'package:flutter/material.dart';
|
|||||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||||
import 'package:flutter_dotenv/flutter_dotenv.dart';
|
import 'package:flutter_dotenv/flutter_dotenv.dart';
|
||||||
import 'package:go_router/go_router.dart';
|
import 'package:go_router/go_router.dart';
|
||||||
import 'package:syncrow_web/firebase_options_prod.dart';
|
import 'package:syncrow_web/firebase_options.dart';
|
||||||
import 'package:syncrow_web/pages/auth/bloc/auth_bloc.dart';
|
import 'package:syncrow_web/pages/auth/bloc/auth_bloc.dart';
|
||||||
import 'package:syncrow_web/pages/home/bloc/home_bloc.dart';
|
import 'package:syncrow_web/pages/home/bloc/home_bloc.dart';
|
||||||
import 'package:syncrow_web/pages/home/bloc/home_event.dart';
|
import 'package:syncrow_web/pages/home/bloc/home_event.dart';
|
||||||
@ -24,7 +24,9 @@ Future<void> main() async {
|
|||||||
await dotenv.load(fileName: '.env.$environment');
|
await dotenv.load(fileName: '.env.$environment');
|
||||||
WidgetsFlutterBinding.ensureInitialized();
|
WidgetsFlutterBinding.ensureInitialized();
|
||||||
await Firebase.initializeApp(
|
await Firebase.initializeApp(
|
||||||
options: DefaultFirebaseOptionsStaging.currentPlatform,
|
options: DefaultFirebaseOptions(
|
||||||
|
databaseUrl: dotenv.env['RTDB_URL']!,
|
||||||
|
),
|
||||||
);
|
);
|
||||||
initialSetup();
|
initialSetup();
|
||||||
} catch (_) {}
|
} catch (_) {}
|
||||||
@ -56,7 +58,7 @@ class MyApp extends StatelessWidget {
|
|||||||
BlocProvider<CreateRoutineBloc>(
|
BlocProvider<CreateRoutineBloc>(
|
||||||
create: (context) => CreateRoutineBloc(),
|
create: (context) => CreateRoutineBloc(),
|
||||||
),
|
),
|
||||||
BlocProvider(create: (context) => HomeBloc()..add(FetchUserInfo())),
|
BlocProvider(create: (context) => HomeBloc()..add(const FetchUserInfo())),
|
||||||
BlocProvider<VisitorPasswordBloc>(
|
BlocProvider<VisitorPasswordBloc>(
|
||||||
create: (context) => VisitorPasswordBloc(),
|
create: (context) => VisitorPasswordBloc(),
|
||||||
),
|
),
|
||||||
|
@ -2,7 +2,7 @@ import 'package:flutter/material.dart';
|
|||||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||||
import 'package:syncrow_web/pages/access_management/bloc/access_event.dart';
|
import 'package:syncrow_web/pages/access_management/bloc/access_event.dart';
|
||||||
import 'package:syncrow_web/pages/access_management/bloc/access_state.dart';
|
import 'package:syncrow_web/pages/access_management/bloc/access_state.dart';
|
||||||
import 'package:syncrow_web/pages/access_management/model/password_model.dart';
|
import 'package:syncrow_web/pages/access_management/booking_system/presentation/model/password_model.dart';
|
||||||
import 'package:syncrow_web/pages/common/bloc/project_manager.dart';
|
import 'package:syncrow_web/pages/common/bloc/project_manager.dart';
|
||||||
import 'package:syncrow_web/pages/common/hour_picker_dialog.dart';
|
import 'package:syncrow_web/pages/common/hour_picker_dialog.dart';
|
||||||
import 'package:syncrow_web/services/access_mang_api.dart';
|
import 'package:syncrow_web/services/access_mang_api.dart';
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import 'package:equatable/equatable.dart';
|
import 'package:equatable/equatable.dart';
|
||||||
import 'package:syncrow_web/pages/access_management/model/password_model.dart';
|
import 'package:syncrow_web/pages/access_management/booking_system/presentation/model/password_model.dart';
|
||||||
|
|
||||||
abstract class AccessState extends Equatable {
|
abstract class AccessState extends Equatable {
|
||||||
const AccessState();
|
const AccessState();
|
||||||
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,72 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_svg/flutter_svg.dart';
|
||||||
|
import 'package:syncrow_web/utils/color_manager.dart';
|
||||||
|
|
||||||
|
class SvgTextButton extends StatelessWidget {
|
||||||
|
final String svgAsset;
|
||||||
|
final String label;
|
||||||
|
final VoidCallback onPressed;
|
||||||
|
final Color backgroundColor;
|
||||||
|
final Color svgColor;
|
||||||
|
final Color labelColor;
|
||||||
|
final double borderRadius;
|
||||||
|
final List<BoxShadow> boxShadow;
|
||||||
|
final double svgSize;
|
||||||
|
|
||||||
|
const SvgTextButton({
|
||||||
|
super.key,
|
||||||
|
required this.svgAsset,
|
||||||
|
required this.label,
|
||||||
|
required this.onPressed,
|
||||||
|
this.backgroundColor = ColorsManager.circleRolesBackground,
|
||||||
|
this.svgColor = const Color(0xFF496EFF),
|
||||||
|
this.labelColor = Colors.black,
|
||||||
|
this.borderRadius = 10.0,
|
||||||
|
this.boxShadow = const [
|
||||||
|
BoxShadow(
|
||||||
|
color: ColorsManager.lightGrayColor,
|
||||||
|
blurRadius: 4,
|
||||||
|
offset: Offset(0, 1),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
this.svgSize = 24.0,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Material(
|
||||||
|
color: Colors.transparent,
|
||||||
|
child: InkWell(
|
||||||
|
borderRadius: BorderRadius.circular(borderRadius),
|
||||||
|
onTap: onPressed,
|
||||||
|
child: Container(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 12),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: backgroundColor,
|
||||||
|
borderRadius: BorderRadius.circular(borderRadius),
|
||||||
|
boxShadow: boxShadow,
|
||||||
|
),
|
||||||
|
child: Row(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
SvgPicture.asset(
|
||||||
|
svgAsset,
|
||||||
|
width: svgSize,
|
||||||
|
height: svgSize,
|
||||||
|
),
|
||||||
|
const SizedBox(width: 12),
|
||||||
|
Text(
|
||||||
|
label,
|
||||||
|
style: TextStyle(
|
||||||
|
color: labelColor,
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
@ -2,302 +2,86 @@ import 'package:flutter/material.dart';
|
|||||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||||
import 'package:syncrow_web/pages/access_management/bloc/access_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/bloc/access_event.dart';
|
||||||
import 'package:syncrow_web/pages/access_management/bloc/access_state.dart';
|
import 'package:syncrow_web/pages/access_management/booking_system/presentation/view/booking_page.dart';
|
||||||
import 'package:syncrow_web/pages/common/buttons/default_button.dart';
|
import 'package:syncrow_web/pages/access_management/view/access_overview_content.dart';
|
||||||
import 'package:syncrow_web/pages/common/buttons/search_reset_buttons.dart';
|
|
||||||
import 'package:syncrow_web/pages/common/custom_table.dart';
|
|
||||||
import 'package:syncrow_web/pages/common/date_time_widget.dart';
|
|
||||||
import 'package:syncrow_web/pages/common/filter/filter_widget.dart';
|
|
||||||
import 'package:syncrow_web/pages/common/text_field/custom_web_textfield.dart';
|
|
||||||
import 'package:syncrow_web/pages/device_managment/shared/navigate_home_grid_view.dart';
|
import 'package:syncrow_web/pages/device_managment/shared/navigate_home_grid_view.dart';
|
||||||
import 'package:syncrow_web/pages/visitor_password/view/visitor_password_dialog.dart';
|
|
||||||
// import 'package:syncrow_web/utils/color_manager.dart';
|
|
||||||
import 'package:syncrow_web/utils/constants/app_enum.dart';
|
|
||||||
import 'package:syncrow_web/utils/constants/assets.dart';
|
|
||||||
import 'package:syncrow_web/utils/extension/build_context_x.dart';
|
import 'package:syncrow_web/utils/extension/build_context_x.dart';
|
||||||
import 'package:syncrow_web/utils/helpers/responsice_layout_helper/responsive_layout_helper.dart';
|
import 'package:syncrow_web/utils/helpers/responsice_layout_helper/responsive_layout_helper.dart';
|
||||||
import 'package:syncrow_web/utils/style.dart';
|
|
||||||
import 'package:syncrow_web/utils/theme/responsive_text_theme.dart';
|
import 'package:syncrow_web/utils/theme/responsive_text_theme.dart';
|
||||||
import 'package:syncrow_web/web_layout/web_scaffold.dart';
|
import 'package:syncrow_web/web_layout/web_scaffold.dart';
|
||||||
|
|
||||||
class AccessManagementPage extends StatelessWidget with HelperResponsiveLayout {
|
class AccessManagementPage extends StatefulWidget {
|
||||||
const AccessManagementPage({super.key});
|
const AccessManagementPage({super.key});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
State<AccessManagementPage> createState() => _AccessManagementPageState();
|
||||||
final isLargeScreen = isLargeScreenSize(context);
|
}
|
||||||
final isSmallScreen = isSmallScreenSize(context);
|
|
||||||
final isHalfMediumScreen = isHafMediumScreenSize(context);
|
|
||||||
final padding =
|
|
||||||
isLargeScreen ? const EdgeInsets.all(30) : const EdgeInsets.all(15);
|
|
||||||
|
|
||||||
return WebScaffold(
|
class _AccessManagementPageState extends State<AccessManagementPage>
|
||||||
|
with HelperResponsiveLayout {
|
||||||
|
final PageController _pageController = PageController(initialPage: 0);
|
||||||
|
int _currentPageIndex = 0;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
_pageController.dispose();
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return BlocProvider(
|
||||||
|
create: (BuildContext context) => AccessBloc()..add(FetchTableData()),
|
||||||
|
child: WebScaffold(
|
||||||
enableMenuSidebar: false,
|
enableMenuSidebar: false,
|
||||||
appBarTitle: Text(
|
appBarTitle: Text(
|
||||||
'Access Management',
|
'Access Management',
|
||||||
style: ResponsiveTextTheme.of(context).deviceManagementTitle,
|
style: ResponsiveTextTheme.of(context).deviceManagementTitle,
|
||||||
),
|
),
|
||||||
rightBody: const NavigateHomeGridView(),
|
centerBody: Row(
|
||||||
scaffoldBody: BlocProvider(
|
|
||||||
create: (BuildContext context) =>
|
|
||||||
AccessBloc()..add(FetchTableData()),
|
|
||||||
child: BlocConsumer<AccessBloc, AccessState>(
|
|
||||||
listener: (context, state) {},
|
|
||||||
builder: (context, state) {
|
|
||||||
final accessBloc = BlocProvider.of<AccessBloc>(context);
|
|
||||||
final filteredData = accessBloc.filteredData;
|
|
||||||
return state is AccessLoaded
|
|
||||||
? const Center(child: CircularProgressIndicator())
|
|
||||||
: Container(
|
|
||||||
padding: padding,
|
|
||||||
height: MediaQuery.of(context).size.height,
|
|
||||||
width: MediaQuery.of(context).size.width,
|
|
||||||
child: Column(
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
|
||||||
children: [
|
|
||||||
FilterWidget(
|
|
||||||
size: MediaQuery.of(context).size,
|
|
||||||
tabs: accessBloc.tabs,
|
|
||||||
selectedIndex: accessBloc.selectedIndex,
|
|
||||||
onTabChanged: (index) {
|
|
||||||
accessBloc.add(TabChangedEvent(index));
|
|
||||||
},
|
|
||||||
),
|
|
||||||
const SizedBox(height: 20),
|
|
||||||
if (isSmallScreen || isHalfMediumScreen)
|
|
||||||
_buildSmallSearchFilters(context, accessBloc)
|
|
||||||
else
|
|
||||||
_buildNormalSearchWidgets(context, accessBloc),
|
|
||||||
const SizedBox(height: 20),
|
|
||||||
_buildVisitorAdminPasswords(context, accessBloc),
|
|
||||||
const SizedBox(height: 20),
|
|
||||||
Expanded(
|
|
||||||
child: DynamicTable(
|
|
||||||
tableName: 'AccessManagement',
|
|
||||||
uuidIndex: 1,
|
|
||||||
withSelectAll: true,
|
|
||||||
isEmpty: filteredData.isEmpty,
|
|
||||||
withCheckBox: false,
|
|
||||||
size: MediaQuery.of(context).size,
|
|
||||||
cellDecoration: containerDecoration,
|
|
||||||
headers: const [
|
|
||||||
'Name',
|
|
||||||
'Access Type',
|
|
||||||
'Access Start',
|
|
||||||
'Access End',
|
|
||||||
'Accessible Device',
|
|
||||||
'Authorizer',
|
|
||||||
'Authorization Date & Time',
|
|
||||||
'Access Status'
|
|
||||||
],
|
|
||||||
data: filteredData.map((item) {
|
|
||||||
return [
|
|
||||||
item.passwordName,
|
|
||||||
item.passwordType.value,
|
|
||||||
accessBloc
|
|
||||||
.timestampToDate(item.effectiveTime),
|
|
||||||
accessBloc
|
|
||||||
.timestampToDate(item.invalidTime),
|
|
||||||
item.deviceName.toString(),
|
|
||||||
item.authorizerEmail.toString(),
|
|
||||||
accessBloc
|
|
||||||
.timestampToDate(item.invalidTime),
|
|
||||||
item.passwordStatus.value,
|
|
||||||
];
|
|
||||||
}).toList(),
|
|
||||||
)),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
);
|
|
||||||
})));
|
|
||||||
}
|
|
||||||
|
|
||||||
Wrap _buildVisitorAdminPasswords(
|
|
||||||
BuildContext context, AccessBloc accessBloc) {
|
|
||||||
return Wrap(
|
|
||||||
spacing: 10,
|
|
||||||
runSpacing: 10,
|
|
||||||
children: [
|
|
||||||
Container(
|
|
||||||
width: 205,
|
|
||||||
height: 42,
|
|
||||||
decoration: containerDecoration,
|
|
||||||
child: DefaultButton(
|
|
||||||
onPressed: () {
|
|
||||||
showDialog(
|
|
||||||
context: context,
|
|
||||||
barrierDismissible: false,
|
|
||||||
builder: (BuildContext context) {
|
|
||||||
return const VisitorPasswordDialog();
|
|
||||||
},
|
|
||||||
).then((v) {
|
|
||||||
if (v != null) {
|
|
||||||
accessBloc.add(FetchTableData());
|
|
||||||
}
|
|
||||||
});
|
|
||||||
},
|
|
||||||
borderRadius: 8,
|
|
||||||
child: Text(
|
|
||||||
'Create Visitor Password ',
|
|
||||||
style: context.textTheme.titleSmall!
|
|
||||||
.copyWith(color: Colors.white, fontSize: 12),
|
|
||||||
)),
|
|
||||||
),
|
|
||||||
// Container(
|
|
||||||
// width: 133,
|
|
||||||
// height: 42,
|
|
||||||
// decoration: containerDecoration,
|
|
||||||
// child: DefaultButton(
|
|
||||||
// borderRadius: 8,
|
|
||||||
// backgroundColor: ColorsManager.whiteColors,
|
|
||||||
// child: Text(
|
|
||||||
// 'Admin Password',
|
|
||||||
// style: context.textTheme.titleSmall!
|
|
||||||
// .copyWith(color: Colors.black, fontSize: 12),
|
|
||||||
// )),
|
|
||||||
// ),
|
|
||||||
],
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Row _buildNormalSearchWidgets(BuildContext context, AccessBloc accessBloc) {
|
|
||||||
// TimeOfDay _selectedTime = TimeOfDay.now();
|
|
||||||
|
|
||||||
return Row(
|
|
||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
crossAxisAlignment: CrossAxisAlignment.end,
|
|
||||||
textBaseline: TextBaseline.ideographic,
|
|
||||||
children: [
|
children: [
|
||||||
SizedBox(
|
TextButton(
|
||||||
width: 250,
|
onPressed: () => _switchPage(0),
|
||||||
child: CustomWebTextField(
|
child: Text(
|
||||||
controller: accessBloc.passwordName,
|
'Access Overview',
|
||||||
height: 43,
|
style: context.textTheme.titleMedium?.copyWith(
|
||||||
isRequired: false,
|
color: _currentPageIndex == 0 ? Colors.white : Colors.grey,
|
||||||
textFieldName: 'Name',
|
fontWeight: _currentPageIndex == 0
|
||||||
description: '',
|
? FontWeight.w700
|
||||||
onSubmitted: (value) {
|
: FontWeight.w400,
|
||||||
accessBloc.add(FilterDataEvent(
|
|
||||||
emailAuthorizer:
|
|
||||||
accessBloc.emailAuthorizer.text.toLowerCase(),
|
|
||||||
selectedTabIndex:
|
|
||||||
BlocProvider.of<AccessBloc>(context).selectedIndex,
|
|
||||||
passwordName: accessBloc.passwordName.text.toLowerCase(),
|
|
||||||
startTime: accessBloc.effectiveTimeTimeStamp,
|
|
||||||
endTime: accessBloc.expirationTimeTimeStamp));
|
|
||||||
},
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(width: 15),
|
),
|
||||||
SizedBox(
|
TextButton(
|
||||||
width: 250,
|
onPressed: () => _switchPage(1),
|
||||||
child: CustomWebTextField(
|
child: Text(
|
||||||
controller: accessBloc.emailAuthorizer,
|
'Booking System',
|
||||||
height: 43,
|
style: context.textTheme.titleMedium?.copyWith(
|
||||||
isRequired: false,
|
color: _currentPageIndex == 1 ? Colors.white : Colors.grey,
|
||||||
textFieldName: 'Authorizer',
|
fontWeight: _currentPageIndex == 1
|
||||||
description: '',
|
? FontWeight.w700
|
||||||
onSubmitted: (value) {
|
: FontWeight.w400,
|
||||||
accessBloc.add(FilterDataEvent(
|
|
||||||
emailAuthorizer:
|
|
||||||
accessBloc.emailAuthorizer.text.toLowerCase(),
|
|
||||||
selectedTabIndex:
|
|
||||||
BlocProvider.of<AccessBloc>(context).selectedIndex,
|
|
||||||
passwordName: accessBloc.passwordName.text.toLowerCase(),
|
|
||||||
startTime: accessBloc.effectiveTimeTimeStamp,
|
|
||||||
endTime: accessBloc.expirationTimeTimeStamp));
|
|
||||||
},
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(width: 15),
|
|
||||||
SizedBox(
|
|
||||||
child: DateTimeWebWidget(
|
|
||||||
icon: Assets.calendarIcon,
|
|
||||||
isRequired: false,
|
|
||||||
title: 'Access Time',
|
|
||||||
size: MediaQuery.of(context).size,
|
|
||||||
endTime: () {
|
|
||||||
accessBloc.add(SelectTime(context: context, isStart: false));
|
|
||||||
},
|
|
||||||
startTime: () {
|
|
||||||
accessBloc.add(SelectTime(context: context, isStart: true));
|
|
||||||
},
|
|
||||||
firstString: BlocProvider.of<AccessBloc>(context).startTime,
|
|
||||||
secondString: BlocProvider.of<AccessBloc>(context).endTime,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
const SizedBox(width: 15),
|
|
||||||
SearchResetButtons(
|
|
||||||
onSearch: () {
|
|
||||||
accessBloc.add(FilterDataEvent(
|
|
||||||
emailAuthorizer: accessBloc.emailAuthorizer.text.toLowerCase(),
|
|
||||||
selectedTabIndex:
|
|
||||||
BlocProvider.of<AccessBloc>(context).selectedIndex,
|
|
||||||
passwordName: accessBloc.passwordName.text.toLowerCase(),
|
|
||||||
startTime: accessBloc.effectiveTimeTimeStamp,
|
|
||||||
endTime: accessBloc.expirationTimeTimeStamp));
|
|
||||||
},
|
|
||||||
onReset: () {
|
|
||||||
accessBloc.add(ResetSearch());
|
|
||||||
},
|
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
|
),
|
||||||
|
rightBody: const NavigateHomeGridView(),
|
||||||
|
scaffoldBody: PageView(
|
||||||
|
controller: _pageController,
|
||||||
|
physics: const NeverScrollableScrollPhysics(),
|
||||||
|
children: const [
|
||||||
|
AccessOverviewContent(),
|
||||||
|
BookingPage(),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildSmallSearchFilters(BuildContext context, AccessBloc accessBloc) {
|
void _switchPage(int index) {
|
||||||
return Wrap(
|
setState(() => _currentPageIndex = index);
|
||||||
spacing: 20,
|
_pageController.jumpToPage(index);
|
||||||
runSpacing: 10,
|
|
||||||
children: [
|
|
||||||
SizedBox(
|
|
||||||
width: 300,
|
|
||||||
child: CustomWebTextField(
|
|
||||||
controller: accessBloc.passwordName,
|
|
||||||
isRequired: true,
|
|
||||||
height: 40,
|
|
||||||
textFieldName: 'Name',
|
|
||||||
description: '',
|
|
||||||
onSubmitted: (value) {
|
|
||||||
accessBloc.add(FilterDataEvent(
|
|
||||||
emailAuthorizer:
|
|
||||||
accessBloc.emailAuthorizer.text.toLowerCase(),
|
|
||||||
selectedTabIndex:
|
|
||||||
BlocProvider.of<AccessBloc>(context).selectedIndex,
|
|
||||||
passwordName: accessBloc.passwordName.text.toLowerCase(),
|
|
||||||
startTime: accessBloc.effectiveTimeTimeStamp,
|
|
||||||
endTime: accessBloc.expirationTimeTimeStamp));
|
|
||||||
}),
|
|
||||||
),
|
|
||||||
DateTimeWebWidget(
|
|
||||||
icon: Assets.calendarIcon,
|
|
||||||
isRequired: false,
|
|
||||||
title: 'Access Time',
|
|
||||||
size: MediaQuery.of(context).size,
|
|
||||||
endTime: () {
|
|
||||||
accessBloc.add(SelectTime(context: context, isStart: false));
|
|
||||||
},
|
|
||||||
startTime: () {
|
|
||||||
accessBloc.add(SelectTime(context: context, isStart: true));
|
|
||||||
},
|
|
||||||
firstString: BlocProvider.of<AccessBloc>(context).startTime,
|
|
||||||
secondString: BlocProvider.of<AccessBloc>(context).endTime,
|
|
||||||
),
|
|
||||||
SearchResetButtons(
|
|
||||||
onSearch: () {
|
|
||||||
accessBloc.add(FilterDataEvent(
|
|
||||||
emailAuthorizer: accessBloc.emailAuthorizer.text.toLowerCase(),
|
|
||||||
selectedTabIndex:
|
|
||||||
BlocProvider.of<AccessBloc>(context).selectedIndex,
|
|
||||||
passwordName: accessBloc.passwordName.text.toLowerCase(),
|
|
||||||
startTime: accessBloc.effectiveTimeTimeStamp,
|
|
||||||
endTime: accessBloc.expirationTimeTimeStamp));
|
|
||||||
},
|
|
||||||
onReset: () {
|
|
||||||
accessBloc.add(ResetSearch());
|
|
||||||
},
|
|
||||||
),
|
|
||||||
],
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
289
lib/pages/access_management/view/access_overview_content.dart
Normal file
289
lib/pages/access_management/view/access_overview_content.dart
Normal file
@ -0,0 +1,289 @@
|
|||||||
|
import 'package:syncrow_web/pages/common/buttons/default_button.dart';
|
||||||
|
import 'package:syncrow_web/pages/common/buttons/search_reset_buttons.dart';
|
||||||
|
import 'package:syncrow_web/pages/common/custom_table.dart';
|
||||||
|
import 'package:syncrow_web/pages/common/date_time_widget.dart';
|
||||||
|
import 'package:syncrow_web/pages/common/filter/filter_widget.dart';
|
||||||
|
import 'package:syncrow_web/pages/common/text_field/custom_web_textfield.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:syncrow_web/pages/access_management/bloc/access_state.dart';
|
||||||
|
import 'package:syncrow_web/pages/visitor_password/view/visitor_password_dialog.dart';
|
||||||
|
import 'package:syncrow_web/utils/constants/app_enum.dart';
|
||||||
|
import 'package:syncrow_web/utils/constants/assets.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/utils/extension/build_context_x.dart';
|
||||||
|
import 'package:syncrow_web/utils/helpers/responsice_layout_helper/responsive_layout_helper.dart';
|
||||||
|
import 'package:syncrow_web/utils/style.dart';
|
||||||
|
|
||||||
|
class AccessOverviewContent extends StatelessWidget
|
||||||
|
with HelperResponsiveLayout {
|
||||||
|
const AccessOverviewContent({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final isLargeScreen = isLargeScreenSize(context);
|
||||||
|
final isSmallScreen = isSmallScreenSize(context);
|
||||||
|
final isHalfMediumScreen = isHafMediumScreenSize(context);
|
||||||
|
final padding =
|
||||||
|
isLargeScreen ? const EdgeInsets.all(30) : const EdgeInsets.all(15);
|
||||||
|
|
||||||
|
return BlocProvider(
|
||||||
|
create: (BuildContext context) => AccessBloc()..add(FetchTableData()),
|
||||||
|
child: BlocConsumer<AccessBloc, AccessState>(
|
||||||
|
listener: (context, state) {},
|
||||||
|
builder: (context, state) {
|
||||||
|
final accessBloc = BlocProvider.of<AccessBloc>(context);
|
||||||
|
final filteredData = accessBloc.filteredData;
|
||||||
|
return state is AccessLoaded
|
||||||
|
? const Center(child: CircularProgressIndicator())
|
||||||
|
: Container(
|
||||||
|
padding: padding,
|
||||||
|
height: MediaQuery.of(context).size.height,
|
||||||
|
width: MediaQuery.of(context).size.width,
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
FilterWidget(
|
||||||
|
size: MediaQuery.of(context).size,
|
||||||
|
tabs: accessBloc.tabs,
|
||||||
|
selectedIndex: accessBloc.selectedIndex,
|
||||||
|
onTabChanged: (index) {
|
||||||
|
accessBloc.add(TabChangedEvent(index));
|
||||||
|
},
|
||||||
|
),
|
||||||
|
const SizedBox(height: 20),
|
||||||
|
if (isSmallScreen || isHalfMediumScreen)
|
||||||
|
_buildSmallSearchFilters(context, accessBloc)
|
||||||
|
else
|
||||||
|
_buildNormalSearchWidgets(context, accessBloc),
|
||||||
|
const SizedBox(height: 20),
|
||||||
|
_buildVisitorAdminPasswords(context, accessBloc),
|
||||||
|
const SizedBox(height: 20),
|
||||||
|
Expanded(
|
||||||
|
child: DynamicTable(
|
||||||
|
tableName: 'AccessManagement',
|
||||||
|
uuidIndex: 1,
|
||||||
|
withSelectAll: true,
|
||||||
|
isEmpty: filteredData.isEmpty,
|
||||||
|
withCheckBox: false,
|
||||||
|
size: MediaQuery.of(context).size,
|
||||||
|
cellDecoration: containerDecoration,
|
||||||
|
headers: const [
|
||||||
|
'Name',
|
||||||
|
'Access Type',
|
||||||
|
'Access Start',
|
||||||
|
'Access End',
|
||||||
|
'Accessible Device',
|
||||||
|
'Authorizer',
|
||||||
|
'Authorization Date & Time',
|
||||||
|
'Access Status'
|
||||||
|
],
|
||||||
|
data: filteredData.map((item) {
|
||||||
|
return [
|
||||||
|
item.passwordName,
|
||||||
|
item.passwordType.value,
|
||||||
|
accessBloc.timestampToDate(item.effectiveTime),
|
||||||
|
accessBloc.timestampToDate(item.invalidTime),
|
||||||
|
item.deviceName.toString(),
|
||||||
|
item.authorizerEmail.toString(),
|
||||||
|
accessBloc.timestampToDate(item.invalidTime),
|
||||||
|
item.passwordStatus.value,
|
||||||
|
];
|
||||||
|
}).toList(),
|
||||||
|
)),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
Wrap _buildVisitorAdminPasswords(
|
||||||
|
BuildContext context, AccessBloc accessBloc) {
|
||||||
|
return Wrap(
|
||||||
|
spacing: 10,
|
||||||
|
runSpacing: 10,
|
||||||
|
children: [
|
||||||
|
Container(
|
||||||
|
width: 205,
|
||||||
|
height: 42,
|
||||||
|
decoration: containerDecoration,
|
||||||
|
child: DefaultButton(
|
||||||
|
onPressed: () {
|
||||||
|
showDialog(
|
||||||
|
context: context,
|
||||||
|
barrierDismissible: false,
|
||||||
|
builder: (BuildContext context) {
|
||||||
|
return const VisitorPasswordDialog();
|
||||||
|
},
|
||||||
|
).then((v) {
|
||||||
|
if (v != null) {
|
||||||
|
accessBloc.add(FetchTableData());
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
borderRadius: 8,
|
||||||
|
child: Text(
|
||||||
|
'Create Visitor Password ',
|
||||||
|
style: context.textTheme.titleSmall!
|
||||||
|
.copyWith(color: Colors.white, fontSize: 12),
|
||||||
|
)),
|
||||||
|
),
|
||||||
|
// Container(
|
||||||
|
// width: 133,
|
||||||
|
// height: 42,
|
||||||
|
// decoration: containerDecoration,
|
||||||
|
// child: DefaultButton(
|
||||||
|
// borderRadius: 8,
|
||||||
|
// backgroundColor: ColorsManager.whiteColors,
|
||||||
|
// child: Text(
|
||||||
|
// 'Admin Password',
|
||||||
|
// style: context.textTheme.titleSmall!
|
||||||
|
// .copyWith(color: Colors.black, fontSize: 12),
|
||||||
|
// )),
|
||||||
|
// ),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Row _buildNormalSearchWidgets(BuildContext context, AccessBloc accessBloc) {
|
||||||
|
// TimeOfDay _selectedTime = TimeOfDay.now();
|
||||||
|
|
||||||
|
return Row(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.end,
|
||||||
|
textBaseline: TextBaseline.ideographic,
|
||||||
|
children: [
|
||||||
|
SizedBox(
|
||||||
|
width: 250,
|
||||||
|
child: CustomWebTextField(
|
||||||
|
controller: accessBloc.passwordName,
|
||||||
|
height: 43,
|
||||||
|
isRequired: false,
|
||||||
|
textFieldName: 'Name',
|
||||||
|
description: '',
|
||||||
|
onSubmitted: (value) {
|
||||||
|
accessBloc.add(FilterDataEvent(
|
||||||
|
emailAuthorizer:
|
||||||
|
accessBloc.emailAuthorizer.text.toLowerCase(),
|
||||||
|
selectedTabIndex:
|
||||||
|
BlocProvider.of<AccessBloc>(context).selectedIndex,
|
||||||
|
passwordName: accessBloc.passwordName.text.toLowerCase(),
|
||||||
|
startTime: accessBloc.effectiveTimeTimeStamp,
|
||||||
|
endTime: accessBloc.expirationTimeTimeStamp));
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 15),
|
||||||
|
SizedBox(
|
||||||
|
width: 250,
|
||||||
|
child: CustomWebTextField(
|
||||||
|
controller: accessBloc.emailAuthorizer,
|
||||||
|
height: 43,
|
||||||
|
isRequired: false,
|
||||||
|
textFieldName: 'Authorizer',
|
||||||
|
description: '',
|
||||||
|
onSubmitted: (value) {
|
||||||
|
accessBloc.add(FilterDataEvent(
|
||||||
|
emailAuthorizer:
|
||||||
|
accessBloc.emailAuthorizer.text.toLowerCase(),
|
||||||
|
selectedTabIndex:
|
||||||
|
BlocProvider.of<AccessBloc>(context).selectedIndex,
|
||||||
|
passwordName: accessBloc.passwordName.text.toLowerCase(),
|
||||||
|
startTime: accessBloc.effectiveTimeTimeStamp,
|
||||||
|
endTime: accessBloc.expirationTimeTimeStamp));
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 15),
|
||||||
|
SizedBox(
|
||||||
|
child: DateTimeWebWidget(
|
||||||
|
icon: Assets.calendarIcon,
|
||||||
|
isRequired: false,
|
||||||
|
title: 'Access Time',
|
||||||
|
size: MediaQuery.of(context).size,
|
||||||
|
endTime: () {
|
||||||
|
accessBloc.add(SelectTime(context: context, isStart: false));
|
||||||
|
},
|
||||||
|
startTime: () {
|
||||||
|
accessBloc.add(SelectTime(context: context, isStart: true));
|
||||||
|
},
|
||||||
|
firstString: BlocProvider.of<AccessBloc>(context).startTime,
|
||||||
|
secondString: BlocProvider.of<AccessBloc>(context).endTime,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 15),
|
||||||
|
SearchResetButtons(
|
||||||
|
onSearch: () {
|
||||||
|
accessBloc.add(FilterDataEvent(
|
||||||
|
emailAuthorizer: accessBloc.emailAuthorizer.text.toLowerCase(),
|
||||||
|
selectedTabIndex:
|
||||||
|
BlocProvider.of<AccessBloc>(context).selectedIndex,
|
||||||
|
passwordName: accessBloc.passwordName.text.toLowerCase(),
|
||||||
|
startTime: accessBloc.effectiveTimeTimeStamp,
|
||||||
|
endTime: accessBloc.expirationTimeTimeStamp));
|
||||||
|
},
|
||||||
|
onReset: () {
|
||||||
|
accessBloc.add(ResetSearch());
|
||||||
|
},
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildSmallSearchFilters(BuildContext context, AccessBloc accessBloc) {
|
||||||
|
return Wrap(
|
||||||
|
spacing: 20,
|
||||||
|
runSpacing: 10,
|
||||||
|
children: [
|
||||||
|
SizedBox(
|
||||||
|
width: 300,
|
||||||
|
child: CustomWebTextField(
|
||||||
|
controller: accessBloc.passwordName,
|
||||||
|
isRequired: true,
|
||||||
|
height: 40,
|
||||||
|
textFieldName: 'Name',
|
||||||
|
description: '',
|
||||||
|
onSubmitted: (value) {
|
||||||
|
accessBloc.add(FilterDataEvent(
|
||||||
|
emailAuthorizer:
|
||||||
|
accessBloc.emailAuthorizer.text.toLowerCase(),
|
||||||
|
selectedTabIndex:
|
||||||
|
BlocProvider.of<AccessBloc>(context).selectedIndex,
|
||||||
|
passwordName: accessBloc.passwordName.text.toLowerCase(),
|
||||||
|
startTime: accessBloc.effectiveTimeTimeStamp,
|
||||||
|
endTime: accessBloc.expirationTimeTimeStamp));
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
DateTimeWebWidget(
|
||||||
|
icon: Assets.calendarIcon,
|
||||||
|
isRequired: false,
|
||||||
|
title: 'Access Time',
|
||||||
|
size: MediaQuery.of(context).size,
|
||||||
|
endTime: () {
|
||||||
|
accessBloc.add(SelectTime(context: context, isStart: false));
|
||||||
|
},
|
||||||
|
startTime: () {
|
||||||
|
accessBloc.add(SelectTime(context: context, isStart: true));
|
||||||
|
},
|
||||||
|
firstString: BlocProvider.of<AccessBloc>(context).startTime,
|
||||||
|
secondString: BlocProvider.of<AccessBloc>(context).endTime,
|
||||||
|
),
|
||||||
|
SearchResetButtons(
|
||||||
|
onSearch: () {
|
||||||
|
accessBloc.add(FilterDataEvent(
|
||||||
|
emailAuthorizer: accessBloc.emailAuthorizer.text.toLowerCase(),
|
||||||
|
selectedTabIndex:
|
||||||
|
BlocProvider.of<AccessBloc>(context).selectedIndex,
|
||||||
|
passwordName: accessBloc.passwordName.text.toLowerCase(),
|
||||||
|
startTime: accessBloc.effectiveTimeTimeStamp,
|
||||||
|
endTime: accessBloc.expirationTimeTimeStamp));
|
||||||
|
},
|
||||||
|
onReset: () {
|
||||||
|
accessBloc.add(ResetSearch());
|
||||||
|
},
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@ -20,6 +20,7 @@ class AqiDistributionChart extends StatelessWidget {
|
|||||||
return BarChart(
|
return BarChart(
|
||||||
BarChartData(
|
BarChartData(
|
||||||
maxY: 100.1,
|
maxY: 100.1,
|
||||||
|
alignment: BarChartAlignment.start,
|
||||||
gridData: EnergyManagementChartsHelper.gridData(
|
gridData: EnergyManagementChartsHelper.gridData(
|
||||||
horizontalInterval: 20,
|
horizontalInterval: 20,
|
||||||
),
|
),
|
||||||
|
@ -38,7 +38,7 @@ class AnalyticsEnergyManagementView extends StatelessWidget {
|
|||||||
return SingleChildScrollView(
|
return SingleChildScrollView(
|
||||||
child: Container(
|
child: Container(
|
||||||
padding: _padding,
|
padding: _padding,
|
||||||
height: MediaQuery.sizeOf(context).height * 1,
|
height: MediaQuery.sizeOf(context).height * 1.05,
|
||||||
child: const Column(
|
child: const Column(
|
||||||
children: [
|
children: [
|
||||||
Expanded(
|
Expanded(
|
||||||
|
@ -18,6 +18,7 @@ class OccupancyChart extends StatelessWidget {
|
|||||||
return BarChart(
|
return BarChart(
|
||||||
BarChartData(
|
BarChartData(
|
||||||
maxY: 100.001,
|
maxY: 100.001,
|
||||||
|
alignment: BarChartAlignment.start,
|
||||||
gridData: EnergyManagementChartsHelper.gridData().copyWith(
|
gridData: EnergyManagementChartsHelper.gridData().copyWith(
|
||||||
checkToShowHorizontalLine: (value) => true,
|
checkToShowHorizontalLine: (value) => true,
|
||||||
horizontalInterval: 20,
|
horizontalInterval: 20,
|
||||||
|
@ -17,8 +17,8 @@ class DeviceLocationDetailsServiceDecorator implements DeviceLocationService {
|
|||||||
'reverse',
|
'reverse',
|
||||||
queryParameters: {
|
queryParameters: {
|
||||||
'format': 'json',
|
'format': 'json',
|
||||||
'lat': param.latitude,
|
'lat': 25.1880567,
|
||||||
'lon': param.longitude,
|
'lon': 55.266608,
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -50,6 +50,9 @@ class _DynamicTableState extends State<DynamicTable> {
|
|||||||
bool _selectAll = false;
|
bool _selectAll = false;
|
||||||
final ScrollController _verticalScrollController = ScrollController();
|
final ScrollController _verticalScrollController = ScrollController();
|
||||||
final ScrollController _horizontalScrollController = ScrollController();
|
final ScrollController _horizontalScrollController = ScrollController();
|
||||||
|
static const double _fixedRowHeight = 60;
|
||||||
|
static const double _checkboxColumnWidth = 50;
|
||||||
|
static const double _settingsColumnWidth = 100;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
@ -67,7 +70,6 @@ class _DynamicTableState extends State<DynamicTable> {
|
|||||||
|
|
||||||
bool _compareListOfLists(
|
bool _compareListOfLists(
|
||||||
List<List<dynamic>> oldList, List<List<dynamic>> newList) {
|
List<List<dynamic>> oldList, List<List<dynamic>> newList) {
|
||||||
// Check if the old and new lists are the same
|
|
||||||
if (oldList.length != newList.length) return false;
|
if (oldList.length != newList.length) return false;
|
||||||
|
|
||||||
for (int i = 0; i < oldList.length; i++) {
|
for (int i = 0; i < oldList.length; i++) {
|
||||||
@ -104,73 +106,130 @@ class _DynamicTableState extends State<DynamicTable> {
|
|||||||
context.read<DeviceManagementBloc>().add(UpdateSelection(_selectedRows));
|
context.read<DeviceManagementBloc>().add(UpdateSelection(_selectedRows));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
double get _totalTableWidth {
|
||||||
|
final hasSettings = widget.headers.contains('Settings');
|
||||||
|
final base = (widget.withCheckBox ? _checkboxColumnWidth : 0) +
|
||||||
|
(hasSettings ? _settingsColumnWidth : 0);
|
||||||
|
final regularCount = widget.headers.length - (hasSettings ? 1 : 0);
|
||||||
|
final regularWidth = (widget.size.width - base) / regularCount;
|
||||||
|
return base + regularCount * regularWidth;
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return Container(
|
return Container(
|
||||||
|
width: widget.size.width,
|
||||||
|
height: widget.size.height,
|
||||||
decoration: widget.cellDecoration,
|
decoration: widget.cellDecoration,
|
||||||
|
child: ScrollConfiguration(
|
||||||
|
behavior: const ScrollBehavior().copyWith(scrollbars: false),
|
||||||
child: Scrollbar(
|
child: Scrollbar(
|
||||||
controller: _verticalScrollController,
|
|
||||||
thumbVisibility: true,
|
|
||||||
trackVisibility: true,
|
|
||||||
child: Scrollbar(
|
|
||||||
//fixed the horizontal scrollbar issue
|
|
||||||
controller: _horizontalScrollController,
|
controller: _horizontalScrollController,
|
||||||
thumbVisibility: true,
|
thumbVisibility: true,
|
||||||
trackVisibility: true,
|
trackVisibility: true,
|
||||||
notificationPredicate: (notif) => notif.depth == 1,
|
notificationPredicate: (notif) =>
|
||||||
child: SingleChildScrollView(
|
notif.metrics.axis == Axis.horizontal,
|
||||||
controller: _verticalScrollController,
|
|
||||||
child: SingleChildScrollView(
|
child: SingleChildScrollView(
|
||||||
controller: _horizontalScrollController,
|
controller: _horizontalScrollController,
|
||||||
scrollDirection: Axis.horizontal,
|
scrollDirection: Axis.horizontal,
|
||||||
child: SizedBox(
|
child: SizedBox(
|
||||||
width: widget.size.width,
|
width: _totalTableWidth,
|
||||||
child: Column(
|
child: Column(
|
||||||
children: [
|
children: [
|
||||||
Container(
|
Container(
|
||||||
|
height: _fixedRowHeight,
|
||||||
decoration: widget.headerDecoration ??
|
decoration: widget.headerDecoration ??
|
||||||
const BoxDecoration(
|
const BoxDecoration(color: ColorsManager.boxColor),
|
||||||
color: ColorsManager.boxColor,
|
|
||||||
),
|
|
||||||
child: Row(
|
child: Row(
|
||||||
children: [
|
children: [
|
||||||
if (widget.withCheckBox) _buildSelectAllCheckbox(),
|
if (widget.withCheckBox)
|
||||||
...List.generate(widget.headers.length, (index) {
|
_buildSelectAllCheckbox(_checkboxColumnWidth),
|
||||||
return _buildTableHeaderCell(
|
for (var i = 0; i < widget.headers.length; i++)
|
||||||
widget.headers[index], index);
|
_buildTableHeaderCell(
|
||||||
})
|
widget.headers[i],
|
||||||
//...widget.headers.map((header) => _buildTableHeaderCell(header)),
|
widget.headers[i] == 'Settings'
|
||||||
|
? _settingsColumnWidth
|
||||||
|
: (_totalTableWidth -
|
||||||
|
(widget.withCheckBox
|
||||||
|
? _checkboxColumnWidth
|
||||||
|
: 0) -
|
||||||
|
(widget.headers.contains('Settings')
|
||||||
|
? _settingsColumnWidth
|
||||||
|
: 0)) /
|
||||||
|
(widget.headers.length -
|
||||||
|
(widget.headers.contains('Settings')
|
||||||
|
? 1
|
||||||
|
: 0)),
|
||||||
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
SizedBox(
|
|
||||||
width: widget.size.width,
|
Expanded(
|
||||||
child: widget.isEmpty
|
child: widget.isEmpty
|
||||||
? _buildEmptyState()
|
? _buildEmptyState()
|
||||||
: Column(
|
: Scrollbar(
|
||||||
children:
|
controller: _verticalScrollController,
|
||||||
List.generate(widget.data.length, (rowIndex) {
|
thumbVisibility: true,
|
||||||
|
trackVisibility: true,
|
||||||
|
notificationPredicate: (notif) =>
|
||||||
|
notif.metrics.axis == Axis.vertical,
|
||||||
|
child: ListView.builder(
|
||||||
|
controller: _verticalScrollController,
|
||||||
|
itemCount: widget.data.length,
|
||||||
|
itemBuilder: (_, rowIndex) {
|
||||||
final row = widget.data[rowIndex];
|
final row = widget.data[rowIndex];
|
||||||
return Row(
|
return SizedBox(
|
||||||
|
height: _fixedRowHeight,
|
||||||
|
child: Row(
|
||||||
children: [
|
children: [
|
||||||
if (widget.withCheckBox)
|
if (widget.withCheckBox)
|
||||||
_buildRowCheckbox(
|
_buildRowCheckbox(
|
||||||
rowIndex, widget.size.height * 0.08),
|
rowIndex,
|
||||||
...row.asMap().entries.map((entry) {
|
_checkboxColumnWidth,
|
||||||
return _buildTableCell(
|
),
|
||||||
entry.value.toString(),
|
for (var colIndex = 0;
|
||||||
widget.size.height * 0.08,
|
colIndex < row.length;
|
||||||
|
colIndex++)
|
||||||
|
widget.headers[colIndex] == 'Settings'
|
||||||
|
? buildSettingsIcon(
|
||||||
|
width: _settingsColumnWidth,
|
||||||
|
onTap: () => widget
|
||||||
|
.onSettingsPressed
|
||||||
|
?.call(rowIndex),
|
||||||
|
)
|
||||||
|
: _buildTableCell(
|
||||||
|
row[colIndex].toString(),
|
||||||
|
width: widget.headers[
|
||||||
|
colIndex] ==
|
||||||
|
'Settings'
|
||||||
|
? _settingsColumnWidth
|
||||||
|
: (_totalTableWidth -
|
||||||
|
(widget.withCheckBox
|
||||||
|
? _checkboxColumnWidth
|
||||||
|
: 0) -
|
||||||
|
(widget.headers
|
||||||
|
.contains(
|
||||||
|
'Settings')
|
||||||
|
? _settingsColumnWidth
|
||||||
|
: 0)) /
|
||||||
|
(widget.headers.length -
|
||||||
|
(widget.headers
|
||||||
|
.contains(
|
||||||
|
'Settings')
|
||||||
|
? 1
|
||||||
|
: 0)),
|
||||||
rowIndex: rowIndex,
|
rowIndex: rowIndex,
|
||||||
columnIndex: entry.key,
|
columnIndex: colIndex,
|
||||||
);
|
|
||||||
}).toList(),
|
|
||||||
],
|
|
||||||
);
|
|
||||||
}),
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@ -210,9 +269,10 @@ class _DynamicTableState extends State<DynamicTable> {
|
|||||||
],
|
],
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
Widget _buildSelectAllCheckbox() {
|
|
||||||
|
Widget _buildSelectAllCheckbox(double width) {
|
||||||
return Container(
|
return Container(
|
||||||
width: 50,
|
width: width,
|
||||||
decoration: const BoxDecoration(
|
decoration: const BoxDecoration(
|
||||||
border: Border.symmetric(
|
border: Border.symmetric(
|
||||||
vertical: BorderSide(color: ColorsManager.boxDivider),
|
vertical: BorderSide(color: ColorsManager.boxDivider),
|
||||||
@ -227,11 +287,11 @@ class _DynamicTableState extends State<DynamicTable> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildRowCheckbox(int index, double size) {
|
Widget _buildRowCheckbox(int index, double width) {
|
||||||
return Container(
|
return Container(
|
||||||
width: 50,
|
width: width,
|
||||||
padding: const EdgeInsets.all(8.0),
|
padding: const EdgeInsets.all(8.0),
|
||||||
height: size,
|
height: _fixedRowHeight,
|
||||||
decoration: const BoxDecoration(
|
decoration: const BoxDecoration(
|
||||||
border: Border(
|
border: Border(
|
||||||
bottom: BorderSide(
|
bottom: BorderSide(
|
||||||
@ -253,20 +313,18 @@ class _DynamicTableState extends State<DynamicTable> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildTableHeaderCell(String title, int index) {
|
Widget _buildTableHeaderCell(String title, double width) {
|
||||||
return Expanded(
|
return Container(
|
||||||
child: Container(
|
width: width,
|
||||||
decoration: const BoxDecoration(
|
decoration: const BoxDecoration(
|
||||||
border: Border.symmetric(
|
border: Border.symmetric(
|
||||||
vertical: BorderSide(color: ColorsManager.boxDivider),
|
vertical: BorderSide(color: ColorsManager.boxDivider),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
constraints: const BoxConstraints.expand(height: 40),
|
constraints: BoxConstraints(minHeight: 40, maxHeight: _fixedRowHeight),
|
||||||
alignment: Alignment.centerLeft,
|
alignment: Alignment.centerLeft,
|
||||||
child: Padding(
|
child: Padding(
|
||||||
padding: EdgeInsets.symmetric(
|
padding: const EdgeInsets.symmetric(horizontal: 8.0, vertical: 4),
|
||||||
horizontal: index == widget.headers.length - 1 ? 12 : 8.0,
|
|
||||||
vertical: 4),
|
|
||||||
child: Text(
|
child: Text(
|
||||||
title,
|
title,
|
||||||
style: context.textTheme.titleSmall!.copyWith(
|
style: context.textTheme.titleSmall!.copyWith(
|
||||||
@ -275,28 +333,27 @@ class _DynamicTableState extends State<DynamicTable> {
|
|||||||
fontWeight: FontWeight.w400,
|
fontWeight: FontWeight.w400,
|
||||||
),
|
),
|
||||||
maxLines: 2,
|
maxLines: 2,
|
||||||
),
|
overflow: TextOverflow.ellipsis,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildTableCell(String content, double size,
|
Widget _buildTableCell(String content,
|
||||||
{required int rowIndex, required int columnIndex}) {
|
{required double width,
|
||||||
|
required int rowIndex,
|
||||||
|
required int columnIndex}) {
|
||||||
bool isBatteryLevel = content.endsWith('%');
|
bool isBatteryLevel = content.endsWith('%');
|
||||||
double? batteryLevel;
|
double? batteryLevel;
|
||||||
|
|
||||||
if (isBatteryLevel) {
|
if (isBatteryLevel) {
|
||||||
batteryLevel = double.tryParse(content.replaceAll('%', '').trim());
|
batteryLevel = double.tryParse(content.replaceAll('%', '').trim());
|
||||||
}
|
}
|
||||||
|
|
||||||
bool isSettingsColumn = widget.headers[columnIndex] == 'Settings';
|
bool isSettingsColumn = widget.headers[columnIndex] == 'Settings';
|
||||||
if (isSettingsColumn) {
|
if (isSettingsColumn) {
|
||||||
return buildSettingsIcon(
|
return buildSettingsIcon(
|
||||||
width: 120,
|
width: width, onTap: () => widget.onSettingsPressed?.call(rowIndex));
|
||||||
height: 60,
|
|
||||||
iconSize: 40,
|
|
||||||
onTap: () => widget.onSettingsPressed?.call(rowIndex),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Color? statusColor;
|
Color? statusColor;
|
||||||
@ -320,10 +377,10 @@ class _DynamicTableState extends State<DynamicTable> {
|
|||||||
statusColor = Colors.black;
|
statusColor = Colors.black;
|
||||||
}
|
}
|
||||||
|
|
||||||
return Expanded(
|
return Container(
|
||||||
child: Container(
|
width: width,
|
||||||
height: size,
|
height: _fixedRowHeight,
|
||||||
padding: const EdgeInsets.all(5.0),
|
padding: const EdgeInsets.symmetric(horizontal: 8.0, vertical: 4),
|
||||||
decoration: const BoxDecoration(
|
decoration: const BoxDecoration(
|
||||||
border: Border(
|
border: Border(
|
||||||
bottom: BorderSide(
|
bottom: BorderSide(
|
||||||
@ -343,23 +400,19 @@ class _DynamicTableState extends State<DynamicTable> {
|
|||||||
? ColorsManager.green
|
? ColorsManager.green
|
||||||
: statusColor,
|
: statusColor,
|
||||||
fontSize: 13,
|
fontSize: 13,
|
||||||
fontWeight: FontWeight.w400),
|
fontWeight: FontWeight.w400,
|
||||||
maxLines: 2,
|
|
||||||
),
|
),
|
||||||
|
maxLines: 2,
|
||||||
|
overflow: TextOverflow.ellipsis,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget buildSettingsIcon(
|
Widget buildSettingsIcon({required double width, VoidCallback? onTap}) {
|
||||||
{double width = 120,
|
return Container(
|
||||||
double height = 60,
|
width: width,
|
||||||
double iconSize = 40,
|
height: _fixedRowHeight,
|
||||||
VoidCallback? onTap}) {
|
padding: const EdgeInsets.only(left: 15, top: 10, bottom: 10),
|
||||||
return Column(
|
|
||||||
children: [
|
|
||||||
Container(
|
|
||||||
padding: const EdgeInsets.only(top: 10, bottom: 15, left: 10),
|
|
||||||
margin: const EdgeInsets.only(right: 15),
|
|
||||||
decoration: const BoxDecoration(
|
decoration: const BoxDecoration(
|
||||||
color: ColorsManager.whiteColors,
|
color: ColorsManager.whiteColors,
|
||||||
border: Border(
|
border: Border(
|
||||||
@ -369,17 +422,13 @@ class _DynamicTableState extends State<DynamicTable> {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
width: width,
|
child: Align(
|
||||||
child: Padding(
|
alignment: Alignment.centerLeft,
|
||||||
padding: const EdgeInsets.only(
|
|
||||||
right: 16.0,
|
|
||||||
left: 17.0,
|
|
||||||
),
|
|
||||||
child: Container(
|
child: Container(
|
||||||
width: 50,
|
width: 50,
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: const Color(0xFFF7F8FA),
|
color: const Color(0xFFF7F8FA),
|
||||||
borderRadius: BorderRadius.circular(height / 2),
|
borderRadius: BorderRadius.circular(20),
|
||||||
boxShadow: [
|
boxShadow: [
|
||||||
BoxShadow(
|
BoxShadow(
|
||||||
color: Colors.black.withOpacity(0.17),
|
color: Colors.black.withOpacity(0.17),
|
||||||
@ -391,12 +440,12 @@ class _DynamicTableState extends State<DynamicTable> {
|
|||||||
child: InkWell(
|
child: InkWell(
|
||||||
onTap: onTap,
|
onTap: onTap,
|
||||||
child: Padding(
|
child: Padding(
|
||||||
padding: const EdgeInsets.all(8.0),
|
padding: EdgeInsets.all(8.0),
|
||||||
child: Center(
|
child: Center(
|
||||||
child: SvgPicture.asset(
|
child: SvgPicture.asset(
|
||||||
Assets.settings,
|
Assets.settings,
|
||||||
width: 40,
|
width: 40,
|
||||||
height: 22,
|
height: 20,
|
||||||
color: ColorsManager.primaryColor,
|
color: ColorsManager.primaryColor,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@ -404,8 +453,6 @@ class _DynamicTableState extends State<DynamicTable> {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
|
||||||
],
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -15,7 +15,8 @@ import 'package:syncrow_web/utils/constants/assets.dart';
|
|||||||
import 'package:syncrow_web/utils/extension/build_context_x.dart';
|
import 'package:syncrow_web/utils/extension/build_context_x.dart';
|
||||||
import 'package:syncrow_web/utils/helpers/responsice_layout_helper/responsive_layout_helper.dart';
|
import 'package:syncrow_web/utils/helpers/responsice_layout_helper/responsive_layout_helper.dart';
|
||||||
|
|
||||||
class AcDeviceBatchControlView extends StatelessWidget with HelperResponsiveLayout {
|
class AcDeviceBatchControlView extends StatelessWidget
|
||||||
|
with HelperResponsiveLayout {
|
||||||
const AcDeviceBatchControlView({super.key, required this.devicesIds});
|
const AcDeviceBatchControlView({super.key, required this.devicesIds});
|
||||||
|
|
||||||
final List<String> devicesIds;
|
final List<String> devicesIds;
|
||||||
@ -51,7 +52,7 @@ class AcDeviceBatchControlView extends StatelessWidget with HelperResponsiveLayo
|
|||||||
deviceId: devicesIds.first,
|
deviceId: devicesIds.first,
|
||||||
code: 'switch',
|
code: 'switch',
|
||||||
value: state.status.acSwitch,
|
value: state.status.acSwitch,
|
||||||
label: 'ThermoState',
|
label: 'Thermostat',
|
||||||
icon: Assets.ac,
|
icon: Assets.ac,
|
||||||
onChange: (value) {
|
onChange: (value) {
|
||||||
context.read<AcBloc>().add(AcBatchControlEvent(
|
context.read<AcBloc>().add(AcBatchControlEvent(
|
||||||
@ -100,8 +101,8 @@ class AcDeviceBatchControlView extends StatelessWidget with HelperResponsiveLayo
|
|||||||
),
|
),
|
||||||
Text(
|
Text(
|
||||||
'h',
|
'h',
|
||||||
style:
|
style: context.textTheme.bodySmall!
|
||||||
context.textTheme.bodySmall!.copyWith(color: ColorsManager.blackColor),
|
.copyWith(color: ColorsManager.blackColor),
|
||||||
),
|
),
|
||||||
Text(
|
Text(
|
||||||
'30',
|
'30',
|
||||||
@ -148,7 +149,8 @@ class AcDeviceBatchControlView extends StatelessWidget with HelperResponsiveLayo
|
|||||||
callFactoryReset: () {
|
callFactoryReset: () {
|
||||||
context.read<AcBloc>().add(AcFactoryResetEvent(
|
context.read<AcBloc>().add(AcFactoryResetEvent(
|
||||||
deviceId: state.status.uuid,
|
deviceId: state.status.uuid,
|
||||||
factoryResetModel: FactoryResetModel(devicesUuid: devicesIds),
|
factoryResetModel:
|
||||||
|
FactoryResetModel(devicesUuid: devicesIds),
|
||||||
));
|
));
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
|
@ -57,6 +57,9 @@ class Status {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
factory Status.fromJson(String source) => Status.fromMap(json.decode(source));
|
||||||
|
|
||||||
|
String toJson() => json.encode(toMap());
|
||||||
Status copyWith({
|
Status copyWith({
|
||||||
String? code,
|
String? code,
|
||||||
dynamic value,
|
dynamic value,
|
||||||
@ -66,8 +69,4 @@ class Status {
|
|||||||
value: value ?? this.value,
|
value: value ?? this.value,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
factory Status.fromJson(String source) => Status.fromMap(json.decode(source));
|
|
||||||
|
|
||||||
String toJson() => json.encode(toMap());
|
|
||||||
}
|
}
|
||||||
|
@ -68,6 +68,7 @@ class DeviceManagementBody extends StatelessWidget with HelperResponsiveLayout {
|
|||||||
children: [
|
children: [
|
||||||
Expanded(child: SpaceTreeView(
|
Expanded(child: SpaceTreeView(
|
||||||
onSelect: () {
|
onSelect: () {
|
||||||
|
context.read<DeviceManagementBloc>().add(ResetFilters());
|
||||||
context.read<DeviceManagementBloc>().add(FetchDevices(context));
|
context.read<DeviceManagementBloc>().add(FetchDevices(context));
|
||||||
},
|
},
|
||||||
)),
|
)),
|
||||||
|
@ -62,9 +62,10 @@ class CurtainModuleItems extends StatelessWidget with HelperResponsiveLayout {
|
|||||||
BlocProvider.of<CurtainModuleBloc>(context),
|
BlocProvider.of<CurtainModuleBloc>(context),
|
||||||
child: BuildScheduleView(
|
child: BuildScheduleView(
|
||||||
deviceUuid: deviceId,
|
deviceUuid: deviceId,
|
||||||
category: 'CUR_2',
|
category: 'Timer',
|
||||||
code: 'control',
|
code: 'control',
|
||||||
|
countdownCode: 'Timer',
|
||||||
|
deviceType: 'CUR_2',
|
||||||
),
|
),
|
||||||
));
|
));
|
||||||
},
|
},
|
||||||
|
@ -17,6 +17,7 @@ class CalibrateCompletedDialog extends StatelessWidget {
|
|||||||
@override
|
@override
|
||||||
Widget build(_) {
|
Widget build(_) {
|
||||||
return AlertDialog(
|
return AlertDialog(
|
||||||
|
backgroundColor: ColorsManager.whiteColors,
|
||||||
contentPadding: EdgeInsets.zero,
|
contentPadding: EdgeInsets.zero,
|
||||||
content: SizedBox(
|
content: SizedBox(
|
||||||
height: 250,
|
height: 250,
|
||||||
|
@ -40,7 +40,7 @@ class OneGangGlassSwitchBloc
|
|||||||
emit(OneGangGlassSwitchLoading());
|
emit(OneGangGlassSwitchLoading());
|
||||||
try {
|
try {
|
||||||
final status = await DevicesManagementApi().getDeviceStatus(event.deviceId);
|
final status = await DevicesManagementApi().getDeviceStatus(event.deviceId);
|
||||||
_listenToChanges(event.deviceId, emit);
|
_listenToChanges(event.deviceId);
|
||||||
deviceStatus = OneGangGlassStatusModel.fromJson(event.deviceId, status.status);
|
deviceStatus = OneGangGlassStatusModel.fromJson(event.deviceId, status.status);
|
||||||
emit(OneGangGlassSwitchStatusLoaded(deviceStatus));
|
emit(OneGangGlassSwitchStatusLoaded(deviceStatus));
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
@ -48,42 +48,28 @@ class OneGangGlassSwitchBloc
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void _listenToChanges(
|
StreamSubscription<DatabaseEvent>? _deviceStatusSubscription;
|
||||||
String deviceId,
|
|
||||||
Emitter<OneGangGlassSwitchState> emit,
|
void _listenToChanges(String deviceId) {
|
||||||
) {
|
|
||||||
try {
|
try {
|
||||||
final ref = FirebaseDatabase.instance.ref('device-status/$deviceId');
|
final ref = FirebaseDatabase.instance.ref('device-status/$deviceId');
|
||||||
final stream = ref.onValue;
|
_deviceStatusSubscription = ref.onValue.listen((DatabaseEvent event) async {
|
||||||
|
if (event.snapshot.value == null) return;
|
||||||
|
|
||||||
stream.listen((DatabaseEvent event) {
|
final usersMap = event.snapshot.value! as Map<dynamic, dynamic>;
|
||||||
final data = event.snapshot.value as Map<dynamic, dynamic>?;
|
|
||||||
if (data == null) return;
|
|
||||||
|
|
||||||
final statusList = <Status>[];
|
final statusList = <Status>[];
|
||||||
if (data['status'] != null) {
|
|
||||||
for (var element in data['status']) {
|
usersMap['status'].forEach((element) {
|
||||||
statusList.add(
|
statusList.add(Status(code: element['code'], value: element['value']));
|
||||||
Status(
|
|
||||||
code: element['code'].toString(),
|
|
||||||
value: element['value'].toString(),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (statusList.isNotEmpty) {
|
|
||||||
final newStatus = OneGangGlassStatusModel.fromJson(deviceId, statusList);
|
|
||||||
if (newStatus != deviceStatus) {
|
|
||||||
deviceStatus = newStatus;
|
|
||||||
if (!isClosed) {
|
|
||||||
add(StatusUpdated(deviceStatus));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
} catch (e) {
|
|
||||||
emit(OneGangGlassSwitchError('Failed to listen to changes: $e'));
|
deviceStatus =
|
||||||
}
|
OneGangGlassStatusModel.fromJson(usersMap['productUuid'], statusList);
|
||||||
|
|
||||||
|
add(StatusUpdated(deviceStatus));
|
||||||
|
});
|
||||||
|
} catch (_) {}
|
||||||
}
|
}
|
||||||
|
|
||||||
void _onStatusUpdated(
|
void _onStatusUpdated(
|
||||||
@ -174,4 +160,10 @@ class OneGangGlassSwitchBloc
|
|||||||
deviceStatus = deviceStatus.copyWith(switch1: value);
|
deviceStatus = deviceStatus.copyWith(switch1: value);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<void> close() {
|
||||||
|
_deviceStatusSubscription?.cancel();
|
||||||
|
return super.close();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -90,6 +90,8 @@ class OneGangGlassSwitchControlView extends StatelessWidget
|
|||||||
child: BuildScheduleView(
|
child: BuildScheduleView(
|
||||||
category: 'switch_1',
|
category: 'switch_1',
|
||||||
deviceUuid: deviceId,
|
deviceUuid: deviceId,
|
||||||
|
countdownCode: 'countdown_1',
|
||||||
|
deviceType: '1GT',
|
||||||
),
|
),
|
||||||
));
|
));
|
||||||
},
|
},
|
||||||
|
@ -80,6 +80,8 @@ class WallLightDeviceControl extends StatelessWidget
|
|||||||
child: BuildScheduleView(
|
child: BuildScheduleView(
|
||||||
category: 'switch_1',
|
category: 'switch_1',
|
||||||
deviceUuid: deviceId,
|
deviceUuid: deviceId,
|
||||||
|
countdownCode: 'countdown_1',
|
||||||
|
deviceType: '1G',
|
||||||
),
|
),
|
||||||
));
|
));
|
||||||
},
|
},
|
||||||
|
@ -277,6 +277,32 @@ class SmartPowerDeviceControl extends StatelessWidget
|
|||||||
totalConsumption: 10000,
|
totalConsumption: 10000,
|
||||||
date: blocProvider.formattedDate,
|
date: blocProvider.formattedDate,
|
||||||
),
|
),
|
||||||
|
EnergyConsumptionPage(
|
||||||
|
formattedDate:
|
||||||
|
'${blocProvider.dateTime!.day}/${blocProvider.dateTime!.month}/${blocProvider.dateTime!.year} ${blocProvider.endChartDate}',
|
||||||
|
onTap: () {
|
||||||
|
blocProvider.add(SelectDateEvent(context: context));
|
||||||
|
},
|
||||||
|
widget: blocProvider.dateSwitcher(),
|
||||||
|
chartData: blocProvider.energyDataList.isNotEmpty
|
||||||
|
? blocProvider.energyDataList
|
||||||
|
: [
|
||||||
|
EnergyData('12:00 AM', 4.0),
|
||||||
|
EnergyData('01:00 AM', 6.5),
|
||||||
|
EnergyData('02:00 AM', 3.8),
|
||||||
|
EnergyData('03:00 AM', 3.2),
|
||||||
|
EnergyData('04:00 AM', 6.0),
|
||||||
|
EnergyData('05:00 AM', 3.4),
|
||||||
|
EnergyData('06:00 AM', 5.2),
|
||||||
|
EnergyData('07:00 AM', 3.5),
|
||||||
|
EnergyData('08:00 AM', 6.8),
|
||||||
|
EnergyData('09:00 AM', 5.6),
|
||||||
|
EnergyData('10:00 AM', 3.9),
|
||||||
|
EnergyData('11:00 AM', 4.0),
|
||||||
|
],
|
||||||
|
totalConsumption: 10000,
|
||||||
|
date: blocProvider.formattedDate,
|
||||||
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
@ -47,7 +47,7 @@ class ScheduleBloc extends Bloc<ScheduleEvent, ScheduleState> {
|
|||||||
final success = await RemoteControlDeviceService().controlDevice(
|
final success = await RemoteControlDeviceService().controlDevice(
|
||||||
deviceUuid: deviceId,
|
deviceUuid: deviceId,
|
||||||
status: Status(
|
status: Status(
|
||||||
code: 'countdown_1',
|
code: event.countdownCode,
|
||||||
value: 0,
|
value: 0,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
@ -80,15 +80,18 @@ class ScheduleBloc extends Bloc<ScheduleEvent, ScheduleState> {
|
|||||||
) {
|
) {
|
||||||
if (state is ScheduleLoaded) {
|
if (state is ScheduleLoaded) {
|
||||||
final currentState = state as ScheduleLoaded;
|
final currentState = state as ScheduleLoaded;
|
||||||
|
|
||||||
emit(currentState.copyWith(
|
emit(currentState.copyWith(
|
||||||
|
countdownSeconds: currentState.countdownSeconds,
|
||||||
|
selectedTime: currentState.selectedTime,
|
||||||
|
deviceId: deviceId,
|
||||||
scheduleMode: event.scheduleMode,
|
scheduleMode: event.scheduleMode,
|
||||||
countdownRemaining: Duration.zero,
|
countdownHours: currentState.countdownHours,
|
||||||
countdownHours: 0,
|
countdownMinutes: currentState.countdownMinutes,
|
||||||
countdownMinutes: 0,
|
inchingHours: currentState.inchingHours,
|
||||||
inchingHours: 0,
|
inchingMinutes: currentState.inchingMinutes,
|
||||||
inchingMinutes: 0,
|
|
||||||
isCountdownActive: false,
|
|
||||||
isInchingActive: false,
|
isInchingActive: false,
|
||||||
|
isCountdownActive: currentState.countdownRemaining > Duration.zero,
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -221,7 +224,6 @@ class ScheduleBloc extends Bloc<ScheduleEvent, ScheduleState> {
|
|||||||
deviceId,
|
deviceId,
|
||||||
event.category,
|
event.category,
|
||||||
);
|
);
|
||||||
|
|
||||||
if (state is ScheduleLoaded) {
|
if (state is ScheduleLoaded) {
|
||||||
final currentState = state as ScheduleLoaded;
|
final currentState = state as ScheduleLoaded;
|
||||||
emit(currentState.copyWith(
|
emit(currentState.copyWith(
|
||||||
@ -230,7 +232,6 @@ class ScheduleBloc extends Bloc<ScheduleEvent, ScheduleState> {
|
|||||||
selectedDays: List.filled(7, false),
|
selectedDays: List.filled(7, false),
|
||||||
functionOn: false,
|
functionOn: false,
|
||||||
isEditing: false,
|
isEditing: false,
|
||||||
countdownRemaining: Duration.zero,
|
|
||||||
));
|
));
|
||||||
} else {
|
} else {
|
||||||
emit(ScheduleLoaded(
|
emit(ScheduleLoaded(
|
||||||
@ -285,9 +286,8 @@ class ScheduleBloc extends Bloc<ScheduleEvent, ScheduleState> {
|
|||||||
) async {
|
) async {
|
||||||
try {
|
try {
|
||||||
if (state is ScheduleLoaded) {
|
if (state is ScheduleLoaded) {
|
||||||
final dateTime = DateTime.parse(event.time);
|
|
||||||
Status status = Status(code: '', value: '');
|
Status status = Status(code: '', value: '');
|
||||||
if (event.category == 'CUR_2') {
|
if (event.deviceType == 'CUR_2') {
|
||||||
status = status.copyWith(
|
status = status.copyWith(
|
||||||
code: 'control',
|
code: 'control',
|
||||||
value: event.functionOn == true ? 'open' : 'close');
|
value: event.functionOn == true ? 'open' : 'close');
|
||||||
@ -295,6 +295,8 @@ class ScheduleBloc extends Bloc<ScheduleEvent, ScheduleState> {
|
|||||||
status =
|
status =
|
||||||
status.copyWith(code: event.category, value: event.functionOn);
|
status.copyWith(code: event.category, value: event.functionOn);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
final dateTime = DateTime.parse(event.time);
|
||||||
final updatedSchedule = ScheduleEntry(
|
final updatedSchedule = ScheduleEntry(
|
||||||
scheduleId: event.scheduleId,
|
scheduleId: event.scheduleId,
|
||||||
category: event.category,
|
category: event.category,
|
||||||
@ -405,7 +407,7 @@ class ScheduleBloc extends Bloc<ScheduleEvent, ScheduleState> {
|
|||||||
final totalSeconds =
|
final totalSeconds =
|
||||||
Duration(hours: event.hours, minutes: event.minutes).inSeconds;
|
Duration(hours: event.hours, minutes: event.minutes).inSeconds;
|
||||||
final code = event.mode == ScheduleModes.countdown
|
final code = event.mode == ScheduleModes.countdown
|
||||||
? 'countdown_1'
|
? event.countDownCode
|
||||||
: 'switch_inching';
|
: 'switch_inching';
|
||||||
final currentState = state as ScheduleLoaded;
|
final currentState = state as ScheduleLoaded;
|
||||||
final duration = Duration(seconds: totalSeconds);
|
final duration = Duration(seconds: totalSeconds);
|
||||||
@ -432,7 +434,7 @@ class ScheduleBloc extends Bloc<ScheduleEvent, ScheduleState> {
|
|||||||
);
|
);
|
||||||
|
|
||||||
if (success) {
|
if (success) {
|
||||||
if (code == 'countdown_1') {
|
if (code == event.countDownCode) {
|
||||||
final countdownDuration = Duration(seconds: totalSeconds);
|
final countdownDuration = Duration(seconds: totalSeconds);
|
||||||
|
|
||||||
emit(
|
emit(
|
||||||
@ -446,7 +448,7 @@ class ScheduleBloc extends Bloc<ScheduleEvent, ScheduleState> {
|
|||||||
);
|
);
|
||||||
|
|
||||||
if (countdownDuration.inSeconds > 0) {
|
if (countdownDuration.inSeconds > 0) {
|
||||||
_startCountdownTimer(emit, countdownDuration);
|
_startCountdownTimer(emit, countdownDuration, event.countDownCode);
|
||||||
} else {
|
} else {
|
||||||
_countdownTimer?.cancel();
|
_countdownTimer?.cancel();
|
||||||
emit(
|
emit(
|
||||||
@ -476,9 +478,7 @@ class ScheduleBloc extends Bloc<ScheduleEvent, ScheduleState> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void _startCountdownTimer(
|
void _startCountdownTimer(
|
||||||
Emitter<ScheduleState> emit,
|
Emitter<ScheduleState> emit, Duration duration, String countdownCode) {
|
||||||
Duration duration,
|
|
||||||
) {
|
|
||||||
_countdownTimer?.cancel();
|
_countdownTimer?.cancel();
|
||||||
_countdownTimer = Timer.periodic(const Duration(seconds: 1), (timer) {
|
_countdownTimer = Timer.periodic(const Duration(seconds: 1), (timer) {
|
||||||
if (_currentCountdown != null && _currentCountdown! > Duration.zero) {
|
if (_currentCountdown != null && _currentCountdown! > Duration.zero) {
|
||||||
@ -488,6 +488,7 @@ class ScheduleBloc extends Bloc<ScheduleEvent, ScheduleState> {
|
|||||||
} else {
|
} else {
|
||||||
timer.cancel();
|
timer.cancel();
|
||||||
add(StopScheduleEvent(
|
add(StopScheduleEvent(
|
||||||
|
countdownCode: countdownCode,
|
||||||
mode: _currentCountdown == null
|
mode: _currentCountdown == null
|
||||||
? ScheduleModes.countdown
|
? ScheduleModes.countdown
|
||||||
: ScheduleModes.inching,
|
: ScheduleModes.inching,
|
||||||
@ -524,70 +525,75 @@ class ScheduleBloc extends Bloc<ScheduleEvent, ScheduleState> {
|
|||||||
try {
|
try {
|
||||||
final status =
|
final status =
|
||||||
await DevicesManagementApi().getDeviceStatus(event.deviceId);
|
await DevicesManagementApi().getDeviceStatus(event.deviceId);
|
||||||
print(status.status);
|
int totalSeconds = 0;
|
||||||
|
final countdownItem = status.status.firstWhere(
|
||||||
|
(item) => item.code == event.countdownCode,
|
||||||
|
orElse: () => Status(code: '', value: 0),
|
||||||
|
);
|
||||||
|
totalSeconds = (countdownItem.value as int?) ?? 0;
|
||||||
|
final countdownHours = totalSeconds ~/ 3600;
|
||||||
|
final countdownMinutes = (totalSeconds % 3600) ~/ 60;
|
||||||
|
final countdownSeconds = totalSeconds % 60;
|
||||||
|
|
||||||
final deviceStatus =
|
final deviceStatus =
|
||||||
WaterHeaterStatusModel.fromJson(event.deviceId, status.status);
|
WaterHeaterStatusModel.fromJson(event.deviceId, status.status);
|
||||||
|
final isCountdownActive = totalSeconds > 0;
|
||||||
|
final isInchingActive = !isCountdownActive &&
|
||||||
|
(deviceStatus.inchingHours > 0 || deviceStatus.inchingMinutes > 0);
|
||||||
|
|
||||||
final scheduleMode =
|
final newState = state is ScheduleLoaded
|
||||||
deviceStatus.countdownHours > 0 || deviceStatus.countdownMinutes > 0
|
? (state as ScheduleLoaded).copyWith(
|
||||||
? ScheduleModes.countdown
|
scheduleMode: ScheduleModes.schedule,
|
||||||
: deviceStatus.inchingHours > 0 || deviceStatus.inchingMinutes > 0
|
countdownHours: countdownHours,
|
||||||
? ScheduleModes.inching
|
countdownMinutes: countdownMinutes,
|
||||||
: ScheduleModes.schedule;
|
countdownSeconds: countdownSeconds,
|
||||||
final isCountdown = scheduleMode == ScheduleModes.countdown;
|
|
||||||
final isInching = scheduleMode == ScheduleModes.inching;
|
|
||||||
|
|
||||||
Duration? countdownRemaining;
|
|
||||||
var isCountdownActive = false;
|
|
||||||
var isInchingActive = false;
|
|
||||||
|
|
||||||
if (isCountdown) {
|
|
||||||
countdownRemaining = Duration(
|
|
||||||
hours: deviceStatus.countdownHours,
|
|
||||||
minutes: deviceStatus.countdownMinutes,
|
|
||||||
);
|
|
||||||
isCountdownActive = countdownRemaining > Duration.zero;
|
|
||||||
} else if (isInching) {
|
|
||||||
isInchingActive = Duration(
|
|
||||||
hours: deviceStatus.inchingHours,
|
|
||||||
minutes: deviceStatus.inchingMinutes,
|
|
||||||
) >
|
|
||||||
Duration.zero;
|
|
||||||
}
|
|
||||||
if (state is ScheduleLoaded) {
|
|
||||||
final currentState = state as ScheduleLoaded;
|
|
||||||
emit(currentState.copyWith(
|
|
||||||
scheduleMode: scheduleMode,
|
|
||||||
countdownHours: deviceStatus.countdownHours,
|
|
||||||
countdownMinutes: deviceStatus.countdownMinutes,
|
|
||||||
inchingHours: deviceStatus.inchingHours,
|
inchingHours: deviceStatus.inchingHours,
|
||||||
inchingMinutes: deviceStatus.inchingMinutes,
|
inchingMinutes: deviceStatus.inchingMinutes,
|
||||||
isCountdownActive: isCountdownActive,
|
isCountdownActive: isCountdownActive,
|
||||||
isInchingActive: isInchingActive,
|
isInchingActive: isInchingActive,
|
||||||
countdownRemaining: countdownRemaining ?? Duration.zero,
|
countdownRemaining: isCountdownActive
|
||||||
));
|
? Duration(seconds: totalSeconds)
|
||||||
} else {
|
: Duration.zero,
|
||||||
emit(ScheduleLoaded(
|
)
|
||||||
|
: ScheduleLoaded(
|
||||||
|
scheduleMode: ScheduleModes.schedule,
|
||||||
schedules: const [],
|
schedules: const [],
|
||||||
selectedTime: null,
|
selectedTime: null,
|
||||||
selectedDays: List.filled(7, false),
|
selectedDays: List.filled(7, false),
|
||||||
functionOn: false,
|
functionOn: false,
|
||||||
isEditing: false,
|
isEditing: false,
|
||||||
deviceId: deviceId,
|
deviceId: event.deviceId,
|
||||||
scheduleMode: scheduleMode,
|
countdownHours: countdownHours,
|
||||||
countdownHours: deviceStatus.countdownHours,
|
countdownMinutes: countdownMinutes,
|
||||||
countdownMinutes: deviceStatus.countdownMinutes,
|
countdownSeconds: countdownSeconds,
|
||||||
inchingHours: deviceStatus.inchingHours,
|
inchingHours: deviceStatus.inchingHours,
|
||||||
inchingMinutes: deviceStatus.inchingMinutes,
|
inchingMinutes: deviceStatus.inchingMinutes,
|
||||||
isCountdownActive: isCountdownActive,
|
isCountdownActive: isCountdownActive,
|
||||||
isInchingActive: isInchingActive,
|
isInchingActive: isInchingActive,
|
||||||
countdownRemaining: countdownRemaining ?? Duration.zero,
|
countdownRemaining: isCountdownActive
|
||||||
|
? Duration(seconds: totalSeconds)
|
||||||
|
: Duration.zero,
|
||||||
|
);
|
||||||
|
emit(newState);
|
||||||
|
|
||||||
|
if (isCountdownActive) {
|
||||||
|
_countdownTimer?.cancel();
|
||||||
|
_currentCountdown = Duration(seconds: totalSeconds);
|
||||||
|
countdownRemaining = _currentCountdown!;
|
||||||
|
|
||||||
|
if (totalSeconds > 0) {
|
||||||
|
_startCountdownTimer(
|
||||||
|
emit, Duration(seconds: totalSeconds), event.countdownCode);
|
||||||
|
} else {
|
||||||
|
add(StopScheduleEvent(
|
||||||
|
countdownCode: event.countdownCode,
|
||||||
|
mode: ScheduleModes.countdown,
|
||||||
|
deviceId: event.deviceId,
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
// if (isCountdownActive && countdownRemaining != null) {
|
_countdownTimer?.cancel();
|
||||||
// _startCountdownTimer(emit, countdownRemaining);
|
}
|
||||||
// }
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
emit(ScheduleError('Failed to fetch device status: $e'));
|
emit(ScheduleError('Failed to fetch device status: $e'));
|
||||||
}
|
}
|
||||||
|
@ -91,6 +91,7 @@ class ScheduleEditEvent extends ScheduleEvent {
|
|||||||
final String time;
|
final String time;
|
||||||
final List<String> selectedDays;
|
final List<String> selectedDays;
|
||||||
final bool functionOn;
|
final bool functionOn;
|
||||||
|
final String deviceType;
|
||||||
|
|
||||||
const ScheduleEditEvent({
|
const ScheduleEditEvent({
|
||||||
required this.scheduleId,
|
required this.scheduleId,
|
||||||
@ -98,6 +99,7 @@ class ScheduleEditEvent extends ScheduleEvent {
|
|||||||
required this.time,
|
required this.time,
|
||||||
required this.selectedDays,
|
required this.selectedDays,
|
||||||
required this.functionOn,
|
required this.functionOn,
|
||||||
|
required this.deviceType,
|
||||||
});
|
});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@ -107,6 +109,7 @@ class ScheduleEditEvent extends ScheduleEvent {
|
|||||||
time,
|
time,
|
||||||
selectedDays,
|
selectedDays,
|
||||||
functionOn,
|
functionOn,
|
||||||
|
deviceType,
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -138,11 +141,13 @@ class ScheduleUpdateEntryEvent extends ScheduleEvent {
|
|||||||
|
|
||||||
class UpdateScheduleModeEvent extends ScheduleEvent {
|
class UpdateScheduleModeEvent extends ScheduleEvent {
|
||||||
final ScheduleModes scheduleMode;
|
final ScheduleModes scheduleMode;
|
||||||
|
final String countdownCode;
|
||||||
|
|
||||||
const UpdateScheduleModeEvent({required this.scheduleMode});
|
const UpdateScheduleModeEvent(
|
||||||
|
{required this.scheduleMode, required this.countdownCode});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
List<Object> get props => [scheduleMode];
|
List<Object> get props => [scheduleMode, countdownCode!];
|
||||||
}
|
}
|
||||||
|
|
||||||
class UpdateCountdownTimeEvent extends ScheduleEvent {
|
class UpdateCountdownTimeEvent extends ScheduleEvent {
|
||||||
@ -177,28 +182,32 @@ class StartScheduleEvent extends ScheduleEvent {
|
|||||||
final ScheduleModes mode;
|
final ScheduleModes mode;
|
||||||
final int hours;
|
final int hours;
|
||||||
final int minutes;
|
final int minutes;
|
||||||
|
final String countDownCode;
|
||||||
|
|
||||||
const StartScheduleEvent({
|
const StartScheduleEvent({
|
||||||
required this.mode,
|
required this.mode,
|
||||||
required this.hours,
|
required this.hours,
|
||||||
required this.minutes,
|
required this.minutes,
|
||||||
|
required this.countDownCode,
|
||||||
});
|
});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
List<Object?> get props => [mode, hours, minutes];
|
List<Object?> get props => [mode, hours, minutes, countDownCode];
|
||||||
}
|
}
|
||||||
|
|
||||||
class StopScheduleEvent extends ScheduleEvent {
|
class StopScheduleEvent extends ScheduleEvent {
|
||||||
final ScheduleModes mode;
|
final ScheduleModes mode;
|
||||||
final String deviceId;
|
final String deviceId;
|
||||||
|
final String countdownCode;
|
||||||
|
|
||||||
const StopScheduleEvent({
|
const StopScheduleEvent({
|
||||||
required this.mode,
|
required this.mode,
|
||||||
required this.deviceId,
|
required this.deviceId,
|
||||||
|
required this.countdownCode,
|
||||||
});
|
});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
List<Object?> get props => [mode, deviceId];
|
List<Object?> get props => [mode, deviceId, countdownCode];
|
||||||
}
|
}
|
||||||
|
|
||||||
class ScheduleDecrementCountdownEvent extends ScheduleEvent {
|
class ScheduleDecrementCountdownEvent extends ScheduleEvent {
|
||||||
@ -210,11 +219,13 @@ class ScheduleDecrementCountdownEvent extends ScheduleEvent {
|
|||||||
|
|
||||||
class ScheduleFetchStatusEvent extends ScheduleEvent {
|
class ScheduleFetchStatusEvent extends ScheduleEvent {
|
||||||
final String deviceId;
|
final String deviceId;
|
||||||
|
final String countdownCode;
|
||||||
|
|
||||||
const ScheduleFetchStatusEvent(this.deviceId);
|
const ScheduleFetchStatusEvent(
|
||||||
|
{required this.deviceId, required this.countdownCode});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
List<Object> get props => [deviceId];
|
List<Object> get props => [deviceId, countdownCode];
|
||||||
}
|
}
|
||||||
|
|
||||||
class DeleteScheduleEvent extends ScheduleEvent {
|
class DeleteScheduleEvent extends ScheduleEvent {
|
||||||
|
@ -29,7 +29,7 @@ class ScheduleLoaded extends ScheduleState {
|
|||||||
final int inchingSeconds;
|
final int inchingSeconds;
|
||||||
final bool isInchingActive;
|
final bool isInchingActive;
|
||||||
final ScheduleModes scheduleMode;
|
final ScheduleModes scheduleMode;
|
||||||
final Duration? countdownRemaining;
|
final Duration countdownRemaining;
|
||||||
final int? countdownSeconds;
|
final int? countdownSeconds;
|
||||||
|
|
||||||
const ScheduleLoaded({
|
const ScheduleLoaded({
|
||||||
@ -48,7 +48,7 @@ class ScheduleLoaded extends ScheduleState {
|
|||||||
this.inchingMinutes = 0,
|
this.inchingMinutes = 0,
|
||||||
this.isInchingActive = false,
|
this.isInchingActive = false,
|
||||||
this.scheduleMode = ScheduleModes.countdown,
|
this.scheduleMode = ScheduleModes.countdown,
|
||||||
this.countdownRemaining,
|
this.countdownRemaining = Duration.zero,
|
||||||
});
|
});
|
||||||
|
|
||||||
ScheduleLoaded copyWith({
|
ScheduleLoaded copyWith({
|
||||||
|
@ -11,6 +11,7 @@ class CountdownModeButtons extends StatelessWidget {
|
|||||||
final String deviceId;
|
final String deviceId;
|
||||||
final int hours;
|
final int hours;
|
||||||
final int minutes;
|
final int minutes;
|
||||||
|
final String countDownCode;
|
||||||
|
|
||||||
const CountdownModeButtons({
|
const CountdownModeButtons({
|
||||||
super.key,
|
super.key,
|
||||||
@ -18,6 +19,7 @@ class CountdownModeButtons extends StatelessWidget {
|
|||||||
required this.deviceId,
|
required this.deviceId,
|
||||||
required this.hours,
|
required this.hours,
|
||||||
required this.minutes,
|
required this.minutes,
|
||||||
|
required this.countDownCode,
|
||||||
});
|
});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@ -27,7 +29,9 @@ class CountdownModeButtons extends StatelessWidget {
|
|||||||
children: [
|
children: [
|
||||||
Expanded(
|
Expanded(
|
||||||
child: DefaultButton(
|
child: DefaultButton(
|
||||||
|
elevation: 2.5,
|
||||||
height: 40,
|
height: 40,
|
||||||
|
borderRadius: 8,
|
||||||
onPressed: () => Navigator.pop(context),
|
onPressed: () => Navigator.pop(context),
|
||||||
backgroundColor: ColorsManager.boxColor,
|
backgroundColor: ColorsManager.boxColor,
|
||||||
child: Text('Cancel', style: context.textTheme.bodyMedium),
|
child: Text('Cancel', style: context.textTheme.bodyMedium),
|
||||||
@ -37,19 +41,24 @@ class CountdownModeButtons extends StatelessWidget {
|
|||||||
Expanded(
|
Expanded(
|
||||||
child: isActive
|
child: isActive
|
||||||
? DefaultButton(
|
? DefaultButton(
|
||||||
|
elevation: 2.5,
|
||||||
|
borderRadius: 8,
|
||||||
height: 40,
|
height: 40,
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
context.read<ScheduleBloc>().add(
|
context.read<ScheduleBloc>().add(
|
||||||
StopScheduleEvent(
|
StopScheduleEvent(
|
||||||
mode: ScheduleModes.countdown,
|
mode: ScheduleModes.countdown,
|
||||||
deviceId: deviceId,
|
deviceId: deviceId,
|
||||||
|
countdownCode: countDownCode,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
backgroundColor: Colors.red,
|
backgroundColor: ColorsManager.red100,
|
||||||
child: const Text('Stop'),
|
child: const Text('Stop'),
|
||||||
)
|
)
|
||||||
: DefaultButton(
|
: DefaultButton(
|
||||||
|
elevation: 2.5,
|
||||||
|
borderRadius: 8,
|
||||||
height: 40,
|
height: 40,
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
context.read<ScheduleBloc>().add(
|
context.read<ScheduleBloc>().add(
|
||||||
@ -57,10 +66,10 @@ class CountdownModeButtons extends StatelessWidget {
|
|||||||
mode: ScheduleModes.countdown,
|
mode: ScheduleModes.countdown,
|
||||||
hours: hours,
|
hours: hours,
|
||||||
minutes: minutes,
|
minutes: minutes,
|
||||||
),
|
countDownCode: countDownCode),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
backgroundColor: ColorsManager.primaryColor,
|
backgroundColor: ColorsManager.primaryColorWithOpacity,
|
||||||
child: const Text('Save'),
|
child: const Text('Save'),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
@ -75,23 +75,33 @@ class _CountdownInchingViewState extends State<CountdownInchingView> {
|
|||||||
final isCountDown = state.scheduleMode == ScheduleModes.countdown;
|
final isCountDown = state.scheduleMode == ScheduleModes.countdown;
|
||||||
final isActive =
|
final isActive =
|
||||||
isCountDown ? state.isCountdownActive : state.isInchingActive;
|
isCountDown ? state.isCountdownActive : state.isInchingActive;
|
||||||
final displayHours = isActive && state.countdownRemaining != null
|
|
||||||
? state.countdownRemaining!.inHours
|
final displayHours =
|
||||||
|
isActive && state.countdownRemaining != Duration.zero
|
||||||
|
? state.countdownRemaining.inHours
|
||||||
: (isCountDown ? state.countdownHours : state.inchingHours);
|
: (isCountDown ? state.countdownHours : state.inchingHours);
|
||||||
final displayMinutes = isActive && state.countdownRemaining != null
|
|
||||||
? state.countdownRemaining!.inMinutes.remainder(60)
|
final displayMinutes =
|
||||||
|
isActive && state.countdownRemaining != Duration.zero
|
||||||
|
? state.countdownRemaining.inMinutes.remainder(60)
|
||||||
: (isCountDown ? state.countdownMinutes : state.inchingMinutes);
|
: (isCountDown ? state.countdownMinutes : state.inchingMinutes);
|
||||||
final displaySeconds = isActive && state.countdownRemaining != null
|
|
||||||
? state.countdownRemaining!.inSeconds.remainder(60)
|
|
||||||
: (isCountDown ? state.countdownSeconds : state.inchingSeconds);
|
|
||||||
|
|
||||||
_updateControllers(displayHours, displayMinutes, displaySeconds!);
|
final displaySeconds =
|
||||||
|
isActive && state.countdownRemaining != Duration.zero
|
||||||
|
? state.countdownRemaining.inSeconds.remainder(60)
|
||||||
|
: (isCountDown ? (state.countdownSeconds ?? 0) : 0);
|
||||||
|
|
||||||
if (displayHours == 0 && displayMinutes == 0 && displaySeconds == 0) {
|
_updateControllers(displayHours, displayMinutes, displaySeconds);
|
||||||
|
|
||||||
|
if (isActive &&
|
||||||
|
displayHours == 0 &&
|
||||||
|
displayMinutes == 0 &&
|
||||||
|
displaySeconds == 0) {
|
||||||
context.read<ScheduleBloc>().add(
|
context.read<ScheduleBloc>().add(
|
||||||
StopScheduleEvent(
|
StopScheduleEvent(
|
||||||
mode: ScheduleModes.countdown,
|
mode: ScheduleModes.countdown,
|
||||||
deviceId: widget.deviceId,
|
deviceId: widget.deviceId,
|
||||||
|
countdownCode: '',
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -216,6 +226,7 @@ class _CountdownInchingViewState extends State<CountdownInchingView> {
|
|||||||
index.toString().padLeft(2, '0'),
|
index.toString().padLeft(2, '0'),
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
fontSize: 24,
|
fontSize: 24,
|
||||||
|
fontWeight: FontWeight.w400,
|
||||||
color: isActive ? ColorsManager.grayColor : Colors.black,
|
color: isActive ? ColorsManager.grayColor : Colors.black,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@ -230,7 +241,8 @@ class _CountdownInchingViewState extends State<CountdownInchingView> {
|
|||||||
label,
|
label,
|
||||||
style: const TextStyle(
|
style: const TextStyle(
|
||||||
color: ColorsManager.grayColor,
|
color: ColorsManager.grayColor,
|
||||||
fontSize: 18,
|
fontSize: 24,
|
||||||
|
fontWeight: FontWeight.w400,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
|
@ -43,7 +43,9 @@ class InchingModeButtons extends StatelessWidget {
|
|||||||
onPressed: () {
|
onPressed: () {
|
||||||
context.read<ScheduleBloc>().add(
|
context.read<ScheduleBloc>().add(
|
||||||
StopScheduleEvent(
|
StopScheduleEvent(
|
||||||
deviceId: deviceId, mode: ScheduleModes.inching),
|
deviceId: deviceId,
|
||||||
|
mode: ScheduleModes.inching,
|
||||||
|
countdownCode: ''),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
backgroundColor: Colors.red,
|
backgroundColor: Colors.red,
|
||||||
|
@ -18,20 +18,24 @@ class BuildScheduleView extends StatelessWidget {
|
|||||||
super.key,
|
super.key,
|
||||||
required this.deviceUuid,
|
required this.deviceUuid,
|
||||||
required this.category,
|
required this.category,
|
||||||
|
required this.countdownCode,
|
||||||
this.code,
|
this.code,
|
||||||
|
required this.deviceType,
|
||||||
});
|
});
|
||||||
final String deviceUuid;
|
final String deviceUuid;
|
||||||
final String category;
|
final String category;
|
||||||
final String? code;
|
final String? code;
|
||||||
|
final String? countdownCode;
|
||||||
|
final String deviceType;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return BlocProvider(
|
return BlocProvider(
|
||||||
create: (_) => ScheduleBloc(
|
create: (_) => ScheduleBloc(deviceId: deviceUuid,)
|
||||||
deviceId: deviceUuid,
|
|
||||||
)
|
|
||||||
..add(ScheduleGetEvent(category: category))
|
..add(ScheduleGetEvent(category: category))
|
||||||
..add(ScheduleFetchStatusEvent(deviceUuid)),
|
..add(ScheduleFetchStatusEvent(
|
||||||
|
deviceId: deviceUuid,
|
||||||
|
countdownCode: countdownCode ?? '')),
|
||||||
child: Dialog(
|
child: Dialog(
|
||||||
backgroundColor: Colors.white,
|
backgroundColor: Colors.white,
|
||||||
insetPadding: const EdgeInsets.all(20),
|
insetPadding: const EdgeInsets.all(20),
|
||||||
@ -52,31 +56,33 @@ class BuildScheduleView extends StatelessWidget {
|
|||||||
children: [
|
children: [
|
||||||
const ScheduleHeader(),
|
const ScheduleHeader(),
|
||||||
const SizedBox(height: 20),
|
const SizedBox(height: 20),
|
||||||
if (category == 'CUR_2')
|
if (deviceType == 'CUR_2')
|
||||||
const SizedBox()
|
const SizedBox()
|
||||||
else
|
else
|
||||||
ScheduleModeSelector(
|
ScheduleModeSelector(
|
||||||
|
countdownCode: countdownCode ?? '',
|
||||||
currentMode: state.scheduleMode,
|
currentMode: state.scheduleMode,
|
||||||
),
|
),
|
||||||
const SizedBox(height: 20),
|
const SizedBox(height: 20),
|
||||||
if (state.scheduleMode == ScheduleModes.schedule)
|
if (state.scheduleMode == ScheduleModes.schedule)
|
||||||
ScheduleManagementUI(
|
ScheduleManagementUI(
|
||||||
|
deviceType: deviceType,
|
||||||
category: category,
|
category: category,
|
||||||
deviceUuid: deviceUuid,
|
deviceUuid: deviceUuid,
|
||||||
onAddSchedule: () async {
|
onAddSchedule: () async {
|
||||||
final entry = await ScheduleDialogHelper
|
final entry = await ScheduleDialogHelper
|
||||||
.showAddScheduleDialog(
|
.showAddScheduleDialog(context,
|
||||||
context,
|
|
||||||
schedule: ScheduleEntry(
|
schedule: ScheduleEntry(
|
||||||
category: category,
|
category: category,
|
||||||
time: '',
|
time: '',
|
||||||
function: Status(
|
function: Status(
|
||||||
code: code.toString(), value: null),
|
code: code.toString(),
|
||||||
|
value: true),
|
||||||
days: [],
|
days: [],
|
||||||
),
|
),
|
||||||
isEdit: false,
|
isEdit: false,
|
||||||
code: code,
|
code: code,
|
||||||
);
|
deviceType: deviceType);
|
||||||
if (entry != null) {
|
if (entry != null) {
|
||||||
context.read<ScheduleBloc>().add(
|
context.read<ScheduleBloc>().add(
|
||||||
ScheduleAddEvent(
|
ScheduleAddEvent(
|
||||||
@ -90,6 +96,7 @@ class BuildScheduleView extends StatelessWidget {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
|
if (deviceType != 'CUR_2')
|
||||||
if (state.scheduleMode == ScheduleModes.countdown ||
|
if (state.scheduleMode == ScheduleModes.countdown ||
|
||||||
state.scheduleMode == ScheduleModes.inching)
|
state.scheduleMode == ScheduleModes.inching)
|
||||||
CountdownInchingView(
|
CountdownInchingView(
|
||||||
@ -98,6 +105,7 @@ class BuildScheduleView extends StatelessWidget {
|
|||||||
const SizedBox(height: 20),
|
const SizedBox(height: 20),
|
||||||
if (state.scheduleMode == ScheduleModes.countdown)
|
if (state.scheduleMode == ScheduleModes.countdown)
|
||||||
CountdownModeButtons(
|
CountdownModeButtons(
|
||||||
|
countDownCode: countdownCode ?? '',
|
||||||
isActive: state.isCountdownActive,
|
isActive: state.isCountdownActive,
|
||||||
deviceId: deviceUuid,
|
deviceId: deviceUuid,
|
||||||
hours: state.countdownHours,
|
hours: state.countdownHours,
|
||||||
|
@ -13,9 +13,9 @@ class ScheduleHeader extends StatelessWidget {
|
|||||||
Text(
|
Text(
|
||||||
'Scheduling',
|
'Scheduling',
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
fontWeight: FontWeight.bold,
|
color: ColorsManager.primaryColorWithOpacity,
|
||||||
fontSize: 22,
|
fontWeight: FontWeight.w700,
|
||||||
color: ColorsManager.dialogBlueTitle,
|
fontSize: 30,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
Container(
|
Container(
|
||||||
|
@ -8,11 +8,13 @@ class ScheduleManagementUI extends StatelessWidget {
|
|||||||
final String deviceUuid;
|
final String deviceUuid;
|
||||||
final VoidCallback onAddSchedule;
|
final VoidCallback onAddSchedule;
|
||||||
final String category;
|
final String category;
|
||||||
|
final String deviceType;
|
||||||
|
|
||||||
const ScheduleManagementUI({
|
const ScheduleManagementUI({
|
||||||
super.key,
|
super.key,
|
||||||
required this.deviceUuid,
|
required this.deviceUuid,
|
||||||
required this.onAddSchedule,
|
required this.onAddSchedule,
|
||||||
|
required this.deviceType,
|
||||||
this.category = 'switch_1',
|
this.category = 'switch_1',
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -25,7 +27,7 @@ class ScheduleManagementUI extends StatelessWidget {
|
|||||||
width: 170,
|
width: 170,
|
||||||
height: 40,
|
height: 40,
|
||||||
child: DefaultButton(
|
child: DefaultButton(
|
||||||
borderColor: ColorsManager.boxColor,
|
borderColor: ColorsManager.grayColor.withOpacity(0.5),
|
||||||
padding: 2,
|
padding: 2,
|
||||||
backgroundColor: ColorsManager.graysColor,
|
backgroundColor: ColorsManager.graysColor,
|
||||||
borderRadius: 15,
|
borderRadius: 15,
|
||||||
@ -44,7 +46,11 @@ class ScheduleManagementUI extends StatelessWidget {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 20),
|
const SizedBox(height: 20),
|
||||||
ScheduleTableWidget(deviceUuid: deviceUuid, category: category),
|
ScheduleTableWidget(
|
||||||
|
deviceUuid: deviceUuid,
|
||||||
|
category: category,
|
||||||
|
deviceType: deviceType,
|
||||||
|
),
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -19,6 +19,8 @@ class ScheduleModeButtons extends StatelessWidget {
|
|||||||
children: [
|
children: [
|
||||||
Expanded(
|
Expanded(
|
||||||
child: DefaultButton(
|
child: DefaultButton(
|
||||||
|
elevation: 2.5,
|
||||||
|
borderRadius: 8,
|
||||||
height: 40,
|
height: 40,
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
Navigator.pop(context);
|
Navigator.pop(context);
|
||||||
@ -33,9 +35,11 @@ class ScheduleModeButtons extends StatelessWidget {
|
|||||||
const SizedBox(width: 20),
|
const SizedBox(width: 20),
|
||||||
Expanded(
|
Expanded(
|
||||||
child: DefaultButton(
|
child: DefaultButton(
|
||||||
|
elevation: 2.5,
|
||||||
|
borderRadius: 8,
|
||||||
height: 40,
|
height: 40,
|
||||||
onPressed: onSave,
|
onPressed: onSave,
|
||||||
backgroundColor: ColorsManager.primaryColor,
|
backgroundColor: ColorsManager.primaryColorWithOpacity,
|
||||||
child: const Text('Save'),
|
child: const Text('Save'),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
@ -7,10 +7,12 @@ import 'package:syncrow_web/utils/extension/build_context_x.dart';
|
|||||||
|
|
||||||
class ScheduleModeSelector extends StatelessWidget {
|
class ScheduleModeSelector extends StatelessWidget {
|
||||||
final ScheduleModes currentMode;
|
final ScheduleModes currentMode;
|
||||||
|
final String countdownCode;
|
||||||
|
|
||||||
const ScheduleModeSelector({
|
const ScheduleModeSelector({
|
||||||
super.key,
|
super.key,
|
||||||
required this.currentMode,
|
required this.currentMode,
|
||||||
|
required this.countdownCode,
|
||||||
});
|
});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@ -33,12 +35,12 @@ class ScheduleModeSelector extends StatelessWidget {
|
|||||||
),
|
),
|
||||||
const SizedBox(height: 4),
|
const SizedBox(height: 4),
|
||||||
Row(
|
Row(
|
||||||
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
|
|
||||||
children: [
|
children: [
|
||||||
_buildRadioTile(
|
_buildRadioTile(
|
||||||
context, 'Countdown', ScheduleModes.countdown, currentMode),
|
context, 'Countdown', ScheduleModes.countdown, currentMode),
|
||||||
_buildRadioTile(
|
_buildRadioTile(
|
||||||
context, 'Schedule', ScheduleModes.schedule, currentMode),
|
context, 'Schedule', ScheduleModes.schedule, currentMode),
|
||||||
|
const Spacer(flex: 1),
|
||||||
// _buildRadioTile(
|
// _buildRadioTile(
|
||||||
// context, 'Circulate', ScheduleModes.circulate, currentMode),
|
// context, 'Circulate', ScheduleModes.circulate, currentMode),
|
||||||
// _buildRadioTile(
|
// _buildRadioTile(
|
||||||
@ -63,6 +65,7 @@ class ScheduleModeSelector extends StatelessWidget {
|
|||||||
style: context.textTheme.bodySmall!.copyWith(
|
style: context.textTheme.bodySmall!.copyWith(
|
||||||
fontSize: 13,
|
fontSize: 13,
|
||||||
color: ColorsManager.blackColor,
|
color: ColorsManager.blackColor,
|
||||||
|
fontWeight: FontWeight.w400,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
leading: Radio<ScheduleModes>(
|
leading: Radio<ScheduleModes>(
|
||||||
@ -71,7 +74,8 @@ class ScheduleModeSelector extends StatelessWidget {
|
|||||||
onChanged: (ScheduleModes? value) {
|
onChanged: (ScheduleModes? value) {
|
||||||
if (value != null) {
|
if (value != null) {
|
||||||
context.read<ScheduleBloc>().add(
|
context.read<ScheduleBloc>().add(
|
||||||
UpdateScheduleModeEvent(scheduleMode: value),
|
UpdateScheduleModeEvent(
|
||||||
|
scheduleMode: value, countdownCode: countdownCode),
|
||||||
);
|
);
|
||||||
if (value == ScheduleModes.schedule) {
|
if (value == ScheduleModes.schedule) {
|
||||||
context.read<ScheduleBloc>().add(
|
context.read<ScheduleBloc>().add(
|
||||||
|
@ -12,11 +12,13 @@ import 'package:syncrow_web/utils/format_date_time.dart';
|
|||||||
class ScheduleTableWidget extends StatelessWidget {
|
class ScheduleTableWidget extends StatelessWidget {
|
||||||
final String deviceUuid;
|
final String deviceUuid;
|
||||||
final String category;
|
final String category;
|
||||||
|
final String deviceType;
|
||||||
|
|
||||||
const ScheduleTableWidget({
|
const ScheduleTableWidget({
|
||||||
super.key,
|
super.key,
|
||||||
required this.deviceUuid,
|
required this.deviceUuid,
|
||||||
this.category = 'switch_1',
|
this.category = 'switch_1',
|
||||||
|
required this.deviceType,
|
||||||
});
|
});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@ -25,13 +27,14 @@ class ScheduleTableWidget extends StatelessWidget {
|
|||||||
create: (_) => ScheduleBloc(
|
create: (_) => ScheduleBloc(
|
||||||
deviceId: deviceUuid,
|
deviceId: deviceUuid,
|
||||||
)..add(ScheduleGetEvent(category: category)),
|
)..add(ScheduleGetEvent(category: category)),
|
||||||
child: _ScheduleTableView(),
|
child: _ScheduleTableView(deviceType),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class _ScheduleTableView extends StatelessWidget {
|
class _ScheduleTableView extends StatelessWidget {
|
||||||
const _ScheduleTableView();
|
final String deviceType;
|
||||||
|
const _ScheduleTableView(this.deviceType);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
@ -81,7 +84,7 @@ class _ScheduleTableView extends StatelessWidget {
|
|||||||
bottomLeft: Radius.circular(20),
|
bottomLeft: Radius.circular(20),
|
||||||
bottomRight: Radius.circular(20)),
|
bottomRight: Radius.circular(20)),
|
||||||
),
|
),
|
||||||
child: _buildTableBody(state.schedules, context));
|
child: _buildTableBody(state.schedules, context, deviceType));
|
||||||
}
|
}
|
||||||
if (state is ScheduleError) {
|
if (state is ScheduleError) {
|
||||||
return Center(child: Text(state.error));
|
return Center(child: Text(state.error));
|
||||||
@ -123,7 +126,8 @@ class _ScheduleTableView extends StatelessWidget {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildTableBody(List<ScheduleModel> schedules, BuildContext context) {
|
Widget _buildTableBody(
|
||||||
|
List<ScheduleModel> schedules, BuildContext context, String deviceType) {
|
||||||
return SizedBox(
|
return SizedBox(
|
||||||
height: 200,
|
height: 200,
|
||||||
child: SingleChildScrollView(
|
child: SingleChildScrollView(
|
||||||
@ -132,7 +136,8 @@ class _ScheduleTableView extends StatelessWidget {
|
|||||||
defaultVerticalAlignment: TableCellVerticalAlignment.middle,
|
defaultVerticalAlignment: TableCellVerticalAlignment.middle,
|
||||||
children: [
|
children: [
|
||||||
for (int i = 0; i < schedules.length; i++)
|
for (int i = 0; i < schedules.length; i++)
|
||||||
_buildScheduleRow(schedules[i], i, context),
|
_buildScheduleRow(schedules[i], i, context,
|
||||||
|
deviceType: deviceType),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@ -155,25 +160,19 @@ class _ScheduleTableView extends StatelessWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
TableRow _buildScheduleRow(
|
TableRow _buildScheduleRow(
|
||||||
ScheduleModel schedule, int index, BuildContext context) {
|
ScheduleModel schedule, int index, BuildContext context,
|
||||||
|
{required String deviceType}) {
|
||||||
return TableRow(
|
return TableRow(
|
||||||
children: [
|
children: [
|
||||||
Center(
|
Center(
|
||||||
child: GestureDetector(
|
child: GestureDetector(
|
||||||
behavior: HitTestBehavior.opaque,
|
behavior: HitTestBehavior.opaque,
|
||||||
onTap: () {
|
onTap: () {
|
||||||
bool temp;
|
|
||||||
if (schedule.category == 'CUR_2') {
|
|
||||||
temp = schedule.function.value == 'open' ? true : false;
|
|
||||||
} else {
|
|
||||||
temp = schedule.function.value as bool;
|
|
||||||
}
|
|
||||||
context.read<ScheduleBloc>().add(
|
context.read<ScheduleBloc>().add(
|
||||||
ScheduleUpdateEntryEvent(
|
ScheduleUpdateEntryEvent(
|
||||||
category: schedule.category,
|
category: schedule.category,
|
||||||
scheduleId: schedule.scheduleId,
|
scheduleId: schedule.scheduleId,
|
||||||
functionOn: temp,
|
functionOn: schedule.function.value,
|
||||||
// schedule.function.value,
|
|
||||||
enable: !schedule.enable,
|
enable: !schedule.enable,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
@ -195,8 +194,9 @@ class _ScheduleTableView extends StatelessWidget {
|
|||||||
child: Text(_getSelectedDays(
|
child: Text(_getSelectedDays(
|
||||||
ScheduleModel.parseSelectedDays(schedule.days)))),
|
ScheduleModel.parseSelectedDays(schedule.days)))),
|
||||||
Center(child: Text(formatIsoStringToTime(schedule.time, context))),
|
Center(child: Text(formatIsoStringToTime(schedule.time, context))),
|
||||||
if (schedule.category == 'CUR_2')
|
if (deviceType == 'CUR_2')
|
||||||
Center(child: Text(schedule.function.value))
|
Center(
|
||||||
|
child: Text(schedule.function.value == true ? 'open' : 'close'))
|
||||||
else
|
else
|
||||||
Center(child: Text(schedule.function.value ? 'On' : 'Off')),
|
Center(child: Text(schedule.function.value ? 'On' : 'Off')),
|
||||||
Center(
|
Center(
|
||||||
@ -206,14 +206,14 @@ class _ScheduleTableView extends StatelessWidget {
|
|||||||
TextButton(
|
TextButton(
|
||||||
style: TextButton.styleFrom(padding: EdgeInsets.zero),
|
style: TextButton.styleFrom(padding: EdgeInsets.zero),
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
ScheduleDialogHelper.showAddScheduleDialog(
|
ScheduleDialogHelper.showAddScheduleDialog(context,
|
||||||
context,
|
|
||||||
schedule: ScheduleEntry.fromScheduleModel(schedule),
|
schedule: ScheduleEntry.fromScheduleModel(schedule),
|
||||||
isEdit: true,
|
isEdit: true,
|
||||||
).then((updatedSchedule) {
|
deviceType: deviceType)
|
||||||
|
.then((updatedSchedule) {
|
||||||
if (updatedSchedule != null) {
|
if (updatedSchedule != null) {
|
||||||
bool temp;
|
bool temp;
|
||||||
if (schedule.category == 'CUR_2') {
|
if (deviceType == 'CUR_2') {
|
||||||
updatedSchedule.function.value == 'open'
|
updatedSchedule.function.value == 'open'
|
||||||
? temp = true
|
? temp = true
|
||||||
: temp = false;
|
: temp = false;
|
||||||
@ -222,6 +222,7 @@ class _ScheduleTableView extends StatelessWidget {
|
|||||||
}
|
}
|
||||||
context.read<ScheduleBloc>().add(
|
context.read<ScheduleBloc>().add(
|
||||||
ScheduleEditEvent(
|
ScheduleEditEvent(
|
||||||
|
deviceType: deviceType,
|
||||||
scheduleId: schedule.scheduleId,
|
scheduleId: schedule.scheduleId,
|
||||||
category: schedule.category,
|
category: schedule.category,
|
||||||
time: updatedSchedule.time,
|
time: updatedSchedule.time,
|
||||||
|
@ -41,7 +41,7 @@ class ThreeGangGlassSwitchBloc
|
|||||||
emit(ThreeGangGlassSwitchLoading());
|
emit(ThreeGangGlassSwitchLoading());
|
||||||
try {
|
try {
|
||||||
final status = await DevicesManagementApi().getDeviceStatus(event.deviceId);
|
final status = await DevicesManagementApi().getDeviceStatus(event.deviceId);
|
||||||
_listenToChanges(event.deviceId, emit);
|
_listenToChanges(event.deviceId);
|
||||||
deviceStatus =
|
deviceStatus =
|
||||||
ThreeGangGlassStatusModel.fromJson(event.deviceId, status.status);
|
ThreeGangGlassStatusModel.fromJson(event.deviceId, status.status);
|
||||||
emit(ThreeGangGlassSwitchStatusLoaded(deviceStatus));
|
emit(ThreeGangGlassSwitchStatusLoaded(deviceStatus));
|
||||||
@ -50,42 +50,28 @@ class ThreeGangGlassSwitchBloc
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void _listenToChanges(
|
StreamSubscription<DatabaseEvent>? _deviceStatusSubscription;
|
||||||
String deviceId,
|
|
||||||
Emitter<ThreeGangGlassSwitchState> emit,
|
void _listenToChanges(String deviceId) {
|
||||||
) {
|
|
||||||
try {
|
try {
|
||||||
final ref = FirebaseDatabase.instance.ref('device-status/$deviceId');
|
final ref = FirebaseDatabase.instance.ref('device-status/$deviceId');
|
||||||
final stream = ref.onValue;
|
_deviceStatusSubscription = ref.onValue.listen((DatabaseEvent event) async {
|
||||||
|
if (event.snapshot.value == null) return;
|
||||||
|
|
||||||
stream.listen((DatabaseEvent event) {
|
final usersMap = event.snapshot.value! as Map<dynamic, dynamic>;
|
||||||
final data = event.snapshot.value as Map<dynamic, dynamic>?;
|
|
||||||
if (data == null) return;
|
|
||||||
|
|
||||||
final statusList = <Status>[];
|
final statusList = <Status>[];
|
||||||
if (data['status'] != null) {
|
|
||||||
for (var element in data['status']) {
|
usersMap['status'].forEach((element) {
|
||||||
statusList.add(
|
statusList.add(Status(code: element['code'], value: element['value']));
|
||||||
Status(
|
|
||||||
code: element['code'].toString(),
|
|
||||||
value: element['value'].toString(),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (statusList.isNotEmpty) {
|
|
||||||
final newStatus = ThreeGangGlassStatusModel.fromJson(deviceId, statusList);
|
|
||||||
if (newStatus != deviceStatus) {
|
|
||||||
deviceStatus = newStatus;
|
|
||||||
if (!isClosed) {
|
|
||||||
add(StatusUpdated(deviceStatus));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
} catch (e) {
|
|
||||||
emit(ThreeGangGlassSwitchError('Failed to listen to changes: $e'));
|
deviceStatus =
|
||||||
}
|
ThreeGangGlassStatusModel.fromJson(usersMap['productUuid'], statusList);
|
||||||
|
|
||||||
|
add(StatusUpdated(deviceStatus));
|
||||||
|
});
|
||||||
|
} catch (_) {}
|
||||||
}
|
}
|
||||||
|
|
||||||
void _onStatusUpdated(
|
void _onStatusUpdated(
|
||||||
@ -184,4 +170,10 @@ class ThreeGangGlassSwitchBloc
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<void> close() {
|
||||||
|
_deviceStatusSubscription?.cancel();
|
||||||
|
return super.close();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -111,6 +111,8 @@ class ThreeGangGlassSwitchControlView extends StatelessWidget
|
|||||||
child: BuildScheduleView(
|
child: BuildScheduleView(
|
||||||
category: 'switch_1',
|
category: 'switch_1',
|
||||||
deviceUuid: deviceId,
|
deviceUuid: deviceId,
|
||||||
|
countdownCode: 'countdown_1',
|
||||||
|
deviceType: '3GT',
|
||||||
),
|
),
|
||||||
));
|
));
|
||||||
},
|
},
|
||||||
@ -127,6 +129,8 @@ class ThreeGangGlassSwitchControlView extends StatelessWidget
|
|||||||
child: BuildScheduleView(
|
child: BuildScheduleView(
|
||||||
category: 'switch_2',
|
category: 'switch_2',
|
||||||
deviceUuid: deviceId,
|
deviceUuid: deviceId,
|
||||||
|
countdownCode: 'countdown_2',
|
||||||
|
deviceType: '3GT',
|
||||||
),
|
),
|
||||||
));
|
));
|
||||||
},
|
},
|
||||||
@ -143,6 +147,8 @@ class ThreeGangGlassSwitchControlView extends StatelessWidget
|
|||||||
child: BuildScheduleView(
|
child: BuildScheduleView(
|
||||||
category: 'switch_3',
|
category: 'switch_3',
|
||||||
deviceUuid: deviceId,
|
deviceUuid: deviceId,
|
||||||
|
countdownCode: 'countdown_3',
|
||||||
|
deviceType: '3GT',
|
||||||
),
|
),
|
||||||
));
|
));
|
||||||
},
|
},
|
||||||
|
@ -102,6 +102,8 @@ class LivingRoomDeviceControlsView extends StatelessWidget
|
|||||||
child: BuildScheduleView(
|
child: BuildScheduleView(
|
||||||
deviceUuid: deviceId,
|
deviceUuid: deviceId,
|
||||||
category: 'switch_1',
|
category: 'switch_1',
|
||||||
|
countdownCode: 'countdown_1',
|
||||||
|
deviceType: '3G',
|
||||||
),
|
),
|
||||||
));
|
));
|
||||||
},
|
},
|
||||||
@ -118,6 +120,8 @@ class LivingRoomDeviceControlsView extends StatelessWidget
|
|||||||
child: BuildScheduleView(
|
child: BuildScheduleView(
|
||||||
deviceUuid: deviceId,
|
deviceUuid: deviceId,
|
||||||
category: 'switch_2',
|
category: 'switch_2',
|
||||||
|
countdownCode: 'countdown_2',
|
||||||
|
deviceType: '3G',
|
||||||
),
|
),
|
||||||
));
|
));
|
||||||
},
|
},
|
||||||
@ -134,6 +138,8 @@ class LivingRoomDeviceControlsView extends StatelessWidget
|
|||||||
child: BuildScheduleView(
|
child: BuildScheduleView(
|
||||||
deviceUuid: deviceId,
|
deviceUuid: deviceId,
|
||||||
category: 'switch_3',
|
category: 'switch_3',
|
||||||
|
countdownCode: 'countdown_3',
|
||||||
|
deviceType: '3G',
|
||||||
),
|
),
|
||||||
));
|
));
|
||||||
},
|
},
|
||||||
|
@ -1,5 +1,4 @@
|
|||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
import 'dart:developer';
|
|
||||||
|
|
||||||
import 'package:bloc/bloc.dart';
|
import 'package:bloc/bloc.dart';
|
||||||
import 'package:equatable/equatable.dart';
|
import 'package:equatable/equatable.dart';
|
||||||
@ -51,29 +50,28 @@ class TwoGangGlassSwitchBloc
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
StreamSubscription<DatabaseEvent>? _deviceStatusSubscription;
|
||||||
|
|
||||||
void _listenToChanges(String deviceId) {
|
void _listenToChanges(String deviceId) {
|
||||||
try {
|
try {
|
||||||
final ref = FirebaseDatabase.instance.ref(
|
final ref = FirebaseDatabase.instance.ref('device-status/$deviceId');
|
||||||
'device-status/$deviceId',
|
_deviceStatusSubscription = ref.onValue.listen((DatabaseEvent event) async {
|
||||||
);
|
if (event.snapshot.value == null) return;
|
||||||
|
|
||||||
ref.onValue.listen((event) {
|
final usersMap = event.snapshot.value! as Map<dynamic, dynamic>;
|
||||||
final eventsMap = event.snapshot.value as Map<dynamic, dynamic>;
|
|
||||||
|
|
||||||
List<Status> statusList = [];
|
final statusList = <Status>[];
|
||||||
eventsMap['status'].forEach((element) {
|
|
||||||
|
usersMap['status'].forEach((element) {
|
||||||
statusList.add(Status(code: element['code'], value: element['value']));
|
statusList.add(Status(code: element['code'], value: element['value']));
|
||||||
});
|
});
|
||||||
|
|
||||||
deviceStatus = TwoGangGlassStatusModel.fromJson(deviceId, statusList);
|
deviceStatus =
|
||||||
|
TwoGangGlassStatusModel.fromJson(usersMap['productUuid'], statusList);
|
||||||
|
|
||||||
add(StatusUpdated(deviceStatus));
|
add(StatusUpdated(deviceStatus));
|
||||||
});
|
});
|
||||||
} catch (_) {
|
} catch (_) {}
|
||||||
log(
|
|
||||||
'Error listening to changes',
|
|
||||||
name: 'TwoGangGlassSwitchBloc._listenToChanges',
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _onControl(
|
Future<void> _onControl(
|
||||||
@ -170,4 +168,10 @@ class TwoGangGlassSwitchBloc
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<void> close() {
|
||||||
|
_deviceStatusSubscription?.cancel();
|
||||||
|
return super.close();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -102,6 +102,8 @@ class TwoGangGlassSwitchControlView extends StatelessWidget
|
|||||||
builder: (ctx) => BlocProvider.value(
|
builder: (ctx) => BlocProvider.value(
|
||||||
value: BlocProvider.of<TwoGangGlassSwitchBloc>(context),
|
value: BlocProvider.of<TwoGangGlassSwitchBloc>(context),
|
||||||
child: BuildScheduleView(
|
child: BuildScheduleView(
|
||||||
|
deviceType: '2GT',
|
||||||
|
countdownCode: 'countdown_1',
|
||||||
deviceUuid: deviceId,
|
deviceUuid: deviceId,
|
||||||
category: 'switch_1',
|
category: 'switch_1',
|
||||||
),
|
),
|
||||||
@ -118,6 +120,8 @@ class TwoGangGlassSwitchControlView extends StatelessWidget
|
|||||||
builder: (ctx) => BlocProvider.value(
|
builder: (ctx) => BlocProvider.value(
|
||||||
value: BlocProvider.of<TwoGangGlassSwitchBloc>(context),
|
value: BlocProvider.of<TwoGangGlassSwitchBloc>(context),
|
||||||
child: BuildScheduleView(
|
child: BuildScheduleView(
|
||||||
|
deviceType: '2GT',
|
||||||
|
countdownCode: 'countdown_2',
|
||||||
deviceUuid: deviceId,
|
deviceUuid: deviceId,
|
||||||
category: 'switch_2',
|
category: 'switch_2',
|
||||||
),
|
),
|
||||||
|
@ -97,6 +97,8 @@ class TwoGangBatchControlView extends StatelessWidget
|
|||||||
child: BuildScheduleView(
|
child: BuildScheduleView(
|
||||||
category: 'switch_1',
|
category: 'switch_1',
|
||||||
deviceUuid: deviceIds.first,
|
deviceUuid: deviceIds.first,
|
||||||
|
countdownCode: 'countdown_1',
|
||||||
|
deviceType: '2G',
|
||||||
),
|
),
|
||||||
));
|
));
|
||||||
},
|
},
|
||||||
@ -114,6 +116,8 @@ class TwoGangBatchControlView extends StatelessWidget
|
|||||||
child: BuildScheduleView(
|
child: BuildScheduleView(
|
||||||
category: 'switch_2',
|
category: 'switch_2',
|
||||||
deviceUuid: deviceIds.first,
|
deviceUuid: deviceIds.first,
|
||||||
|
countdownCode: 'countdown_2',
|
||||||
|
deviceType: '2G',
|
||||||
),
|
),
|
||||||
));
|
));
|
||||||
},
|
},
|
||||||
@ -121,10 +125,7 @@ class TwoGangBatchControlView extends StatelessWidget
|
|||||||
subtitle: 'Scheduling',
|
subtitle: 'Scheduling',
|
||||||
iconPath: Assets.scheduling,
|
iconPath: Assets.scheduling,
|
||||||
),
|
),
|
||||||
// FirmwareUpdateWidget(
|
|
||||||
// deviceId: deviceIds.first,
|
|
||||||
// version: 12,
|
|
||||||
// ),
|
|
||||||
FactoryResetWidget(callFactoryReset: () {
|
FactoryResetWidget(callFactoryReset: () {
|
||||||
context.read<TwoGangSwitchBloc>().add(
|
context.read<TwoGangSwitchBloc>().add(
|
||||||
TwoGangFactoryReset(
|
TwoGangFactoryReset(
|
||||||
|
@ -103,6 +103,8 @@ class TwoGangDeviceControlView extends StatelessWidget
|
|||||||
child: BuildScheduleView(
|
child: BuildScheduleView(
|
||||||
deviceUuid: deviceId,
|
deviceUuid: deviceId,
|
||||||
category: 'switch_1',
|
category: 'switch_1',
|
||||||
|
countdownCode: 'countdown_1',
|
||||||
|
deviceType: '2G',
|
||||||
),
|
),
|
||||||
));
|
));
|
||||||
},
|
},
|
||||||
@ -125,6 +127,8 @@ class TwoGangDeviceControlView extends StatelessWidget
|
|||||||
child: BuildScheduleView(
|
child: BuildScheduleView(
|
||||||
deviceUuid: deviceId,
|
deviceUuid: deviceId,
|
||||||
category: 'switch_2',
|
category: 'switch_2',
|
||||||
|
countdownCode: 'countdown_2',
|
||||||
|
deviceType: '2G',
|
||||||
),
|
),
|
||||||
));
|
));
|
||||||
},
|
},
|
||||||
|
@ -1,6 +1,8 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:syncrow_web/pages/device_managment/schedule_device/schedule_widgets/schedule_mode_buttons.dart';
|
||||||
import 'package:syncrow_web/pages/device_managment/water_heater/models/schedule_entry.dart';
|
import 'package:syncrow_web/pages/device_managment/water_heater/models/schedule_entry.dart';
|
||||||
import 'package:syncrow_web/pages/device_managment/all_devices/models/device_status.dart';
|
import 'package:syncrow_web/pages/device_managment/all_devices/models/device_status.dart';
|
||||||
|
import 'package:syncrow_web/utils/color_manager.dart';
|
||||||
|
|
||||||
class ScheduleDialogHelper {
|
class ScheduleDialogHelper {
|
||||||
static const List<String> allDays = [
|
static const List<String> allDays = [
|
||||||
@ -18,9 +20,10 @@ class ScheduleDialogHelper {
|
|||||||
ScheduleEntry? schedule,
|
ScheduleEntry? schedule,
|
||||||
bool isEdit = false,
|
bool isEdit = false,
|
||||||
String? code,
|
String? code,
|
||||||
|
required String deviceType,
|
||||||
}) {
|
}) {
|
||||||
bool temp;
|
bool temp;
|
||||||
if (schedule?.category == 'CUR_2') {
|
if (deviceType == 'CUR_2') {
|
||||||
temp = schedule!.function.value == 'open' ? true : false;
|
temp = schedule!.function.value == 'open' ? true : false;
|
||||||
} else {
|
} else {
|
||||||
temp = schedule!.function.value;
|
temp = schedule!.function.value;
|
||||||
@ -55,8 +58,9 @@ class ScheduleDialogHelper {
|
|||||||
Text(
|
Text(
|
||||||
isEdit ? 'Edit Schedule' : 'Add Schedule',
|
isEdit ? 'Edit Schedule' : 'Add Schedule',
|
||||||
style: Theme.of(context).textTheme.titleLarge!.copyWith(
|
style: Theme.of(context).textTheme.titleLarge!.copyWith(
|
||||||
color: Colors.blue,
|
color: ColorsManager.primaryColorWithOpacity,
|
||||||
fontWeight: FontWeight.bold,
|
fontWeight: FontWeight.w700,
|
||||||
|
fontSize: 30,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(),
|
const SizedBox(),
|
||||||
@ -68,9 +72,9 @@ class ScheduleDialogHelper {
|
|||||||
height: 40,
|
height: 40,
|
||||||
child: ElevatedButton(
|
child: ElevatedButton(
|
||||||
style: ElevatedButton.styleFrom(
|
style: ElevatedButton.styleFrom(
|
||||||
backgroundColor: Colors.grey[200],
|
backgroundColor: ColorsManager.boxColor,
|
||||||
shape: RoundedRectangleBorder(
|
shape: RoundedRectangleBorder(
|
||||||
borderRadius: BorderRadius.circular(15),
|
borderRadius: BorderRadius.circular(8),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
onPressed: () async {
|
onPressed: () async {
|
||||||
@ -103,48 +107,32 @@ class ScheduleDialogHelper {
|
|||||||
setState(() => selectedDays[i] = v);
|
setState(() => selectedDays[i] = v);
|
||||||
}),
|
}),
|
||||||
const SizedBox(height: 16),
|
const SizedBox(height: 16),
|
||||||
_buildFunctionSwitch(schedule!.category, ctx, functionOn!,
|
_buildFunctionSwitch(deviceType, ctx, functionOn!, (v) {
|
||||||
(v) {
|
|
||||||
setState(() => functionOn = v);
|
setState(() => functionOn = v);
|
||||||
}),
|
}),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
actions: [
|
actions: [
|
||||||
SizedBox(
|
ScheduleModeButtons(
|
||||||
width: 100,
|
onSave: () {
|
||||||
child: OutlinedButton(
|
|
||||||
onPressed: () {
|
|
||||||
Navigator.pop(ctx, null);
|
|
||||||
},
|
|
||||||
child: const Text('Cancel'),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
SizedBox(
|
|
||||||
width: 100,
|
|
||||||
child: ElevatedButton(
|
|
||||||
onPressed: () {
|
|
||||||
dynamic temp;
|
dynamic temp;
|
||||||
if (schedule?.category == 'CUR_2') {
|
if (deviceType == 'CUR_2') {
|
||||||
temp = functionOn! ? 'open' : 'close';
|
temp = functionOn! ? 'open' : 'close';
|
||||||
} else {
|
} else {
|
||||||
temp = functionOn;
|
temp = functionOn;
|
||||||
}
|
}
|
||||||
print(temp);
|
|
||||||
final entry = ScheduleEntry(
|
final entry = ScheduleEntry(
|
||||||
category: schedule?.category ?? 'switch_1',
|
category: schedule?.category ?? 'switch_1',
|
||||||
time: _formatTimeOfDayToISO(selectedTime),
|
time: _formatTimeOfDayToISO(selectedTime),
|
||||||
function: Status(
|
function: Status(
|
||||||
code: code ?? 'switch_1',
|
code: code ?? 'switch_1',
|
||||||
value: temp,
|
value: temp,
|
||||||
// functionOn,
|
|
||||||
),
|
),
|
||||||
days: _convertSelectedDaysToStrings(selectedDays),
|
days: _convertSelectedDaysToStrings(selectedDays),
|
||||||
scheduleId: schedule?.scheduleId,
|
scheduleId: schedule.scheduleId,
|
||||||
);
|
);
|
||||||
Navigator.pop(ctx, entry);
|
Navigator.pop(ctx, entry);
|
||||||
},
|
},
|
||||||
child: const Text('Save'),
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
|
@ -84,6 +84,8 @@ class WaterHeaterDeviceControlView extends StatelessWidget
|
|||||||
child: BuildScheduleView(
|
child: BuildScheduleView(
|
||||||
deviceUuid: device.uuid ?? '',
|
deviceUuid: device.uuid ?? '',
|
||||||
category: 'switch_1',
|
category: 'switch_1',
|
||||||
|
countdownCode: 'countdown_1',
|
||||||
|
deviceType: 'WH',
|
||||||
),
|
),
|
||||||
));
|
));
|
||||||
},
|
},
|
||||||
|
@ -105,7 +105,7 @@ class HomeBloc extends Bloc<HomeEvent, HomeState> {
|
|||||||
color: const Color(0xFF0026A2),
|
color: const Color(0xFF0026A2),
|
||||||
),
|
),
|
||||||
HomeItemModel(
|
HomeItemModel(
|
||||||
title: 'Devices Management',
|
title: 'Device Management',
|
||||||
icon: Assets.devicesIcon,
|
icon: Assets.devicesIcon,
|
||||||
active: true,
|
active: true,
|
||||||
onPress: (context) {
|
onPress: (context) {
|
||||||
|
@ -34,7 +34,8 @@ class _AddNewUserDialogState extends State<AddNewUserDialog> {
|
|||||||
return Dialog(
|
return Dialog(
|
||||||
child: Container(
|
child: Container(
|
||||||
decoration: const BoxDecoration(
|
decoration: const BoxDecoration(
|
||||||
color: Colors.white, borderRadius: BorderRadius.all(Radius.circular(20))),
|
color: Colors.white,
|
||||||
|
borderRadius: BorderRadius.all(Radius.circular(20))),
|
||||||
width: 900,
|
width: 900,
|
||||||
child: Column(
|
child: Column(
|
||||||
children: [
|
children: [
|
||||||
@ -63,7 +64,8 @@ class _AddNewUserDialogState extends State<AddNewUserDialog> {
|
|||||||
children: [
|
children: [
|
||||||
_buildStep1Indicator(1, "Basics", _blocRole),
|
_buildStep1Indicator(1, "Basics", _blocRole),
|
||||||
_buildStep2Indicator(2, "Spaces", _blocRole),
|
_buildStep2Indicator(2, "Spaces", _blocRole),
|
||||||
_buildStep3Indicator(3, "Role & Permissions", _blocRole),
|
_buildStep3Indicator(
|
||||||
|
3, "Role & Permissions", _blocRole),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@ -105,18 +107,32 @@ class _AddNewUserDialogState extends State<AddNewUserDialog> {
|
|||||||
),
|
),
|
||||||
InkWell(
|
InkWell(
|
||||||
onTap: () {
|
onTap: () {
|
||||||
|
final isBasicsStep = currentStep == 1;
|
||||||
|
|
||||||
|
if (isBasicsStep) {
|
||||||
|
// Validate the form first
|
||||||
|
final isValid = _blocRole.formKey.currentState
|
||||||
|
?.validate() ??
|
||||||
|
false;
|
||||||
|
|
||||||
|
if (!isValid)
|
||||||
|
return; // Stop if form is not valid
|
||||||
|
}
|
||||||
_blocRole.add(const CheckEmailEvent());
|
_blocRole.add(const CheckEmailEvent());
|
||||||
|
|
||||||
setState(() {
|
setState(() {
|
||||||
if (currentStep < 3) {
|
if (currentStep < 3) {
|
||||||
currentStep++;
|
currentStep++;
|
||||||
if (currentStep == 2) {
|
if (currentStep == 2) {
|
||||||
_blocRole.add(const CheckStepStatus(isEditUser: false));
|
_blocRole.add(const CheckStepStatus(
|
||||||
|
isEditUser: false));
|
||||||
} else if (currentStep == 3) {
|
} else if (currentStep == 3) {
|
||||||
_blocRole.add(const CheckSpacesStepStatus());
|
_blocRole
|
||||||
|
.add(const CheckSpacesStepStatus());
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
_blocRole.add(SendInviteUsers(context: context));
|
_blocRole
|
||||||
|
.add(SendInviteUsers(context: context));
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
@ -124,8 +140,11 @@ class _AddNewUserDialogState extends State<AddNewUserDialog> {
|
|||||||
currentStep < 3 ? "Next" : "Save",
|
currentStep < 3 ? "Next" : "Save",
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
color: (_blocRole.isCompleteSpaces == false ||
|
color: (_blocRole.isCompleteSpaces == false ||
|
||||||
_blocRole.isCompleteBasics == false ||
|
_blocRole.isCompleteBasics ==
|
||||||
_blocRole.isCompleteRolePermissions == false) &&
|
false ||
|
||||||
|
_blocRole
|
||||||
|
.isCompleteRolePermissions ==
|
||||||
|
false) &&
|
||||||
currentStep == 3
|
currentStep == 3
|
||||||
? ColorsManager.grayColor
|
? ColorsManager.grayColor
|
||||||
: ColorsManager.secondaryColor),
|
: ColorsManager.secondaryColor),
|
||||||
@ -143,7 +162,7 @@ class _AddNewUserDialogState extends State<AddNewUserDialog> {
|
|||||||
Widget _getFormContent() {
|
Widget _getFormContent() {
|
||||||
switch (currentStep) {
|
switch (currentStep) {
|
||||||
case 1:
|
case 1:
|
||||||
return const BasicsView(
|
return BasicsView(
|
||||||
userId: '',
|
userId: '',
|
||||||
);
|
);
|
||||||
case 2:
|
case 2:
|
||||||
@ -196,8 +215,12 @@ class _AddNewUserDialogState extends State<AddNewUserDialog> {
|
|||||||
label,
|
label,
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
fontSize: 16,
|
fontSize: 16,
|
||||||
color: currentStep == step ? ColorsManager.blackColor : ColorsManager.greyColor,
|
color: currentStep == step
|
||||||
fontWeight: currentStep == step ? FontWeight.bold : FontWeight.normal,
|
? ColorsManager.blackColor
|
||||||
|
: ColorsManager.greyColor,
|
||||||
|
fontWeight: currentStep == step
|
||||||
|
? FontWeight.bold
|
||||||
|
: FontWeight.normal,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
@ -260,8 +283,12 @@ class _AddNewUserDialogState extends State<AddNewUserDialog> {
|
|||||||
label,
|
label,
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
fontSize: 16,
|
fontSize: 16,
|
||||||
color: currentStep == step ? ColorsManager.blackColor : ColorsManager.greyColor,
|
color: currentStep == step
|
||||||
fontWeight: currentStep == step ? FontWeight.bold : FontWeight.normal,
|
? ColorsManager.blackColor
|
||||||
|
: ColorsManager.greyColor,
|
||||||
|
fontWeight: currentStep == step
|
||||||
|
? FontWeight.bold
|
||||||
|
: FontWeight.normal,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
@ -318,8 +345,12 @@ class _AddNewUserDialogState extends State<AddNewUserDialog> {
|
|||||||
label,
|
label,
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
fontSize: 16,
|
fontSize: 16,
|
||||||
color: currentStep == step ? ColorsManager.blackColor : ColorsManager.greyColor,
|
color: currentStep == step
|
||||||
fontWeight: currentStep == step ? FontWeight.bold : FontWeight.normal,
|
? ColorsManager.blackColor
|
||||||
|
: ColorsManager.greyColor,
|
||||||
|
fontWeight: currentStep == step
|
||||||
|
? FontWeight.bold
|
||||||
|
: FontWeight.normal,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
|
@ -1,9 +1,12 @@
|
|||||||
|
import 'dart:async';
|
||||||
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||||
import 'package:intl_phone_field/countries.dart';
|
import 'package:intl_phone_field/countries.dart';
|
||||||
import 'package:intl_phone_field/country_picker_dialog.dart';
|
import 'package:intl_phone_field/country_picker_dialog.dart';
|
||||||
import 'package:intl_phone_field/intl_phone_field.dart';
|
import 'package:intl_phone_field/intl_phone_field.dart';
|
||||||
import 'package:syncrow_web/pages/roles_and_permission/users_page/add_user_dialog/bloc/users_bloc.dart';
|
import 'package:syncrow_web/pages/roles_and_permission/users_page/add_user_dialog/bloc/users_bloc.dart';
|
||||||
|
import 'package:syncrow_web/pages/roles_and_permission/users_page/add_user_dialog/bloc/users_event.dart';
|
||||||
import 'package:syncrow_web/pages/roles_and_permission/users_page/add_user_dialog/bloc/users_status.dart';
|
import 'package:syncrow_web/pages/roles_and_permission/users_page/add_user_dialog/bloc/users_status.dart';
|
||||||
import 'package:syncrow_web/utils/color_manager.dart';
|
import 'package:syncrow_web/utils/color_manager.dart';
|
||||||
import 'package:syncrow_web/utils/extension/build_context_x.dart';
|
import 'package:syncrow_web/utils/extension/build_context_x.dart';
|
||||||
@ -11,7 +14,9 @@ import 'package:syncrow_web/utils/style.dart';
|
|||||||
|
|
||||||
class BasicsView extends StatelessWidget {
|
class BasicsView extends StatelessWidget {
|
||||||
final String? userId;
|
final String? userId;
|
||||||
const BasicsView({super.key, this.userId = ''});
|
Timer? _debounce;
|
||||||
|
|
||||||
|
BasicsView({super.key, this.userId = ''});
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return BlocBuilder<UsersBloc, UsersState>(builder: (context, state) {
|
return BlocBuilder<UsersBloc, UsersState>(builder: (context, state) {
|
||||||
@ -21,6 +26,7 @@ class BasicsView extends StatelessWidget {
|
|||||||
}
|
}
|
||||||
return Form(
|
return Form(
|
||||||
key: _blocRole.formKey,
|
key: _blocRole.formKey,
|
||||||
|
autovalidateMode: AutovalidateMode.onUserInteraction,
|
||||||
child: ListView(
|
child: ListView(
|
||||||
shrinkWrap: true,
|
shrinkWrap: true,
|
||||||
children: [
|
children: [
|
||||||
@ -208,6 +214,14 @@ class BasicsView extends StatelessWidget {
|
|||||||
fontSize: 12,
|
fontSize: 12,
|
||||||
color: ColorsManager.textGray),
|
color: ColorsManager.textGray),
|
||||||
),
|
),
|
||||||
|
|
||||||
|
onChanged: (value) {
|
||||||
|
if (_debounce?.isActive ?? false) _debounce!.cancel();
|
||||||
|
_debounce = Timer(const Duration(milliseconds: 800), () {
|
||||||
|
_blocRole.add(const CheckEmailEvent());
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
validator: (value) {
|
validator: (value) {
|
||||||
if (value == null || value.isEmpty) {
|
if (value == null || value.isEmpty) {
|
||||||
return 'Enter Email Address';
|
return 'Enter Email Address';
|
||||||
|
@ -32,15 +32,12 @@ class SpaceDropdown extends StatelessWidget {
|
|||||||
color: ColorsManager.blackColor,
|
color: ColorsManager.blackColor,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
SizedBox(
|
DropdownButton2<String>(
|
||||||
child: Container(
|
|
||||||
height: 40,
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
color: ColorsManager.whiteColors,
|
|
||||||
borderRadius: BorderRadius.circular(10),
|
|
||||||
),
|
|
||||||
child: DropdownButton2<String>(
|
|
||||||
underline: const SizedBox(),
|
underline: const SizedBox(),
|
||||||
|
buttonStyleData: ButtonStyleData(
|
||||||
|
decoration:
|
||||||
|
BoxDecoration(borderRadius: BorderRadius.circular(12)),
|
||||||
|
),
|
||||||
value: selectedValue,
|
value: selectedValue,
|
||||||
items: spaces.map((space) {
|
items: spaces.map((space) {
|
||||||
return DropdownMenuItem<String>(
|
return DropdownMenuItem<String>(
|
||||||
@ -51,17 +48,21 @@ class SpaceDropdown extends StatelessWidget {
|
|||||||
children: [
|
children: [
|
||||||
Text(
|
Text(
|
||||||
' ${space.name}',
|
' ${space.name}',
|
||||||
style:
|
style: Theme.of(context).textTheme.bodyMedium!.copyWith(
|
||||||
Theme.of(context).textTheme.bodyMedium!.copyWith(
|
fontSize: 16,
|
||||||
fontSize: 12,
|
fontWeight: FontWeight.bold,
|
||||||
color: ColorsManager.blackColor,
|
color: selectedValue == space.uuid
|
||||||
|
? ColorsManager.dialogBlueTitle
|
||||||
|
: ColorsManager.blackColor,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
Text(
|
Text(
|
||||||
' ${space.lastThreeParents}',
|
' ${space.lastThreeParents}',
|
||||||
style:
|
style: Theme.of(context).textTheme.bodyMedium!.copyWith(
|
||||||
Theme.of(context).textTheme.bodyMedium!.copyWith(
|
|
||||||
fontSize: 12,
|
fontSize: 12,
|
||||||
|
color: selectedValue == space.uuid
|
||||||
|
? ColorsManager.dialogBlueTitle
|
||||||
|
: ColorsManager.blackColor,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
@ -69,7 +70,10 @@ class SpaceDropdown extends StatelessWidget {
|
|||||||
);
|
);
|
||||||
}).toList(),
|
}).toList(),
|
||||||
onChanged: onChanged,
|
onChanged: onChanged,
|
||||||
style: TextStyle(color: Colors.black),
|
style: TextStyle(
|
||||||
|
color: Colors.black,
|
||||||
|
fontSize: 13,
|
||||||
|
),
|
||||||
hint: Padding(
|
hint: Padding(
|
||||||
padding: const EdgeInsets.only(left: 10),
|
padding: const EdgeInsets.only(left: 10),
|
||||||
child: Text(
|
child: Text(
|
||||||
@ -80,10 +84,9 @@ class SpaceDropdown extends StatelessWidget {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
customButton: Container(
|
customButton: Container(
|
||||||
height: 45,
|
height: 40,
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
border:
|
border: Border.all(color: ColorsManager.textGray, width: 1.0),
|
||||||
Border.all(color: ColorsManager.textGray, width: 1.0),
|
|
||||||
borderRadius: BorderRadius.circular(10),
|
borderRadius: BorderRadius.circular(10),
|
||||||
),
|
),
|
||||||
child: Row(
|
child: Row(
|
||||||
@ -99,8 +102,8 @@ class SpaceDropdown extends StatelessWidget {
|
|||||||
.firstWhere((e) => e.uuid == selectedValue)
|
.firstWhere((e) => e.uuid == selectedValue)
|
||||||
.name
|
.name
|
||||||
: hintMessage,
|
: hintMessage,
|
||||||
style:
|
style: Theme.of(context).textTheme.bodySmall!.copyWith(
|
||||||
Theme.of(context).textTheme.bodySmall!.copyWith(
|
fontSize: 13,
|
||||||
color: selectedValue != null
|
color: selectedValue != null
|
||||||
? Colors.black
|
? Colors.black
|
||||||
: ColorsManager.textGray,
|
: ColorsManager.textGray,
|
||||||
@ -139,8 +142,6 @@ class SpaceDropdown extends StatelessWidget {
|
|||||||
height: 60,
|
height: 60,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
@ -121,7 +121,8 @@ class _RoutineViewCardState extends State<RoutineViewCard> {
|
|||||||
child: SizedBox(
|
child: SizedBox(
|
||||||
width: 16,
|
width: 16,
|
||||||
height: 16,
|
height: 16,
|
||||||
child: CircularProgressIndicator(strokeWidth: 2),
|
child:
|
||||||
|
CircularProgressIndicator(strokeWidth: 2),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
@ -159,7 +160,8 @@ class _RoutineViewCardState extends State<RoutineViewCard> {
|
|||||||
height: iconSize,
|
height: iconSize,
|
||||||
width: iconSize,
|
width: iconSize,
|
||||||
fit: BoxFit.contain,
|
fit: BoxFit.contain,
|
||||||
errorBuilder: (context, error, stackTrace) =>
|
errorBuilder:
|
||||||
|
(context, error, stackTrace) =>
|
||||||
Image.asset(
|
Image.asset(
|
||||||
Assets.logo,
|
Assets.logo,
|
||||||
height: iconSize,
|
height: iconSize,
|
||||||
@ -203,7 +205,8 @@ class _RoutineViewCardState extends State<RoutineViewCard> {
|
|||||||
maxLines: 1,
|
maxLines: 1,
|
||||||
style: context.textTheme.bodySmall?.copyWith(
|
style: context.textTheme.bodySmall?.copyWith(
|
||||||
color: ColorsManager.blackColor,
|
color: ColorsManager.blackColor,
|
||||||
fontSize: widget.isSmallScreenSize(context) ? 10 : 12,
|
fontSize:
|
||||||
|
widget.isSmallScreenSize(context) ? 10 : 12,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
if (widget.spaceName != '')
|
if (widget.spaceName != '')
|
||||||
@ -222,8 +225,9 @@ class _RoutineViewCardState extends State<RoutineViewCard> {
|
|||||||
maxLines: 1,
|
maxLines: 1,
|
||||||
style: context.textTheme.bodySmall?.copyWith(
|
style: context.textTheme.bodySmall?.copyWith(
|
||||||
color: ColorsManager.blackColor,
|
color: ColorsManager.blackColor,
|
||||||
fontSize:
|
fontSize: widget.isSmallScreenSize(context)
|
||||||
widget.isSmallScreenSize(context) ? 10 : 12,
|
? 10
|
||||||
|
: 12,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
|
@ -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;
|
||||||
|
}
|
@ -1,24 +1,39 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
import 'package:syncrow_web/pages/space_management_v2/modules/communities/domain/models/community_model.dart';
|
||||||
import 'package:syncrow_web/pages/space_management_v2/modules/communities/presentation/bloc/communities_bloc.dart';
|
|
||||||
import 'package:syncrow_web/pages/space_management_v2/modules/communities/presentation/communities_tree_selection_bloc/communities_tree_selection_bloc.dart';
|
|
||||||
import 'package:syncrow_web/pages/space_management_v2/modules/create_community/presentation/create_community_dialog.dart';
|
import 'package:syncrow_web/pages/space_management_v2/modules/create_community/presentation/create_community_dialog.dart';
|
||||||
|
import 'package:syncrow_web/pages/space_management_v2/modules/update_community/presentation/edit_community_dialog.dart';
|
||||||
|
|
||||||
abstract final class SpaceManagementCommunityDialogHelper {
|
abstract final class SpaceManagementCommunityDialogHelper {
|
||||||
static void showCreateDialog(BuildContext context) {
|
static void showCreateDialog(BuildContext context) => showDialog<void>(
|
||||||
|
context: context,
|
||||||
|
builder: (_) => const CreateCommunityDialog(),
|
||||||
|
);
|
||||||
|
|
||||||
|
static void showEditDialog(
|
||||||
|
BuildContext context,
|
||||||
|
CommunityModel community,
|
||||||
|
) {
|
||||||
showDialog<void>(
|
showDialog<void>(
|
||||||
context: context,
|
context: context,
|
||||||
builder: (_) => CreateCommunityDialog(
|
builder: (_) => EditCommunityDialog(
|
||||||
title: const SelectableText('Community Name'),
|
community: community,
|
||||||
onCreateCommunity: (community) {
|
parentContext: context,
|
||||||
context.read<CommunitiesBloc>().add(
|
|
||||||
InsertCommunity(community),
|
|
||||||
);
|
|
||||||
context.read<CommunitiesTreeSelectionBloc>().add(
|
|
||||||
SelectCommunityEvent(community: community),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static void showLoadingDialog(BuildContext context) => showDialog<void>(
|
||||||
|
context: context,
|
||||||
|
barrierDismissible: false,
|
||||||
|
builder: (context) => const Center(
|
||||||
|
child: CircularProgressIndicator(),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
static void showSuccessSnackBar(BuildContext context, String message) =>
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
SnackBar(
|
||||||
|
content: Text(message),
|
||||||
|
),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
@ -1,28 +1,29 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
|
||||||
import 'package:syncrow_web/pages/common/buttons/cancel_button.dart';
|
import 'package:syncrow_web/pages/common/buttons/cancel_button.dart';
|
||||||
import 'package:syncrow_web/pages/common/buttons/default_button.dart';
|
import 'package:syncrow_web/pages/common/buttons/default_button.dart';
|
||||||
import 'package:syncrow_web/pages/space_management_v2/modules/create_community/domain/param/create_community_param.dart';
|
|
||||||
import 'package:syncrow_web/pages/space_management_v2/modules/create_community/presentation/bloc/create_community_bloc.dart';
|
|
||||||
import 'package:syncrow_web/pages/space_management_v2/modules/create_community/presentation/create_community_name_text_field.dart';
|
import 'package:syncrow_web/pages/space_management_v2/modules/create_community/presentation/create_community_name_text_field.dart';
|
||||||
import 'package:syncrow_web/utils/color_manager.dart';
|
import 'package:syncrow_web/utils/color_manager.dart';
|
||||||
|
import 'package:syncrow_web/utils/extension/build_context_x.dart';
|
||||||
|
|
||||||
class CreateCommunityDialogWidget extends StatefulWidget {
|
class CommunityDialog extends StatefulWidget {
|
||||||
final String? initialName;
|
final String? initialName;
|
||||||
final Widget title;
|
final Widget title;
|
||||||
|
final void Function(String name) onSubmit;
|
||||||
|
final String? errorMessage;
|
||||||
|
|
||||||
const CreateCommunityDialogWidget({
|
const CommunityDialog({
|
||||||
super.key,
|
|
||||||
required this.title,
|
required this.title,
|
||||||
|
required this.onSubmit,
|
||||||
this.initialName,
|
this.initialName,
|
||||||
|
this.errorMessage,
|
||||||
|
super.key,
|
||||||
});
|
});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
State<CreateCommunityDialogWidget> createState() =>
|
State<CommunityDialog> createState() => _CommunityDialogState();
|
||||||
_CreateCommunityDialogWidgetState();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
class _CreateCommunityDialogWidgetState extends State<CreateCommunityDialogWidget> {
|
class _CommunityDialogState extends State<CommunityDialog> {
|
||||||
late final TextEditingController _nameController;
|
late final TextEditingController _nameController;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@ -63,9 +64,7 @@ class _CreateCommunityDialogWidgetState extends State<CreateCommunityDialogWidge
|
|||||||
child: Form(
|
child: Form(
|
||||||
key: _formKey,
|
key: _formKey,
|
||||||
child: SingleChildScrollView(
|
child: SingleChildScrollView(
|
||||||
child: BlocBuilder<CreateCommunityBloc, CreateCommunityState>(
|
child: Column(
|
||||||
builder: (context, state) {
|
|
||||||
return Column(
|
|
||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
@ -74,24 +73,11 @@ class _CreateCommunityDialogWidgetState extends State<CreateCommunityDialogWidge
|
|||||||
child: widget.title,
|
child: widget.title,
|
||||||
),
|
),
|
||||||
const SizedBox(height: 18),
|
const SizedBox(height: 18),
|
||||||
CreateCommunityNameTextField(
|
CreateCommunityNameTextField(nameController: _nameController),
|
||||||
nameController: _nameController,
|
_buildErrorMessage(),
|
||||||
),
|
|
||||||
if (state case CreateCommunityFailure(:final message))
|
|
||||||
Padding(
|
|
||||||
padding: const EdgeInsets.only(top: 18),
|
|
||||||
child: SelectableText(
|
|
||||||
'* $message',
|
|
||||||
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
|
|
||||||
color: Theme.of(context).colorScheme.error,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
const SizedBox(height: 24),
|
const SizedBox(height: 24),
|
||||||
_buildActionButtons(context),
|
_buildActionButtons(context),
|
||||||
],
|
],
|
||||||
);
|
|
||||||
},
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@ -132,13 +118,22 @@ class _CreateCommunityDialogWidgetState extends State<CreateCommunityDialogWidge
|
|||||||
|
|
||||||
void _onSubmit(BuildContext context) {
|
void _onSubmit(BuildContext context) {
|
||||||
if (_formKey.currentState?.validate() ?? false) {
|
if (_formKey.currentState?.validate() ?? false) {
|
||||||
context.read<CreateCommunityBloc>().add(
|
widget.onSubmit.call(_nameController.text.trim());
|
||||||
CreateCommunity(
|
}
|
||||||
CreateCommunityParam(
|
}
|
||||||
name: _nameController.text.trim(),
|
|
||||||
|
Widget _buildErrorMessage() {
|
||||||
|
return Visibility(
|
||||||
|
visible: widget.errorMessage != null,
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsetsDirectional.symmetric(vertical: 18),
|
||||||
|
child: SelectableText(
|
||||||
|
'* ${widget.errorMessage}',
|
||||||
|
style: context.textTheme.bodyMedium?.copyWith(
|
||||||
|
color: context.theme.colorScheme.error,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
@ -7,25 +7,58 @@ import 'package:syncrow_web/pages/space_management_v2/modules/communities/data/s
|
|||||||
import 'package:syncrow_web/pages/space_management_v2/modules/communities/domain/params/load_communities_param.dart';
|
import 'package:syncrow_web/pages/space_management_v2/modules/communities/domain/params/load_communities_param.dart';
|
||||||
import 'package:syncrow_web/pages/space_management_v2/modules/communities/presentation/bloc/communities_bloc.dart';
|
import 'package:syncrow_web/pages/space_management_v2/modules/communities/presentation/bloc/communities_bloc.dart';
|
||||||
import 'package:syncrow_web/pages/space_management_v2/modules/communities/presentation/communities_tree_selection_bloc/communities_tree_selection_bloc.dart';
|
import 'package:syncrow_web/pages/space_management_v2/modules/communities/presentation/communities_tree_selection_bloc/communities_tree_selection_bloc.dart';
|
||||||
|
import 'package:syncrow_web/pages/space_management_v2/modules/products/data/services/remote_products_service.dart';
|
||||||
|
import 'package:syncrow_web/pages/space_management_v2/modules/products/presentation/bloc/products_bloc.dart';
|
||||||
|
import 'package:syncrow_web/pages/space_management_v2/modules/space_details/data/services/remote_space_details_service.dart';
|
||||||
|
import 'package:syncrow_web/pages/space_management_v2/modules/space_details/data/services/unique_subspaces_decorator.dart';
|
||||||
|
import 'package:syncrow_web/pages/space_management_v2/modules/space_details/presentation/bloc/space_details_bloc.dart';
|
||||||
import 'package:syncrow_web/services/api/http_service.dart';
|
import 'package:syncrow_web/services/api/http_service.dart';
|
||||||
import 'package:syncrow_web/utils/theme/responsive_text_theme.dart';
|
import 'package:syncrow_web/utils/theme/responsive_text_theme.dart';
|
||||||
import 'package:syncrow_web/web_layout/web_scaffold.dart';
|
import 'package:syncrow_web/web_layout/web_scaffold.dart';
|
||||||
|
|
||||||
class SpaceManagementPage extends StatelessWidget {
|
class SpaceManagementPage extends StatefulWidget {
|
||||||
const SpaceManagementPage({super.key});
|
const SpaceManagementPage({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<SpaceManagementPage> createState() => _SpaceManagementPageState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _SpaceManagementPageState extends State<SpaceManagementPage> {
|
||||||
|
late final CommunitiesBloc communitiesBloc;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
communitiesBloc = CommunitiesBloc(
|
||||||
|
communitiesService: DebouncedCommunitiesService(
|
||||||
|
RemoteCommunitiesService(HTTPService()),
|
||||||
|
),
|
||||||
|
)..add(const LoadCommunities(LoadCommunitiesParam()));
|
||||||
|
|
||||||
|
super.initState();
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return MultiBlocProvider(
|
return MultiBlocProvider(
|
||||||
providers: [
|
providers: [
|
||||||
|
BlocProvider.value(value: communitiesBloc),
|
||||||
BlocProvider(
|
BlocProvider(
|
||||||
create: (context) => CommunitiesBloc(
|
create: (context) => CommunitiesTreeSelectionBloc(
|
||||||
communitiesService: DebouncedCommunitiesService(
|
communitiesBloc: communitiesBloc,
|
||||||
RemoteCommunitiesService(HTTPService()),
|
),
|
||||||
|
),
|
||||||
|
BlocProvider(
|
||||||
|
create: (context) => SpaceDetailsBloc(
|
||||||
|
UniqueSubspacesDecorator(
|
||||||
|
RemoteSpaceDetailsService(httpService: HTTPService()),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
BlocProvider(
|
||||||
|
create: (context) => ProductsBloc(
|
||||||
|
RemoteProductsService(HTTPService()),
|
||||||
),
|
),
|
||||||
)..add(const LoadCommunities(LoadCommunitiesParam())),
|
|
||||||
),
|
),
|
||||||
BlocProvider(create: (context) => CommunitiesTreeSelectionBloc()),
|
|
||||||
],
|
],
|
||||||
child: WebScaffold(
|
child: WebScaffold(
|
||||||
appBarTitle: Text(
|
appBarTitle: Text(
|
||||||
|
@ -1,13 +1,17 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||||
import 'package:syncrow_web/pages/space_management_v2/main_module/models/space_connection_model.dart';
|
import 'package:syncrow_web/pages/space_management_v2/main_module/models/space_connection_model.dart';
|
||||||
|
import 'package:syncrow_web/pages/space_management_v2/main_module/models/space_reorder_data_model.dart';
|
||||||
import 'package:syncrow_web/pages/space_management_v2/main_module/painters/spaces_connections_arrow_painter.dart';
|
import 'package:syncrow_web/pages/space_management_v2/main_module/painters/spaces_connections_arrow_painter.dart';
|
||||||
|
import 'package:syncrow_web/pages/space_management_v2/main_module/widgets/create_space_button.dart';
|
||||||
import 'package:syncrow_web/pages/space_management_v2/main_module/widgets/space_card_widget.dart';
|
import 'package:syncrow_web/pages/space_management_v2/main_module/widgets/space_card_widget.dart';
|
||||||
import 'package:syncrow_web/pages/space_management_v2/main_module/widgets/space_cell.dart';
|
import 'package:syncrow_web/pages/space_management_v2/main_module/widgets/space_cell.dart';
|
||||||
import 'package:syncrow_web/pages/space_management_v2/modules/communities/domain/models/community_model.dart';
|
import 'package:syncrow_web/pages/space_management_v2/modules/communities/domain/models/community_model.dart';
|
||||||
import 'package:syncrow_web/pages/space_management_v2/modules/communities/domain/models/space_model.dart';
|
import 'package:syncrow_web/pages/space_management_v2/modules/communities/domain/models/space_model.dart';
|
||||||
|
import 'package:syncrow_web/pages/space_management_v2/modules/communities/presentation/bloc/communities_bloc.dart';
|
||||||
import 'package:syncrow_web/pages/space_management_v2/modules/communities/presentation/communities_tree_selection_bloc/communities_tree_selection_bloc.dart';
|
import 'package:syncrow_web/pages/space_management_v2/modules/communities/presentation/communities_tree_selection_bloc/communities_tree_selection_bloc.dart';
|
||||||
import 'package:syncrow_web/pages/space_management_v2/modules/space_details/presentation/helpers/space_details_dialog_helper.dart';
|
import 'package:syncrow_web/pages/space_management_v2/modules/space_details/presentation/helpers/space_details_dialog_helper.dart';
|
||||||
|
import 'package:syncrow_web/utils/extension/build_context_x.dart';
|
||||||
|
|
||||||
class CommunityStructureCanvas extends StatefulWidget {
|
class CommunityStructureCanvas extends StatefulWidget {
|
||||||
const CommunityStructureCanvas({
|
const CommunityStructureCanvas({
|
||||||
@ -31,8 +35,9 @@ class _CommunityStructureCanvasState extends State<CommunityStructureCanvas>
|
|||||||
final double _horizontalSpacing = 150.0;
|
final double _horizontalSpacing = 150.0;
|
||||||
final double _verticalSpacing = 120.0;
|
final double _verticalSpacing = 120.0;
|
||||||
|
|
||||||
late TransformationController _transformationController;
|
late final TransformationController _transformationController;
|
||||||
late AnimationController _animationController;
|
late final AnimationController _animationController;
|
||||||
|
SpaceReorderDataModel? _draggedData;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
@ -97,7 +102,7 @@ class _CommunityStructureCanvasState extends State<CommunityStructureCanvas>
|
|||||||
final position = _positions[space.uuid];
|
final position = _positions[space.uuid];
|
||||||
if (position == null) return;
|
if (position == null) return;
|
||||||
|
|
||||||
const scale = 1.5;
|
const scale = 1;
|
||||||
final viewSize = context.size;
|
final viewSize = context.size;
|
||||||
if (viewSize == null) return;
|
if (viewSize == null) return;
|
||||||
|
|
||||||
@ -112,16 +117,33 @@ class _CommunityStructureCanvasState extends State<CommunityStructureCanvas>
|
|||||||
_runAnimation(matrix);
|
_runAnimation(matrix);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void _onReorder(SpaceReorderDataModel data, int newIndex) {
|
||||||
|
final newCommunity = widget.community.copyWith();
|
||||||
|
final children = data.parent?.children ?? newCommunity.spaces;
|
||||||
|
final oldIndex = children.indexWhere((s) => s.uuid == data.space.uuid);
|
||||||
|
if (oldIndex != -1) {
|
||||||
|
final item = children.removeAt(oldIndex);
|
||||||
|
if (newIndex > oldIndex) {
|
||||||
|
children.insert(newIndex - 1, item);
|
||||||
|
} else {
|
||||||
|
children.insert(newIndex, item);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
context.read<CommunitiesBloc>().add(
|
||||||
|
CommunitiesUpdateCommunity(newCommunity),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
void _onSpaceTapped(SpaceModel? space) {
|
void _onSpaceTapped(SpaceModel? space) {
|
||||||
context.read<CommunitiesTreeSelectionBloc>().add(
|
context.read<CommunitiesTreeSelectionBloc>().add(
|
||||||
SelectSpaceEvent(community: widget.community, space: space),
|
SelectSpaceEvent(community: widget.community, space: space),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
void _resetSelectionAndZoom() {
|
void _resetSelectionAndZoom([CommunityModel? community]) {
|
||||||
context.read<CommunitiesTreeSelectionBloc>().add(
|
context.read<CommunitiesTreeSelectionBloc>().add(
|
||||||
SelectSpaceEvent(
|
SelectSpaceEvent(
|
||||||
community: widget.community,
|
community: community ?? widget.community,
|
||||||
space: null,
|
space: null,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
@ -182,7 +204,8 @@ class _CommunityStructureCanvasState extends State<CommunityStructureCanvas>
|
|||||||
_positions.clear();
|
_positions.clear();
|
||||||
final community = widget.community;
|
final community = widget.community;
|
||||||
|
|
||||||
_calculateLayout(community.spaces, 0, {});
|
final levelXOffset = <int, double>{};
|
||||||
|
_calculateLayout(community.spaces, 0, levelXOffset);
|
||||||
|
|
||||||
final selectedSpace = widget.selectedSpace;
|
final selectedSpace = widget.selectedSpace;
|
||||||
final highlightedUuids = <String>{};
|
final highlightedUuids = <String>{};
|
||||||
@ -193,7 +216,24 @@ class _CommunityStructureCanvasState extends State<CommunityStructureCanvas>
|
|||||||
|
|
||||||
final widgets = <Widget>[];
|
final widgets = <Widget>[];
|
||||||
final connections = <SpaceConnectionModel>[];
|
final connections = <SpaceConnectionModel>[];
|
||||||
_generateWidgets(community.spaces, widgets, connections, highlightedUuids);
|
_generateWidgets(
|
||||||
|
widget.community.spaces,
|
||||||
|
widgets,
|
||||||
|
connections,
|
||||||
|
highlightedUuids,
|
||||||
|
community: widget.community,
|
||||||
|
);
|
||||||
|
|
||||||
|
final createButtonX = levelXOffset[0] ?? 0.0;
|
||||||
|
const createButtonY = 0.0;
|
||||||
|
|
||||||
|
widgets.add(
|
||||||
|
Positioned(
|
||||||
|
left: createButtonX,
|
||||||
|
top: createButtonY,
|
||||||
|
child: CreateSpaceButton(communityUuid: widget.community.uuid),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
return [
|
return [
|
||||||
CustomPaint(
|
CustomPaint(
|
||||||
@ -211,22 +251,30 @@ class _CommunityStructureCanvasState extends State<CommunityStructureCanvas>
|
|||||||
List<SpaceModel> spaces,
|
List<SpaceModel> spaces,
|
||||||
List<Widget> widgets,
|
List<Widget> widgets,
|
||||||
List<SpaceConnectionModel> connections,
|
List<SpaceConnectionModel> connections,
|
||||||
Set<String> highlightedUuids,
|
Set<String> highlightedUuids, {
|
||||||
) {
|
CommunityModel? community,
|
||||||
for (final space in spaces) {
|
SpaceModel? parent,
|
||||||
|
}) {
|
||||||
|
if (spaces.isNotEmpty) {
|
||||||
|
final firstChildPos = _positions[spaces.first.uuid]!;
|
||||||
|
final targetPos = Offset(
|
||||||
|
firstChildPos.dx - (_horizontalSpacing / 4),
|
||||||
|
firstChildPos.dy,
|
||||||
|
);
|
||||||
|
widgets.add(_buildDropTarget(parent, community, 0, targetPos));
|
||||||
|
}
|
||||||
|
|
||||||
|
for (var i = 0; i < spaces.length; i++) {
|
||||||
|
final space = spaces[i];
|
||||||
final position = _positions[space.uuid];
|
final position = _positions[space.uuid];
|
||||||
if (position == null) continue;
|
if (position == null) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
final isHighlighted = highlightedUuids.contains(space.uuid);
|
final isHighlighted = highlightedUuids.contains(space.uuid);
|
||||||
final hasNoSelectedSpace = widget.selectedSpace == null;
|
final hasNoSelectedSpace = widget.selectedSpace == null;
|
||||||
|
|
||||||
widgets.add(
|
final spaceCard = SpaceCardWidget(
|
||||||
Positioned(
|
|
||||||
left: position.dx,
|
|
||||||
top: position.dy,
|
|
||||||
width: _cardWidth,
|
|
||||||
height: _cardHeight,
|
|
||||||
child: SpaceCardWidget(
|
|
||||||
buildSpaceContainer: () {
|
buildSpaceContainer: () {
|
||||||
return Opacity(
|
return Opacity(
|
||||||
opacity: hasNoSelectedSpace || isHighlighted ? 1.0 : 0.5,
|
opacity: hasNoSelectedSpace || isHighlighted ? 1.0 : 0.5,
|
||||||
@ -241,28 +289,140 @@ class _CommunityStructureCanvasState extends State<CommunityStructureCanvas>
|
|||||||
),
|
),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
onTap: () => SpaceDetailsDialogHelper.showCreate(context),
|
onTap: () => SpaceDetailsDialogHelper.showCreate(
|
||||||
|
context,
|
||||||
|
communityUuid: widget.community.uuid,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
final reorderData = SpaceReorderDataModel(
|
||||||
|
space: space,
|
||||||
|
parent: parent,
|
||||||
|
community: community,
|
||||||
|
);
|
||||||
|
|
||||||
|
widgets.add(
|
||||||
|
Positioned(
|
||||||
|
left: position.dx,
|
||||||
|
top: position.dy,
|
||||||
|
width: _cardWidth,
|
||||||
|
height: _cardHeight,
|
||||||
|
child: Draggable<SpaceReorderDataModel>(
|
||||||
|
data: reorderData,
|
||||||
|
feedback: Material(
|
||||||
|
color: Colors.transparent,
|
||||||
|
child: Opacity(
|
||||||
|
opacity: 0.2,
|
||||||
|
child: SizedBox(
|
||||||
|
width: _cardWidth,
|
||||||
|
height: _cardHeight,
|
||||||
|
child: spaceCard,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
onDragStarted: () => setState(() => _draggedData = reorderData),
|
||||||
|
onDragEnd: (_) => setState(() => _draggedData = null),
|
||||||
|
onDraggableCanceled: (_, __) => setState(() => _draggedData = null),
|
||||||
|
childWhenDragging: Opacity(opacity: 0.4, child: spaceCard),
|
||||||
|
child: spaceCard,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
final targetPos = Offset(
|
||||||
|
position.dx + _cardWidth + (_horizontalSpacing / 4) - 20,
|
||||||
|
position.dy,
|
||||||
|
);
|
||||||
|
widgets.add(_buildDropTarget(parent, community, i + 1, targetPos));
|
||||||
|
|
||||||
for (final child in space.children) {
|
for (final child in space.children) {
|
||||||
connections.add(
|
connections.add(SpaceConnectionModel(from: space.uuid, to: child.uuid));
|
||||||
SpaceConnectionModel(from: space.uuid, to: child.uuid),
|
}
|
||||||
|
|
||||||
|
if (space.children.isNotEmpty) {
|
||||||
|
_generateWidgets(
|
||||||
|
space.children,
|
||||||
|
widgets,
|
||||||
|
connections,
|
||||||
|
highlightedUuids,
|
||||||
|
parent: space,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
_generateWidgets(space.children, widgets, connections, highlightedUuids);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Widget _buildDropTarget(
|
||||||
|
SpaceModel? parent,
|
||||||
|
CommunityModel? community,
|
||||||
|
int index,
|
||||||
|
Offset position,
|
||||||
|
) {
|
||||||
|
return Positioned(
|
||||||
|
left: position.dx,
|
||||||
|
top: position.dy,
|
||||||
|
width: 40,
|
||||||
|
height: _cardHeight,
|
||||||
|
child: DragTarget<SpaceReorderDataModel>(
|
||||||
|
builder: (context, candidateData, rejectedData) {
|
||||||
|
if (_draggedData == null) {
|
||||||
|
return const SizedBox();
|
||||||
|
}
|
||||||
|
|
||||||
|
final isTargetForDragged = (_draggedData?.parent?.uuid == parent?.uuid &&
|
||||||
|
_draggedData?.community == null) ||
|
||||||
|
(_draggedData?.community?.uuid == community?.uuid &&
|
||||||
|
_draggedData?.parent == null);
|
||||||
|
|
||||||
|
if (!isTargetForDragged) {
|
||||||
|
return const SizedBox();
|
||||||
|
}
|
||||||
|
|
||||||
|
return Container(
|
||||||
|
width: 40,
|
||||||
|
height: _cardHeight,
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: context.theme.colorScheme.primary.withValues(
|
||||||
|
alpha: candidateData.isNotEmpty ? 0.7 : 0.3,
|
||||||
|
),
|
||||||
|
borderRadius: BorderRadius.circular(8),
|
||||||
|
),
|
||||||
|
child: Icon(
|
||||||
|
Icons.add,
|
||||||
|
color: context.theme.colorScheme.onPrimary,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
onWillAcceptWithDetails: (data) {
|
||||||
|
final children = parent?.children ?? community?.spaces ?? [];
|
||||||
|
final isSameParent = (data.data.parent?.uuid == parent?.uuid &&
|
||||||
|
data.data.community == null) ||
|
||||||
|
(data.data.community?.uuid == community?.uuid &&
|
||||||
|
data.data.parent == null);
|
||||||
|
|
||||||
|
if (!isSameParent) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
final oldIndex =
|
||||||
|
children.indexWhere((s) => s.uuid == data.data.space.uuid);
|
||||||
|
if (oldIndex == index || oldIndex == index - 1) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
},
|
||||||
|
onAcceptWithDetails: (data) => _onReorder(data.data, index),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final treeWidgets = _buildTreeWidgets();
|
final treeWidgets = _buildTreeWidgets();
|
||||||
return InteractiveViewer(
|
return InteractiveViewer(
|
||||||
transformationController: _transformationController,
|
transformationController: _transformationController,
|
||||||
boundaryMargin: EdgeInsets.symmetric(
|
boundaryMargin: EdgeInsets.symmetric(
|
||||||
horizontal: MediaQuery.sizeOf(context).width * 0.3,
|
horizontal: context.screenWidth * 0.3,
|
||||||
vertical: MediaQuery.sizeOf(context).height * 0.3,
|
vertical: context.screenHeight * 0.3,
|
||||||
),
|
),
|
||||||
minScale: 0.5,
|
minScale: 0.5,
|
||||||
maxScale: 3.0,
|
maxScale: 3.0,
|
||||||
@ -270,8 +430,8 @@ class _CommunityStructureCanvasState extends State<CommunityStructureCanvas>
|
|||||||
child: GestureDetector(
|
child: GestureDetector(
|
||||||
onTap: _resetSelectionAndZoom,
|
onTap: _resetSelectionAndZoom,
|
||||||
child: SizedBox(
|
child: SizedBox(
|
||||||
width: MediaQuery.sizeOf(context).width * 5,
|
width: context.screenWidth * 5,
|
||||||
height: MediaQuery.sizeOf(context).height * 5,
|
height: context.screenHeight * 5,
|
||||||
child: Stack(children: treeWidgets),
|
child: Stack(children: treeWidgets),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
@ -0,0 +1,146 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||||
|
import 'package:flutter_svg/svg.dart';
|
||||||
|
import 'package:syncrow_web/pages/space_management_v2/main_module/shared/helpers/space_management_community_dialog_helper.dart';
|
||||||
|
import 'package:syncrow_web/pages/space_management_v2/main_module/widgets/community_structure_header_action_buttons.dart';
|
||||||
|
import 'package:syncrow_web/pages/space_management_v2/modules/communities/domain/models/space_model.dart';
|
||||||
|
import 'package:syncrow_web/pages/space_management_v2/modules/communities/presentation/bloc/communities_bloc.dart';
|
||||||
|
import 'package:syncrow_web/pages/space_management_v2/modules/communities/presentation/communities_tree_selection_bloc/communities_tree_selection_bloc.dart';
|
||||||
|
import 'package:syncrow_web/pages/space_management_v2/modules/space_details/domain/models/space_details_model.dart';
|
||||||
|
import 'package:syncrow_web/pages/space_management_v2/modules/space_details/presentation/helpers/space_details_dialog_helper.dart';
|
||||||
|
import 'package:syncrow_web/utils/color_manager.dart';
|
||||||
|
import 'package:syncrow_web/utils/constants/assets.dart';
|
||||||
|
|
||||||
|
class CommunityStructureHeader extends StatelessWidget {
|
||||||
|
const CommunityStructureHeader({super.key});
|
||||||
|
|
||||||
|
List<SpaceModel> _updateRecursive(
|
||||||
|
List<SpaceModel> spaces,
|
||||||
|
SpaceDetailsModel updatedSpace,
|
||||||
|
) {
|
||||||
|
return spaces.map((space) {
|
||||||
|
if (space.uuid == updatedSpace.uuid) {
|
||||||
|
return space.copyWith(
|
||||||
|
spaceName: updatedSpace.spaceName,
|
||||||
|
icon: updatedSpace.icon,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (space.children.isNotEmpty) {
|
||||||
|
return space.copyWith(
|
||||||
|
children: _updateRecursive(space.children, updatedSpace),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return space;
|
||||||
|
}).toList();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final theme = Theme.of(context);
|
||||||
|
final screenWidth = MediaQuery.of(context).size.width;
|
||||||
|
return Container(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 16.0),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: ColorsManager.whiteColors,
|
||||||
|
boxShadow: [
|
||||||
|
BoxShadow(
|
||||||
|
color: ColorsManager.shadowBlackColor,
|
||||||
|
blurRadius: 8,
|
||||||
|
offset: const Offset(0, 4),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Row(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Expanded(
|
||||||
|
child: _buildCommunityInfo(context, theme, screenWidth),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 16),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildCommunityInfo(
|
||||||
|
BuildContext context, ThemeData theme, double screenWidth) {
|
||||||
|
final selectedCommunity =
|
||||||
|
context.watch<CommunitiesTreeSelectionBloc>().state.selectedCommunity;
|
||||||
|
final selectedSpace =
|
||||||
|
context.watch<CommunitiesTreeSelectionBloc>().state.selectedSpace;
|
||||||
|
return Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
'Community Structure',
|
||||||
|
style: theme.textTheme.headlineLarge?.copyWith(
|
||||||
|
color: ColorsManager.blackColor,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
if (selectedCommunity != null)
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
Expanded(
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
Flexible(
|
||||||
|
child: SelectableText(
|
||||||
|
selectedCommunity.name,
|
||||||
|
style: theme.textTheme.bodyLarge?.copyWith(
|
||||||
|
color: ColorsManager.blackColor,
|
||||||
|
),
|
||||||
|
maxLines: 1,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 2),
|
||||||
|
GestureDetector(
|
||||||
|
onTap: () {
|
||||||
|
SpaceManagementCommunityDialogHelper.showEditDialog(
|
||||||
|
context,
|
||||||
|
selectedCommunity,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
child: SvgPicture.asset(
|
||||||
|
Assets.iconEdit,
|
||||||
|
width: 16,
|
||||||
|
height: 16,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 8),
|
||||||
|
CommunityStructureHeaderActionButtons(
|
||||||
|
onDelete: (space) {},
|
||||||
|
onDuplicate: (space) {},
|
||||||
|
onEdit: (space) => SpaceDetailsDialogHelper.showEdit(
|
||||||
|
context,
|
||||||
|
spaceModel: selectedSpace!,
|
||||||
|
communityUuid: selectedCommunity.uuid,
|
||||||
|
onSuccess: (updatedSpaceDetails) {
|
||||||
|
final communitiesBloc = context.read<CommunitiesBloc>();
|
||||||
|
final updatedSpaces = _updateRecursive(
|
||||||
|
selectedCommunity.spaces,
|
||||||
|
updatedSpaceDetails,
|
||||||
|
);
|
||||||
|
|
||||||
|
final community = selectedCommunity.copyWith(
|
||||||
|
spaces: updatedSpaces,
|
||||||
|
);
|
||||||
|
|
||||||
|
communitiesBloc.add(CommunitiesUpdateCommunity(community));
|
||||||
|
},
|
||||||
|
),
|
||||||
|
selectedSpace: selectedSpace,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,46 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:syncrow_web/pages/space_management_v2/main_module/widgets/community_structure_header_button.dart';
|
||||||
|
import 'package:syncrow_web/pages/space_management_v2/modules/communities/domain/models/space_model.dart';
|
||||||
|
import 'package:syncrow_web/utils/constants/assets.dart';
|
||||||
|
|
||||||
|
class CommunityStructureHeaderActionButtons extends StatelessWidget {
|
||||||
|
const CommunityStructureHeaderActionButtons({
|
||||||
|
super.key,
|
||||||
|
required this.onDelete,
|
||||||
|
required this.selectedSpace,
|
||||||
|
required this.onDuplicate,
|
||||||
|
required this.onEdit,
|
||||||
|
});
|
||||||
|
|
||||||
|
final void Function(SpaceModel space) onDelete;
|
||||||
|
final void Function(SpaceModel space) onDuplicate;
|
||||||
|
final void Function(SpaceModel space) onEdit;
|
||||||
|
final SpaceModel? selectedSpace;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Wrap(
|
||||||
|
alignment: WrapAlignment.end,
|
||||||
|
spacing: 10,
|
||||||
|
children: [
|
||||||
|
if (selectedSpace != null) ...[
|
||||||
|
CommunityStructureHeaderButton(
|
||||||
|
label: 'Edit',
|
||||||
|
svgAsset: Assets.editSpace,
|
||||||
|
onPressed: () => onEdit(selectedSpace!),
|
||||||
|
),
|
||||||
|
CommunityStructureHeaderButton(
|
||||||
|
label: 'Duplicate',
|
||||||
|
svgAsset: Assets.duplicate,
|
||||||
|
onPressed: () => onDuplicate(selectedSpace!),
|
||||||
|
),
|
||||||
|
CommunityStructureHeaderButton(
|
||||||
|
label: 'Delete',
|
||||||
|
svgAsset: Assets.spaceDelete,
|
||||||
|
onPressed: () => onDelete(selectedSpace!),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,61 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_svg/svg.dart';
|
||||||
|
import 'package:syncrow_web/pages/common/buttons/default_button.dart';
|
||||||
|
import 'package:syncrow_web/utils/color_manager.dart';
|
||||||
|
import 'package:syncrow_web/utils/extension/build_context_x.dart';
|
||||||
|
|
||||||
|
class CommunityStructureHeaderButton extends StatelessWidget {
|
||||||
|
const CommunityStructureHeaderButton({
|
||||||
|
super.key,
|
||||||
|
required this.label,
|
||||||
|
required this.onPressed,
|
||||||
|
this.svgAsset,
|
||||||
|
});
|
||||||
|
|
||||||
|
final String label;
|
||||||
|
final VoidCallback onPressed;
|
||||||
|
final String? svgAsset;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
const double buttonHeight = 40;
|
||||||
|
return ConstrainedBox(
|
||||||
|
constraints: const BoxConstraints(
|
||||||
|
maxWidth: 130,
|
||||||
|
minHeight: buttonHeight,
|
||||||
|
),
|
||||||
|
child: DefaultButton(
|
||||||
|
onPressed: onPressed,
|
||||||
|
borderWidth: 2,
|
||||||
|
backgroundColor: ColorsManager.textFieldGreyColor,
|
||||||
|
foregroundColor: ColorsManager.blackColor,
|
||||||
|
borderRadius: 12.0,
|
||||||
|
padding: 2.0,
|
||||||
|
height: buttonHeight,
|
||||||
|
elevation: 0,
|
||||||
|
borderColor: ColorsManager.lightGrayColor,
|
||||||
|
child: Row(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
if (svgAsset != null)
|
||||||
|
SvgPicture.asset(
|
||||||
|
svgAsset!,
|
||||||
|
width: 20,
|
||||||
|
height: 20,
|
||||||
|
),
|
||||||
|
const SizedBox(width: 10),
|
||||||
|
Flexible(
|
||||||
|
child: Text(
|
||||||
|
label,
|
||||||
|
style: context.textTheme.bodySmall
|
||||||
|
?.copyWith(color: ColorsManager.blackColor, fontSize: 14),
|
||||||
|
overflow: TextOverflow.ellipsis,
|
||||||
|
maxLines: 1,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@ -2,42 +2,70 @@ import 'package:flutter/material.dart';
|
|||||||
import 'package:syncrow_web/pages/space_management_v2/modules/space_details/presentation/helpers/space_details_dialog_helper.dart';
|
import 'package:syncrow_web/pages/space_management_v2/modules/space_details/presentation/helpers/space_details_dialog_helper.dart';
|
||||||
import 'package:syncrow_web/utils/color_manager.dart';
|
import 'package:syncrow_web/utils/color_manager.dart';
|
||||||
|
|
||||||
class CreateSpaceButton extends StatelessWidget {
|
class CreateSpaceButton extends StatefulWidget {
|
||||||
const CreateSpaceButton({super.key});
|
const CreateSpaceButton({
|
||||||
|
required this.communityUuid,
|
||||||
|
super.key,
|
||||||
|
});
|
||||||
|
|
||||||
|
final String communityUuid;
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<CreateSpaceButton> createState() => _CreateSpaceButtonState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _CreateSpaceButtonState extends State<CreateSpaceButton> {
|
||||||
|
bool _isHovered = false;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return GestureDetector(
|
return Tooltip(
|
||||||
onTap: () => SpaceDetailsDialogHelper.showCreate(context),
|
margin: const EdgeInsets.symmetric(vertical: 24),
|
||||||
|
message: 'Create a new space',
|
||||||
|
child: InkWell(
|
||||||
|
onTap: () => SpaceDetailsDialogHelper.showCreate(
|
||||||
|
context,
|
||||||
|
communityUuid: widget.communityUuid,
|
||||||
|
),
|
||||||
|
child: MouseRegion(
|
||||||
|
onEnter: (_) => setState(() => _isHovered = true),
|
||||||
|
onExit: (_) => setState(() => _isHovered = false),
|
||||||
|
child: AnimatedOpacity(
|
||||||
|
duration: const Duration(milliseconds: 100),
|
||||||
|
opacity: _isHovered ? 1.0 : 0.45,
|
||||||
child: Container(
|
child: Container(
|
||||||
height: 60,
|
width: 150,
|
||||||
|
height: 90,
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: Colors.white,
|
color: Colors.white,
|
||||||
borderRadius: BorderRadius.circular(20),
|
borderRadius: BorderRadius.circular(20),
|
||||||
boxShadow: [
|
boxShadow: [
|
||||||
BoxShadow(
|
BoxShadow(
|
||||||
color: Colors.grey.withValues(alpha: 0.5),
|
color: Colors.grey.withValues(alpha: 0.2),
|
||||||
spreadRadius: 5,
|
spreadRadius: 3,
|
||||||
blurRadius: 7,
|
blurRadius: 8,
|
||||||
offset: const Offset(0, 3),
|
offset: const Offset(0, 4),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
child: Center(
|
|
||||||
child: Container(
|
child: Container(
|
||||||
width: 40,
|
margin: const EdgeInsets.symmetric(vertical: 20),
|
||||||
height: 40,
|
decoration: BoxDecoration(
|
||||||
decoration: const BoxDecoration(
|
border: Border.all(color: ColorsManager.borderColor, width: 2),
|
||||||
color: ColorsManager.boxColor,
|
color: ColorsManager.boxColor,
|
||||||
shape: BoxShape.circle,
|
shape: BoxShape.circle,
|
||||||
),
|
),
|
||||||
child: const Icon(
|
child: const Center(
|
||||||
|
child: Icon(
|
||||||
Icons.add,
|
Icons.add,
|
||||||
color: Colors.blue,
|
color: Colors.blue,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -22,7 +22,6 @@ class _SpaceCardWidgetState extends State<SpaceCardWidget> {
|
|||||||
return MouseRegion(
|
return MouseRegion(
|
||||||
onEnter: (_) => setState(() => isHovered = true),
|
onEnter: (_) => setState(() => isHovered = true),
|
||||||
onExit: (_) => setState(() => isHovered = false),
|
onExit: (_) => setState(() => isHovered = false),
|
||||||
child: SizedBox(
|
|
||||||
child: Stack(
|
child: Stack(
|
||||||
clipBehavior: Clip.none,
|
clipBehavior: Clip.none,
|
||||||
alignment: Alignment.center,
|
alignment: Alignment.center,
|
||||||
@ -38,7 +37,6 @@ class _SpaceCardWidgetState extends State<SpaceCardWidget> {
|
|||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user