Compare commits

..

1 Commits

Author SHA1 Message Date
df29aab111 Setup new firebase project in the web platform. 2025-07-10 12:18:45 +03:00
72 changed files with 268 additions and 3112 deletions

View File

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

View File

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

View File

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

112
.vscode/launch.json vendored
View File

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

View File

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

16
lib/firebase_options.dart Normal file
View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,143 +0,0 @@
import 'dart:async';
import 'package:bloc/bloc.dart';
import 'package:calendar_view/calendar_view.dart';
import 'package:flutter/material.dart';
part 'events_event.dart';
part 'events_state.dart';
class CalendarEventsBloc extends Bloc<CalendarEventsEvent, CalendarEventState> {
final EventController eventController = EventController();
CalendarEventsBloc() : 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 events = _generateDummyEventsForWeek(event.weekStart);
eventController.addAll(events);
emit(EventsLoaded(
events: events,
initialDate: event.weekStart,
weekDays: _getWeekDays(event.weekStart),
));
} catch (e) {
emit(EventsError('Failed to load events'));
}
}
List<CalendarEventData> _generateDummyEventsForWeek(DateTime weekStart) {
final events = <CalendarEventData>[];
for (int i = 0; i < 7; i++) {
final date = weekStart.add(Duration(days: i));
events.add(CalendarEventData(
date: date,
startTime: date.copyWith(hour: 9, minute: 0),
endTime: date.copyWith(hour: 10, minute: 30),
title: 'Team Meeting',
description: 'Daily standup',
color: Colors.blue,
));
events.add(CalendarEventData(
date: date,
startTime: date.copyWith(hour: 14, minute: 0),
endTime: date.copyWith(hour: 15, minute: 0),
title: 'Client Call',
description: 'Project discussion',
color: Colors.green,
));
}
return events;
}
void _onAddEvent(AddEvent event, Emitter<CalendarEventState> emit) {
eventController.add(event.event);
if (state is EventsLoaded) {
final loaded = state as EventsLoaded;
emit(EventsLoaded(
events: [...eventController.events],
initialDate: loaded.initialDate,
weekDays: loaded.weekDays,
));
}
}
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,
initialDate: event.weekDate,
weekDays: newWeekDays,
));
}
}
List<CalendarEventData> _generateDummyEvents() {
final now = DateTime.now();
return [
CalendarEventData(
date: now,
startTime: now.copyWith(hour: 8, minute: 00, second: 0),
endTime: now.copyWith(hour: 9, minute: 00, second: 0),
title: 'Team Meeting',
description: 'Weekly team sync',
color: Colors.blue,
),
CalendarEventData(
date: now,
startTime: now.copyWith(hour: 9, minute: 00, second: 0),
endTime: now.copyWith(hour: 10, minute: 30, second: 0),
title: 'Team Meeting',
description: 'Weekly team sync',
color: Colors.blue,
),
CalendarEventData(
date: now.add(const Duration(days: 1)),
startTime: now.copyWith(hour: 14, day: now.day + 1),
endTime: now.copyWith(hour: 15, day: now.day + 1),
title: 'Client Call',
description: 'Project discussion',
color: Colors.green,
),
CalendarEventData(
date: now.add(const Duration(days: 2)),
startTime: now.copyWith(hour: 11, day: now.day + 2),
endTime: now.copyWith(hour: 12, day: now.day + 2),
title: 'Lunch with Team',
color: Colors.orange,
),
];
}
List<DateTime> _getWeekDays(DateTime date) {
final int weekday = date.weekday;
final DateTime monday = date.subtract(Duration(days: weekday - 1));
return List.generate(7, (i) => monday.add(Duration(days: i)));
}
@override
Future<void> close() {
eventController.dispose();
return super.close();
}
}

View File

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

View File

@ -1,25 +0,0 @@
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;
final DateTime initialDate;
final List<DateTime> weekDays;
EventsLoaded({
required this.events,
required this.initialDate,
required this.weekDays,
});
}
class EventsError extends CalendarEventState {
final String message;
EventsError(this.message);
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,266 +0,0 @@
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:intl/intl.dart';
import 'package:calendar_view/calendar_view.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/weekly_calendar_page.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();
}
List<CalendarEventData> _generateDummyEventsForWeek(DateTime weekStart) {
final List<CalendarEventData> events = [];
for (int i = 0; i < 7; i++) {
final date = weekStart.add(Duration(days: i));
events.add(CalendarEventData(
date: date,
startTime: date.copyWith(hour: 9, minute: 0),
endTime: date.copyWith(hour: 10, minute: 30),
title: 'Team Meeting',
description: 'Daily standup',
color: Colors.blue,
));
events.add(CalendarEventData(
date: date,
startTime: date.copyWith(hour: 14, minute: 0),
endTime: date.copyWith(hour: 15, minute: 0),
title: 'Client Call',
description: 'Project discussion',
color: Colors.green,
));
}
return events;
}
void _loadEventsForWeek(DateTime weekStart) {
_eventController.removeWhere((_) => true);
_eventController.addAll(_generateDummyEventsForWeek(weekStart));
}
@override
Widget build(BuildContext context) {
return MultiBlocProvider(
providers: [
BlocProvider(create: (_) => SelectedBookableSpaceBloc()),
BlocProvider(create: (_) => DateSelectionBloc()),
],
child: BlocListener<DateSelectionBloc, DateSelectionState>(
listenWhen: (previous, current) =>
previous.weekStart != current.weekStart,
listener: (context, state) {
_loadEventsForWeek(state.weekStart);
},
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 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: () {
context
.read<DateSelectionBloc>()
.add(PreviousWeek());
},
),
const SizedBox(width: 10),
Text(
_getMonthYearText(weekStart, weekEnd),
style: const TextStyle(
color: ColorsManager.lightGrayColor,
fontSize: 14,
fontWeight: FontWeight.w400,
),
),
const SizedBox(width: 10),
IconButton(
iconSize: 15,
icon: const Icon(Icons.arrow_forward_ios,
color: ColorsManager.lightGrayColor),
onPressed: () {
context
.read<DateSelectionBloc>()
.add(NextWeek());
},
),
],
),
);
},
),
],
),
Expanded(
child: BlocBuilder<SelectedBookableSpaceBloc,
SelectedBookableSpaceState>(
builder: (context, roomState) {
final selectedRoom = roomState.selectedBookableSpace;
return BlocBuilder<DateSelectionBloc,
DateSelectionState>(
builder: (context, dateState) {
return WeeklyCalendarPage(
startTime:
selectedRoom?.bookableConfig.startTime,
endTime: selectedRoom?.bookableConfig.endTime,
weekStart: dateState.weekStart,
selectedDate: dateState.selectedDate,
eventController: _eventController,
selectedDateFromSideBarCalender: context
.watch<DateSelectionBloc>()
.state
.selectedDateFromSideBarCalender,
);
},
);
},
),
),
],
),
),
),
],
),
),
);
}
String _getMonthYearText(DateTime start, DateTime end) {
final startMonth = DateFormat('MMM').format(start);
final endMonth = DateFormat('MMM').format(end);
final year = start.year == end.year
? start.year.toString()
: '${start.year}-${end.year}';
if (start.month == end.month) {
return '$startMonth $year';
} else {
return '$startMonth - $endMonth $year';
}
}
}

View File

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

View File

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

View File

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

View File

@ -1,292 +0,0 @@
import 'package:flutter/material.dart';
import 'package:calendar_view/calendar_view.dart';
import 'package:intl/intl.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;
return LayoutBuilder(
builder: (context, constraints) {
final double calendarWidth = constraints.maxWidth;
final double dayColumnWidth =
(calendarWidth - timeLineWidth) / totalDays - 0.1;
return Padding(
padding: const EdgeInsets.only(left: 25.0, right: 25.0, top: 25),
child: Stack(
children: [
WeekView(
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) {
final index = weekDays.indexWhere((d) => isSameDay(d, date));
final isSelectedDay = index == selectedDayIndex;
return Column(
children: [
Text(
DateFormat('EEE').format(date).toUpperCase(),
style: TextStyle(
fontWeight: FontWeight.w400,
fontSize: 14,
color: isSelectedDay ? Colors.blue : Colors.black,
),
),
Text(
DateFormat('d').format(date),
style: TextStyle(
fontWeight: FontWeight.w700,
fontSize: 20,
color: isSelectedDay
? ColorsManager.blue1
: ColorsManager.blackColor,
),
),
],
);
},
timeLineBuilder: (date) {
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,
),
],
),
),
);
},
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 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(),
),
);
},
),
if (selectedDayIndex >= 0)
Positioned(
left: (timeLineWidth + 3) +
(dayColumnWidth - 8) * (selectedDayIndex - 0.01),
top: 0,
bottom: 0,
width: dayColumnWidth,
child: IgnorePointer(
child: Container(
margin: const EdgeInsets.symmetric(
vertical: 0, horizontal: 4),
color: ColorsManager.spaceColor.withOpacity(0.07),
),
),
),
if (selectedSidebarIndex >= 0 &&
selectedSidebarIndex != selectedDayIndex)
Positioned(
left: (timeLineWidth + 3) +
(dayColumnWidth - 8) * (selectedSidebarIndex - 0.01),
top: 0,
bottom: 0,
width: dayColumnWidth,
child: IgnorePointer(
child: Container(
margin: const EdgeInsets.symmetric(
vertical: 0, horizontal: 4),
color: Colors.orange.withOpacity(0.14),
),
),
),
Positioned(
right: 0,
top: 50,
bottom: 0,
child: IgnorePointer(
child: Container(
width: 1,
color: Theme.of(context).scaffoldBackgroundColor,
),
),
),
],
),
);
},
);
}
List<DateTime> _getWeekDays(DateTime date) {
final int weekday = date.weekday;
final DateTime monday = date.subtract(Duration(days: weekday - 1));
return List.generate(7, (i) => monday.add(Duration(days: i)));
}
}
bool isSameDay(DateTime d1, DateTime d2) {
return d1.year == d2.year && d1.month == d2.month && d1.day == d2.day;
}
int _parseHour(String? time, {required int defaultValue}) {
if (time == null || time.isEmpty || !time.contains(':')) {
return defaultValue;
}
try {
return int.parse(time.split(':')[0]);
} catch (e) {
return defaultValue;
}
}

View File

@ -0,0 +1,52 @@
import 'package:flutter/material.dart';
import 'package:syncrow_web/pages/access_management/booking_system/view/widgets/icon_text_button.dart';
import 'package:syncrow_web/utils/constants/assets.dart';
class BookingPage extends StatelessWidget {
const BookingPage({super.key});
@override
Widget build(BuildContext context) {
return Container(
child: Row(
children: [
Expanded(
child: Container(
color: Colors.blueGrey[100],
child: const Center(
child: Text(
'Side bar',
style: TextStyle(fontSize: 24, fontWeight: FontWeight.bold),
),
),
)),
Expanded(
flex: 4,
child: Padding(
padding: const EdgeInsets.all(20.0),
child: SizedBox(
child: Column(
children: [
Row(
mainAxisAlignment: MainAxisAlignment.start,
children: [
SvgTextButton(
svgAsset: Assets.homeIcon,
label: 'Manage Bookable Spaces',
onPressed: () {}),
SizedBox(width: 20),
SvgTextButton(
svgAsset: Assets.groupIcon,
label: 'Manage Users',
onPressed: () {})
],
)
],
),
),
))
],
),
);
}
}

View File

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

View File

@ -2,7 +2,7 @@ import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:syncrow_web/pages/access_management/bloc/access_bloc.dart';
import 'package:syncrow_web/pages/access_management/bloc/access_event.dart';
import 'package:syncrow_web/pages/access_management/booking_system/presentation/view/booking_page.dart';
import 'package:syncrow_web/pages/access_management/booking_system/view/booking_page.dart';
import 'package:syncrow_web/pages/access_management/view/access_overview_content.dart';
import 'package:syncrow_web/pages/device_managment/shared/navigate_home_grid_view.dart';
import 'package:syncrow_web/utils/extension/build_context_x.dart';

View File

@ -7,8 +7,6 @@ import 'package:syncrow_web/pages/space_management_v2/modules/communities/data/s
import 'package:syncrow_web/pages/space_management_v2/modules/communities/domain/params/load_communities_param.dart';
import 'package:syncrow_web/pages/space_management_v2/modules/communities/presentation/bloc/communities_bloc.dart';
import 'package:syncrow_web/pages/space_management_v2/modules/communities/presentation/communities_tree_selection_bloc/communities_tree_selection_bloc.dart';
import 'package:syncrow_web/pages/space_management_v2/modules/products/data/services/remote_products_service.dart';
import 'package:syncrow_web/pages/space_management_v2/modules/products/presentation/bloc/products_bloc.dart';
import 'package:syncrow_web/pages/space_management_v2/modules/space_details/data/services/remote_space_details_service.dart';
import 'package:syncrow_web/pages/space_management_v2/modules/space_details/data/services/unique_subspaces_decorator.dart';
import 'package:syncrow_web/pages/space_management_v2/modules/space_details/presentation/bloc/space_details_bloc.dart';
@ -38,11 +36,6 @@ class SpaceManagementPage extends StatelessWidget {
),
),
),
BlocProvider(
create: (context) => ProductsBloc(
RemoteProductsService(HTTPService()),
),
),
],
child: WebScaffold(
appBarTitle: Text(

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -2,7 +2,6 @@ import 'package:equatable/equatable.dart';
import 'package:syncrow_web/pages/space_management_v2/modules/products/domain/models/product.dart';
import 'package:syncrow_web/pages/space_management_v2/modules/tags/domain/models/tag.dart';
import 'package:syncrow_web/utils/constants/assets.dart';
import 'package:uuid/uuid.dart';
class SpaceDetailsModel extends Equatable {
final String uuid;
@ -22,7 +21,7 @@ class SpaceDetailsModel extends Equatable {
factory SpaceDetailsModel.empty() => const SpaceDetailsModel(
uuid: '',
spaceName: '',
icon: Assets.location,
icon: Assets.villa,
productAllocations: [],
subspaces: [],
);
@ -32,8 +31,8 @@ class SpaceDetailsModel extends Equatable {
spaceName: json['spaceName'] as String,
icon: json['icon'] as String,
productAllocations: (json['productAllocations'] as List)
.map((e) => ProductAllocation.fromJson(e as Map<String, dynamic>))
.toList(),
.map((e) => ProductAllocation.fromJson(e as Map<String, dynamic>))
.toList(),
subspaces: (json['subspaces'] as List)
.map((e) => Subspace.fromJson(e as Map<String, dynamic>))
.toList(),
@ -71,19 +70,16 @@ class SpaceDetailsModel extends Equatable {
}
class ProductAllocation extends Equatable {
final String uuid;
final Product product;
final Tag tag;
const ProductAllocation({
required this.uuid,
required this.product,
required this.tag,
});
factory ProductAllocation.fromJson(Map<String, dynamic> json) {
return ProductAllocation(
uuid: json['uuid'] as String? ?? const Uuid().v4(),
product: Product.fromJson(json['product'] as Map<String, dynamic>),
tag: Tag.fromJson(json['tag'] as Map<String, dynamic>),
);
@ -91,26 +87,23 @@ class ProductAllocation extends Equatable {
Map<String, dynamic> toJson() {
return {
'uuid': uuid,
'product': product.toJson(),
'tag': tag.toJson(),
};
}
ProductAllocation copyWith({
String? uuid,
Product? product,
Tag? tag,
}) {
return ProductAllocation(
uuid: uuid ?? this.uuid,
product: product ?? this.product,
tag: tag ?? this.tag,
);
}
@override
List<Object?> get props => [uuid, product, tag];
List<Object?> get props => [product, tag];
}
class Subspace extends Equatable {

View File

@ -18,7 +18,7 @@ abstract final class SpaceDetailsDialogHelper {
context: context,
title: const SelectableText('Create Space'),
spaceModel: SpaceModel.empty(),
onSave: (space) {},
onSave: print,
),
),
);

View File

@ -8,14 +8,10 @@ class SpaceDetailsActionButtons extends StatelessWidget {
super.key,
required this.onSave,
required this.onCancel,
this.saveButtonLabel = 'OK',
this.cancelButtonLabel = 'Cancel',
});
final VoidCallback onCancel;
final VoidCallback? onSave;
final String saveButtonLabel;
final String cancelButtonLabel;
@override
Widget build(BuildContext context) {
@ -31,7 +27,10 @@ class SpaceDetailsActionButtons extends StatelessWidget {
}
Widget _buildCancelButton(BuildContext context) {
return CancelButton(onPressed: onCancel, label: cancelButtonLabel);
return CancelButton(
onPressed: onCancel,
label: 'Cancel',
);
}
Widget _buildSaveButton() {
@ -40,7 +39,7 @@ class SpaceDetailsActionButtons extends StatelessWidget {
borderRadius: 10,
backgroundColor: ColorsManager.secondaryColor,
foregroundColor: ColorsManager.whiteColors,
child: Text(saveButtonLabel),
child: const Text('OK'),
);
}
}

View File

@ -1,15 +1,9 @@
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_svg/svg.dart';
import 'package:syncrow_web/common/edit_chip.dart';
import 'package:syncrow_web/pages/space_management_v2/modules/space_details/domain/models/space_details_model.dart';
import 'package:syncrow_web/pages/space_management_v2/modules/space_details/presentation/widgets/button_content_widget.dart';
import 'package:syncrow_web/pages/space_management_v2/modules/tags/presentation/widgets/assign_tags_dialog.dart';
import 'package:syncrow_web/pages/space_management_v2/modules/update_space/presentation/bloc/space_details_model_bloc/space_details_model_bloc.dart';
import 'package:syncrow_web/utils/color_manager.dart';
import 'package:syncrow_web/utils/constants/assets.dart';
import 'package:syncrow_web/utils/enum/device_types.dart';
import 'package:syncrow_web/utils/extension/build_context_x.dart';
class SpaceDetailsDevicesBox extends StatelessWidget {
const SpaceDetailsDevicesBox({
@ -21,18 +15,11 @@ class SpaceDetailsDevicesBox extends StatelessWidget {
@override
Widget build(BuildContext context) {
final allAllocations = [
...space.productAllocations,
...space.subspaces.expand((s) => s.productAllocations),
];
if (allAllocations.isNotEmpty) {
final productCounts = <String, int>{};
for (final allocation in allAllocations) {
final productType = allocation.product.productType;
productCounts[productType] = (productCounts[productType] ?? 0) + 1;
}
final productAllocations = space.productAllocations;
final subspaces = space.subspaces;
final isAnySubspaceHasProductAllocations =
subspaces.any((subspace) => subspace.productAllocations.isNotEmpty);
if (productAllocations.isNotEmpty || isAnySubspaceHasProductAllocations) {
return Container(
width: double.infinity,
padding: const EdgeInsets.all(8),
@ -48,40 +35,46 @@ class SpaceDetailsDevicesBox extends StatelessWidget {
spacing: 8.0,
runSpacing: 8.0,
children: [
...productCounts.entries.map((entry) {
final productType = entry.key;
final count = entry.value;
return Chip(
avatar: SizedBox(
width: 24,
height: 24,
child: SvgPicture.asset(
_getDeviceIcon(productType),
fit: BoxFit.contain,
),
),
label: Text(
'x$count',
style: context.textTheme.bodySmall?.copyWith(
color: ColorsManager.spaceColor,
),
),
backgroundColor: ColorsManager.whiteColors,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(16),
side: const BorderSide(
color: ColorsManager.spaceColor,
),
),
);
}),
EditChip(onTap: () => _showAssignTagsDialog(context)),
// Combine tags from spaceModel and subspaces
// ...TagHelper.groupTags([
// ...?tags,
// ...?subspaces?.expand((subspace) => subspace.tags ?? [])
// ]).entries.map(
// (entry) => Chip(
// avatar: SizedBox(
// width: 24,
// height: 24,
// child: SvgPicture.asset(
// entry.key.icon ?? 'assets/icons/gateway.svg',
// fit: BoxFit.contain,
// ),
// ),
// label: Text(
// 'x${entry.value}',
// style: Theme.of(context)
// .textTheme
// .bodySmall
// ?.copyWith(color: ColorsManager.spaceColor),
// ),
// backgroundColor: ColorsManager.whiteColors,
// shape: RoundedRectangleBorder(
// borderRadius: BorderRadius.circular(16),
// side: const BorderSide(
// color: ColorsManager.spaceColor,
// ),
// ),
// ),
// ),
EditChip(
onTap: () {},
),
],
),
);
} else {
return TextButton(
onPressed: () => _showAssignTagsDialog(context),
onPressed: () {},
style: TextButton.styleFrom(
padding: EdgeInsets.zero,
),
@ -90,50 +83,10 @@ class SpaceDetailsDevicesBox extends StatelessWidget {
child: ButtonContentWidget(
svgAssets: Assets.addIcon,
label: 'Add Devices',
// disabled: isTagsAndSubspaceModelDisabled,
),
),
);
}
}
void _showAssignTagsDialog(BuildContext context) {
showDialog<SpaceDetailsModel>(
context: context,
builder: (context) => AssignTagsDialog(space: space),
).then((resultSpace) {
if (resultSpace != null) {
if (context.mounted) {
context.read<SpaceDetailsModelBloc>().add(UpdateSpaceDetails(resultSpace));
}
}
});
}
String _getDeviceIcon(String productType) =>
switch (devicesTypesMap[productType]) {
DeviceType.LightBulb => Assets.lightBulb,
DeviceType.CeilingSensor => Assets.sensors,
DeviceType.AC => Assets.ac,
DeviceType.DoorLock => Assets.doorLock,
DeviceType.Curtain => Assets.curtain,
DeviceType.ThreeGang => Assets.gangSwitch,
DeviceType.Gateway => Assets.gateway,
DeviceType.OneGang => Assets.oneGang,
DeviceType.TwoGang => Assets.twoGang,
DeviceType.WH => Assets.waterHeater,
DeviceType.DoorSensor => Assets.openCloseDoor,
DeviceType.GarageDoor => Assets.openedDoor,
DeviceType.WaterLeak => Assets.waterLeakNormal,
DeviceType.Curtain2 => Assets.curtainIcon,
DeviceType.Blind => Assets.curtainIcon,
DeviceType.WallSensor => Assets.sensors,
DeviceType.DS => Assets.openCloseDoor,
DeviceType.OneTouch => Assets.gangSwitch,
DeviceType.TowTouch => Assets.gangSwitch,
DeviceType.ThreeTouch => Assets.gangSwitch,
DeviceType.NCPS => Assets.sensors,
DeviceType.PC => Assets.powerClamp,
DeviceType.Other => Assets.blackLogo,
null => Assets.blackLogo,
};
}

View File

@ -42,8 +42,9 @@ class SpaceDetailsForm extends StatelessWidget {
Expanded(child: SpaceIconPicker(iconPath: space.icon)),
Expanded(
flex: 2,
child: ListView(
shrinkWrap: true,
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
SpaceNameTextField(
initialValue: space.spaceName,
@ -51,7 +52,7 @@ class SpaceDetailsForm extends StatelessWidget {
(subspace) => subspace.name == value,
),
),
const SizedBox(height: 32),
const Spacer(),
SpaceSubSpacesBox(
subspaces: space.subspaces,
),

View File

@ -1,9 +1,10 @@
import 'package:dio/dio.dart';
import 'package:syncrow_web/pages/common/bloc/project_manager.dart';
import 'package:syncrow_web/pages/space_management_v2/modules/tags/domain/models/tag.dart';
import 'package:syncrow_web/pages/space_management_v2/modules/tags/domain/params/load_tags_param.dart';
import 'package:syncrow_web/pages/space_management_v2/modules/tags/domain/services/tags_service.dart';
import 'package:syncrow_web/services/api/api_exception.dart';
import 'package:syncrow_web/services/api/http_service.dart';
import 'package:syncrow_web/utils/constants/api_const.dart';
final class RemoteTagsService implements TagsService {
const RemoteTagsService(this._httpService);
@ -13,10 +14,17 @@ final class RemoteTagsService implements TagsService {
static const _defaultErrorMessage = 'Failed to load tags';
@override
Future<List<Tag>> loadTags() async {
Future<List<Tag>> loadTags(LoadTagsParam param) async {
if (param.projectUuid == null) {
throw Exception('Project UUID is required');
}
try {
final response = await _httpService.get(
path: await _makeUrl(),
path: ApiEndpoints.listTags.replaceAll(
'{projectUuid}',
param.projectUuid!,
),
expectedResponseModel: (json) {
final result = json as Map<String, dynamic>;
final data = result['data'] as List<dynamic>;
@ -38,12 +46,4 @@ final class RemoteTagsService implements TagsService {
throw APIException(formattedErrorMessage);
}
}
Future<String> _makeUrl() async {
final projectUuid = await ProjectManager.getProjectUUID();
if (projectUuid == null || projectUuid.isEmpty) {
throw APIException('Project UUID is required');
}
return '/projects/$projectUuid/tags';
}
}

View File

@ -13,13 +13,6 @@ class Tag extends Equatable {
required this.updatedAt,
});
factory Tag.empty() => const Tag(
uuid: '',
name: '',
createdAt: '',
updatedAt: '',
);
factory Tag.fromJson(Map<String, dynamic> json) {
return Tag(
uuid: json['uuid'] as String,

View File

@ -1,5 +1,6 @@
import 'package:syncrow_web/pages/space_management_v2/modules/tags/domain/models/tag.dart';
import 'package:syncrow_web/pages/space_management_v2/modules/tags/domain/params/load_tags_param.dart';
abstract interface class TagsService {
Future<List<Tag>> loadTags();
Future<List<Tag>> loadTags(LoadTagsParam param);
}

View File

@ -1,6 +1,7 @@
import 'package:bloc/bloc.dart';
import 'package:equatable/equatable.dart';
import 'package:syncrow_web/pages/space_management_v2/modules/tags/domain/models/tag.dart';
import 'package:syncrow_web/pages/space_management_v2/modules/tags/domain/params/load_tags_param.dart';
import 'package:syncrow_web/pages/space_management_v2/modules/tags/domain/services/tags_service.dart';
import 'package:syncrow_web/services/api/api_exception.dart';
@ -20,7 +21,7 @@ class TagsBloc extends Bloc<TagsEvent, TagsState> {
) async {
emit(TagsLoading());
try {
final tags = await _tagsService.loadTags();
final tags = await _tagsService.loadTags(event.param);
emit(TagsLoaded(tags));
} on APIException catch (e) {
emit(TagsFailure(e.message));

View File

@ -8,5 +8,10 @@ abstract class TagsEvent extends Equatable {
}
class LoadTags extends TagsEvent {
const LoadTags();
final LoadTagsParam param;
const LoadTags(this.param);
@override
List<Object?> get props => [param];
}

View File

@ -1,100 +0,0 @@
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:syncrow_web/pages/space_management_v2/modules/products/data/services/remote_products_service.dart';
import 'package:syncrow_web/pages/space_management_v2/modules/products/domain/models/product.dart';
import 'package:syncrow_web/pages/space_management_v2/modules/products/presentation/bloc/products_bloc.dart';
import 'package:syncrow_web/pages/space_management_v2/modules/space_details/presentation/widgets/space_details_action_buttons.dart';
import 'package:syncrow_web/pages/space_management_v2/modules/tags/presentation/widgets/products_grid.dart';
import 'package:syncrow_web/services/api/http_service.dart';
import 'package:syncrow_web/utils/color_manager.dart';
import 'package:syncrow_web/utils/extension/build_context_x.dart';
class AddDeviceTypeWidget extends StatefulWidget {
const AddDeviceTypeWidget({super.key});
@override
State<AddDeviceTypeWidget> createState() => _AddDeviceTypeWidgetState();
}
class _AddDeviceTypeWidgetState extends State<AddDeviceTypeWidget> {
final Map<Product, int> _selectedProducts = {};
void _onIncrement(Product product) {
setState(() {
_selectedProducts[product] = (_selectedProducts[product] ?? 0) + 1;
});
}
void _onDecrement(Product product) {
setState(() {
if ((_selectedProducts[product] ?? 0) > 0) {
_selectedProducts[product] = _selectedProducts[product]! - 1;
if (_selectedProducts[product] == 0) {
_selectedProducts.remove(product);
}
}
});
}
@override
Widget build(BuildContext context) {
return BlocProvider(
create: (_) => ProductsBloc(RemoteProductsService(HTTPService()))
..add(const LoadProducts()),
child: Builder(
builder: (context) => AlertDialog(
title: const SelectableText('Add Devices'),
backgroundColor: ColorsManager.whiteColors,
content: BlocBuilder<ProductsBloc, ProductsState>(
builder: (context, state) => switch (state) {
ProductsInitial() || ProductsLoading() => _buildLoading(context),
ProductsLoaded(:final products) => ProductsGrid(
products: products,
selectedProducts: _selectedProducts,
onIncrement: _onIncrement,
onDecrement: _onDecrement,
),
ProductsFailure(:final errorMessage) => _buildFailure(
context,
errorMessage,
),
},
),
actions: [
SpaceDetailsActionButtons(
onSave: () {
final result = _selectedProducts.entries
.expand((entry) => List.generate(entry.value, (_) => entry.key))
.toList();
Navigator.of(context).pop(result);
},
onCancel: Navigator.of(context).pop,
saveButtonLabel: 'Next',
),
],
),
),
);
}
Widget _buildLoading(BuildContext context) => SizedBox(
width: context.screenWidth * 0.9,
height: context.screenHeight * 0.65,
child: const Center(child: CircularProgressIndicator()),
);
Widget _buildFailure(BuildContext context, String errorMessage) {
return SizedBox(
width: context.screenWidth * 0.9,
height: context.screenHeight * 0.65,
child: Center(
child: SelectableText(
errorMessage,
style: context.textTheme.bodyMedium?.copyWith(
color: context.theme.colorScheme.error,
),
),
),
);
}
}

View File

@ -1,231 +0,0 @@
import 'package:flutter/material.dart';
import 'package:syncrow_web/pages/space_management_v2/modules/products/domain/models/product.dart';
import 'package:syncrow_web/pages/space_management_v2/modules/space_details/domain/models/space_details_model.dart';
import 'package:syncrow_web/pages/space_management_v2/modules/space_details/presentation/widgets/space_details_action_buttons.dart';
import 'package:syncrow_web/pages/space_management_v2/modules/tags/domain/models/tag.dart';
import 'package:syncrow_web/pages/space_management_v2/modules/tags/presentation/widgets/add_device_type_widget.dart';
import 'package:syncrow_web/pages/space_management_v2/modules/tags/presentation/widgets/assign_tags_error_messages.dart';
import 'package:syncrow_web/pages/space_management_v2/modules/tags/presentation/widgets/assign_tags_table.dart';
import 'package:syncrow_web/utils/extension/build_context_x.dart';
import 'package:uuid/uuid.dart';
class AssignTagsDialog extends StatefulWidget {
const AssignTagsDialog({required this.space, super.key});
final SpaceDetailsModel space;
@override
State<AssignTagsDialog> createState() => _AssignTagsDialogState();
}
class _AssignTagsDialogState extends State<AssignTagsDialog> {
late SpaceDetailsModel _space;
final Map<String, String> _validationErrors = {};
@override
void initState() {
super.initState();
_space = widget.space.copyWith(
productAllocations:
widget.space.productAllocations.map((e) => e.copyWith()).toList(),
subspaces: widget.space.subspaces
.map(
(s) => s.copyWith(
productAllocations:
s.productAllocations.map((e) => e.copyWith()).toList(),
),
)
.toList(),
);
_validateAllTags();
}
void _validateAllTags() {
final newErrors = <String, String>{};
final allAllocations = [
..._space.productAllocations,
..._space.subspaces.expand((s) => s.productAllocations),
];
final allocationsByProductType = <String, List<ProductAllocation>>{};
for (final allocation in allAllocations) {
(allocationsByProductType[allocation.product.productType] ??= [])
.add(allocation);
}
for (final productType in allocationsByProductType.keys) {
final allocations = allocationsByProductType[productType]!;
final tagCounts = <String, int>{};
for (final allocation in allocations) {
final tagName = allocation.tag.name.trim().toLowerCase();
if (tagName.isEmpty) {
newErrors[allocation.uuid] =
'Tag for ${allocation.product.name} cannot be empty.';
} else {
tagCounts[tagName] = (tagCounts[tagName] ?? 0) + 1;
}
}
for (final allocation in allocations) {
final tagName = allocation.tag.name.trim().toLowerCase();
if (tagName.isNotEmpty && (tagCounts[tagName] ?? 0) > 1) {
newErrors[allocation.uuid] =
'Tag "${allocation.tag.name}" is used by multiple $productType devices.';
}
}
}
setState(() {
_validationErrors
..clear()
..addAll(newErrors);
});
}
void _handleTagChange(String allocationUuid, Tag newTag) {
setState(() {
var index =
_space.productAllocations.indexWhere((pa) => pa.uuid == allocationUuid);
if (index != -1) {
final allocation = _space.productAllocations[index];
_space.productAllocations[index] = allocation.copyWith(tag: newTag);
} else {
for (final subspace in _space.subspaces) {
index = subspace.productAllocations
.indexWhere((pa) => pa.uuid == allocationUuid);
if (index != -1) {
final allocation = subspace.productAllocations[index];
subspace.productAllocations[index] = allocation.copyWith(tag: newTag);
break;
}
}
}
});
_validateAllTags();
}
void _handleLocationChange(String allocationUuid, String? newSubspaceUuid) {
setState(() {
ProductAllocation? allocationToMove;
var index =
_space.productAllocations.indexWhere((pa) => pa.uuid == allocationUuid);
if (index != -1) {
allocationToMove = _space.productAllocations.removeAt(index);
} else {
for (final subspace in _space.subspaces) {
index = subspace.productAllocations
.indexWhere((pa) => pa.uuid == allocationUuid);
if (index != -1) {
allocationToMove = subspace.productAllocations.removeAt(index);
break;
}
}
}
if (allocationToMove == null) return;
if (newSubspaceUuid == null) {
_space.productAllocations.add(allocationToMove);
} else {
_space.subspaces
.firstWhere((s) => s.uuid == newSubspaceUuid)
.productAllocations
.add(allocationToMove);
}
});
}
void _handleProductDelete(String allocationUuid) {
setState(() {
_space.productAllocations.removeWhere((pa) => pa.uuid == allocationUuid);
for (final subspace in _space.subspaces) {
subspace.productAllocations.removeWhere(
(pa) => pa.uuid == allocationUuid,
);
}
});
_validateAllTags();
}
@override
Widget build(BuildContext context) {
final allProductAllocations = [
..._space.productAllocations,
..._space.subspaces.expand((s) => s.productAllocations),
];
final productLocations = <String, String?>{};
for (final pa in _space.productAllocations) {
productLocations[pa.uuid] = null;
}
for (final subspace in _space.subspaces) {
for (final pa in subspace.productAllocations) {
productLocations[pa.uuid] = subspace.uuid;
}
}
final hasErrors = _validationErrors.isNotEmpty;
return AlertDialog(
title: const SelectableText('Assign Tags'),
content: ConstrainedBox(
constraints: BoxConstraints(
maxWidth: context.screenWidth * 0.6,
minWidth: context.screenWidth * 0.6,
maxHeight: context.screenHeight * 0.8,
),
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
SizedBox(
width: double.infinity,
child: AssignTagsTable(
productAllocations: allProductAllocations,
subspaces: _space.subspaces,
productLocations: productLocations,
onTagSelected: _handleTagChange,
onLocationSelected: _handleLocationChange,
onProductDeleted: _handleProductDelete,
),
),
if (hasErrors)
AssignTagsErrorMessages(
errorMessages: _validationErrors.values.toSet().toList(),
),
],
),
),
actions: [
SpaceDetailsActionButtons(
onSave: hasErrors ? null : () => Navigator.of(context).pop(_space),
onCancel: () async {
final newProducts = await showDialog<List<Product>>(
context: context,
builder: (context) => const AddDeviceTypeWidget(),
);
if (newProducts == null || newProducts.isEmpty) return;
setState(() {
for (final product in newProducts) {
_space.productAllocations.add(
ProductAllocation(
uuid: const Uuid().v4(),
product: product,
tag: Tag.empty(),
),
);
}
});
_validateAllTags();
},
cancelButtonLabel: 'Add New Device',
)
],
);
}
}

View File

@ -1,29 +0,0 @@
import 'package:flutter/material.dart';
import 'package:syncrow_web/utils/extension/build_context_x.dart';
class AssignTagsErrorMessages extends StatelessWidget {
const AssignTagsErrorMessages({super.key, required this.errorMessages});
final List<String> errorMessages;
@override
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.only(top: 16),
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: errorMessages
.map(
(error) => Text(
'- $error',
style: context.textTheme.bodyMedium?.copyWith(
color: context.theme.colorScheme.error,
),
),
)
.toList(),
),
);
}
}

View File

@ -1,204 +0,0 @@
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:syncrow_web/common/dialog_dropdown.dart';
import 'package:syncrow_web/pages/space_management_v2/modules/space_details/domain/models/space_details_model.dart';
import 'package:syncrow_web/pages/space_management_v2/modules/tags/data/services/remote_tags_service.dart';
import 'package:syncrow_web/pages/space_management_v2/modules/tags/domain/models/tag.dart';
import 'package:syncrow_web/pages/space_management_v2/modules/tags/presentation/bloc/tags_bloc.dart';
import 'package:syncrow_web/pages/space_management_v2/modules/tags/presentation/widgets/product_tag_field.dart';
import 'package:syncrow_web/services/api/http_service.dart';
import 'package:syncrow_web/utils/color_manager.dart';
import 'package:syncrow_web/utils/extension/build_context_x.dart';
class AssignTagsTable extends StatelessWidget {
const AssignTagsTable({
required this.productAllocations,
required this.subspaces,
required this.productLocations,
required this.onTagSelected,
required this.onLocationSelected,
required this.onProductDeleted,
super.key,
});
final List<ProductAllocation> productAllocations;
final List<Subspace> subspaces;
final Map<String, String?> productLocations;
final void Function(String, Tag) onTagSelected;
final void Function(String, String?) onLocationSelected;
final void Function(String) onProductDeleted;
DataColumn _buildDataColumn(BuildContext context, String label) {
return DataColumn(
label: SelectableText(label, style: context.textTheme.bodyMedium),
);
}
@override
Widget build(BuildContext context) {
return BlocProvider<TagsBloc>(
create: (BuildContext context) => TagsBloc(
RemoteTagsService(HTTPService()),
)..add(const LoadTags()),
child: BlocBuilder<TagsBloc, TagsState>(
builder: (context, state) {
return switch (state) {
TagsLoading() || TagsInitial() => const Center(
child: CircularProgressIndicator(),
),
TagsFailure(:final message) => Center(
child: Text(message),
),
TagsLoaded(:final tags) => ClipRRect(
borderRadius: BorderRadius.circular(20),
child: DataTable(
headingRowColor: WidgetStateProperty.all(
ColorsManager.dataHeaderGrey,
),
key: ValueKey(productAllocations.length),
border: TableBorder.all(
color: ColorsManager.dataHeaderGrey,
width: 1,
borderRadius: BorderRadius.circular(20),
),
columns: [
_buildDataColumn(context, '#'),
_buildDataColumn(context, 'Device'),
_buildDataColumn(context, 'Tag'),
_buildDataColumn(context, 'Location'),
],
rows: productAllocations.isEmpty
? [
DataRow(
cells: [
DataCell(
Center(
child: SelectableText(
'No Devices Available',
style: context.textTheme.bodyMedium?.copyWith(
color: ColorsManager.lightGrayColor,
),
),
),
),
DataCell.empty,
DataCell.empty,
DataCell.empty,
],
),
]
: List.generate(productAllocations.length, (index) {
final productAllocation = productAllocations[index];
final allocationUuid = productAllocation.uuid;
final availableTags = tags
.where(
(tag) =>
!productAllocations
.where((p) =>
p.product.productType ==
productAllocation.product.productType)
.map((p) => p.tag.name.toLowerCase())
.contains(tag.name.toLowerCase()) ||
tag.uuid == productAllocation.tag.uuid,
)
.toList();
final currentLocationUuid =
productLocations[allocationUuid];
final currentLocationName = currentLocationUuid == null
? 'Main Space'
: subspaces
.firstWhere((s) => s.uuid == currentLocationUuid)
.name;
return DataRow(
key: ValueKey(allocationUuid),
cells: [
DataCell(Text((index + 1).toString())),
DataCell(
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Expanded(
child: Text(
productAllocation.product.name,
overflow: TextOverflow.ellipsis,
)),
const SizedBox(width: 10),
Container(
width: 20,
height: 20,
decoration: BoxDecoration(
shape: BoxShape.circle,
border: Border.all(
color: ColorsManager.lightGrayColor,
width: 1,
),
),
child: IconButton(
icon: const Icon(
Icons.close,
color: ColorsManager.lightGreyColor,
size: 16,
),
onPressed: () {
onProductDeleted(allocationUuid);
},
tooltip: 'Delete Tag',
padding: EdgeInsets.zero,
constraints: const BoxConstraints(),
),
),
],
),
),
DataCell(
Container(
alignment: Alignment.centerLeft,
width: double.infinity,
child: ProductTagField(
key: ValueKey('dropdown_$allocationUuid'),
productName: productAllocation.product.uuid,
initialValue: productAllocation.tag,
onSelected: (newTag) {
onTagSelected(allocationUuid, newTag);
},
items: availableTags,
),
),
),
DataCell(
SizedBox(
width: double.infinity,
child: DialogDropdown(
items: [
'Main Space',
...subspaces.map((s) => s.name)
],
selectedValue: currentLocationName,
onSelected: (newLocationName) {
final newSubspaceUuid = newLocationName ==
'Main Space'
? null
: subspaces
.firstWhere(
(s) => s.name == newLocationName)
.uuid;
onLocationSelected(
allocationUuid, newSubspaceUuid);
},
)),
),
],
);
}),
),
),
_ => const SizedBox.shrink(),
};
},
),
);
}
}

View File

@ -1,186 +0,0 @@
import 'package:flutter/material.dart';
import 'package:syncrow_web/pages/space_management_v2/modules/tags/domain/models/tag.dart';
import 'package:syncrow_web/utils/color_manager.dart';
import 'package:syncrow_web/utils/extension/build_context_x.dart';
class ProductTagField extends StatefulWidget {
final List<Tag> items;
final ValueChanged<Tag> onSelected;
final Tag? initialValue;
final String productName;
const ProductTagField({
super.key,
required this.items,
required this.onSelected,
this.initialValue,
required this.productName,
});
@override
State<ProductTagField> createState() => _ProductTagFieldState();
}
class _ProductTagFieldState extends State<ProductTagField> {
bool _isOpen = false;
OverlayEntry? _overlayEntry;
final TextEditingController _controller = TextEditingController();
final FocusNode _focusNode = FocusNode();
@override
void initState() {
super.initState();
_controller.text = widget.initialValue?.name ?? '';
_focusNode.addListener(_handleFocusChange);
}
@override
void dispose() {
_focusNode.removeListener(_handleFocusChange);
_controller.dispose();
_focusNode.dispose();
_overlayEntry?.remove();
_overlayEntry = null;
super.dispose();
}
void _handleFocusChange() {
if (!_focusNode.hasFocus) {
_submit(_controller.text);
}
}
void _submit(String value) {
final lowerCaseValue = value.toLowerCase();
final selectedTag = widget.items.firstWhere(
(tag) => tag.name.toLowerCase() == lowerCaseValue,
orElse: () => Tag(
name: value,
uuid: '',
createdAt: '',
updatedAt: '',
),
);
widget.onSelected(selectedTag);
_closeDropdown();
}
void _toggleDropdown() {
if (_isOpen) {
_closeDropdown();
} else {
_openDropdown();
}
}
void _openDropdown() {
_overlayEntry = _createOverlayEntry();
Overlay.of(context).insert(_overlayEntry!);
setState(() => _isOpen = true);
}
void _closeDropdown() {
if (_isOpen) {
_overlayEntry?.remove();
_overlayEntry = null;
setState(() => _isOpen = false);
}
}
@override
Widget build(BuildContext context) {
return GestureDetector(
onTap: () => FocusScope.of(context).unfocus(),
behavior: HitTestBehavior.opaque,
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 12.0, vertical: 8.0),
decoration: BoxDecoration(
border: Border.all(color: ColorsManager.transparentColor),
borderRadius: BorderRadius.circular(8.0),
),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Expanded(
child: TextFormField(
controller: _controller,
focusNode: _focusNode,
onFieldSubmitted: _submit,
style: context.textTheme.bodyMedium,
decoration: const InputDecoration(
hintText: 'Enter or Select a tag',
border: InputBorder.none,
),
),
),
GestureDetector(
onTap: _toggleDropdown,
child: const Icon(Icons.arrow_drop_down),
),
],
),
),
);
}
OverlayEntry _createOverlayEntry() {
final renderBox = context.findRenderObject()! as RenderBox;
final size = renderBox.size;
final offset = renderBox.localToGlobal(Offset.zero);
return OverlayEntry(
builder: (context) {
return GestureDetector(
onTap: _closeDropdown,
behavior: HitTestBehavior.translucent,
child: Stack(
children: [
Positioned(
left: offset.dx,
top: offset.dy + size.height,
width: size.width,
child: Material(
elevation: 4.0,
child: Container(
color: ColorsManager.whiteColors,
constraints: const BoxConstraints(maxHeight: 200.0),
child: ListView.builder(
shrinkWrap: true,
itemCount: widget.items.length,
itemBuilder: (context, index) {
final tag = widget.items[index];
return Container(
decoration: const BoxDecoration(
border: Border(
bottom: BorderSide(
color: ColorsManager.lightGrayBorderColor,
width: 1.0,
),
),
),
child: ListTile(
title: Text(
tag.name,
style: context.textTheme.bodyMedium?.copyWith(
color: ColorsManager.textPrimaryColor,
),
),
onTap: () {
_controller.text = tag.name;
_submit(tag.name);
_closeDropdown();
},
),
);
},
),
),
),
),
],
),
);
},
);
}
}

View File

@ -1,67 +0,0 @@
import 'package:flutter/material.dart';
import 'package:syncrow_web/pages/space_management_v2/modules/products/domain/models/product.dart';
import 'package:syncrow_web/pages/space_management_v2/modules/tags/presentation/widgets/product_type_card_counter.dart';
import 'package:syncrow_web/pages/spaces_management/tag_model/widgets/device_icon_widget.dart';
import 'package:syncrow_web/utils/color_manager.dart';
import 'package:syncrow_web/utils/extension/build_context_x.dart';
class ProductTypeCard extends StatelessWidget {
const ProductTypeCard({
required this.product,
required this.count,
required this.onIncrement,
required this.onDecrement,
super.key,
});
final Product product;
final int count;
final void Function() onIncrement;
final void Function() onDecrement;
@override
Widget build(BuildContext context) {
return Card(
elevation: 2,
color: ColorsManager.whiteColors,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8),
),
child: Padding(
padding: const EdgeInsets.all(4),
child: Column(
spacing: 8,
mainAxisAlignment: MainAxisAlignment.start,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Expanded(child: DeviceIconWidget(icon: product.icon)),
_buildName(context, product.name),
ProductTypeCardCounter(
onIncrement: onIncrement,
onDecrement: onDecrement,
count: count,
),
const SizedBox(height: 4),
],
),
),
);
}
Widget _buildName(BuildContext context, String name) {
return Expanded(
child: SizedBox(
height: 35,
child: Text(
name,
style: context.textTheme.bodySmall?.copyWith(
color: ColorsManager.blackColor,
),
textAlign: TextAlign.center,
maxLines: 2,
overflow: TextOverflow.ellipsis,
),
),
);
}
}

View File

@ -1,58 +0,0 @@
import 'package:flutter/material.dart';
import 'package:syncrow_web/utils/color_manager.dart';
import 'package:syncrow_web/utils/extension/build_context_x.dart';
class ProductTypeCardCounter extends StatelessWidget {
const ProductTypeCardCounter({
super.key,
required this.onIncrement,
required this.onDecrement,
required this.count,
});
final int count;
final void Function() onIncrement;
final void Function() onDecrement;
@override
Widget build(BuildContext context) {
return Container(
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 5),
decoration: BoxDecoration(
color: ColorsManager.counterBackgroundColor,
borderRadius: BorderRadius.circular(20),
),
child: Row(
mainAxisSize: MainAxisSize.min,
spacing: 8,
children: [
_buildCounterButton(
Icons.remove,
onDecrement,
),
Text(
count.toString(),
style: context.textTheme.bodyLarge?.copyWith(
color: ColorsManager.spaceColor,
),
),
_buildCounterButton(Icons.add, onIncrement),
],
),
);
}
Widget _buildCounterButton(
IconData icon,
VoidCallback onPressed,
) {
return GestureDetector(
onTap: onPressed,
child: Icon(
icon,
color: ColorsManager.spaceColor.withValues(alpha: 0.3),
size: 18,
),
);
}
}

View File

@ -1,62 +0,0 @@
import 'package:flutter/material.dart';
import 'package:syncrow_web/pages/space_management_v2/modules/products/domain/models/product.dart';
import 'package:syncrow_web/pages/space_management_v2/modules/tags/presentation/widgets/product_type_card.dart';
import 'package:syncrow_web/utils/color_manager.dart';
import 'package:syncrow_web/utils/extension/build_context_x.dart';
class ProductsGrid extends StatelessWidget {
const ProductsGrid({
required this.products,
required this.selectedProducts,
required this.onIncrement,
required this.onDecrement,
super.key,
});
final List<Product> products;
final Map<Product, int> selectedProducts;
final void Function(Product) onIncrement;
final void Function(Product) onDecrement;
@override
Widget build(BuildContext context) {
final size = MediaQuery.of(context).size;
final crossAxisCount = switch (context.screenWidth) {
> 1200 => 8,
> 800 => 5,
_ => 3,
};
return SingleChildScrollView(
child: Container(
width: size.width * 0.9,
height: size.height * 0.65,
decoration: BoxDecoration(
color: ColorsManager.textFieldGreyColor,
borderRadius: BorderRadius.circular(8),
),
child: GridView.builder(
padding: const EdgeInsets.symmetric(
horizontal: 20,
),
shrinkWrap: true,
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: crossAxisCount,
mainAxisSpacing: 6,
crossAxisSpacing: 4,
childAspectRatio: 0.8,
),
itemCount: products.length,
itemBuilder: (context, index) {
final product = products[index];
return ProductTypeCard(
product: product,
count: selectedProducts[product] ?? 0,
onIncrement: () => onIncrement(product),
onDecrement: () => onDecrement(product),
);
},
),
),
);
}
}

View File

@ -13,7 +13,6 @@ class SpaceDetailsModelBloc extends Bloc<SpaceDetailsModelEvent, SpaceDetailsMod
on<UpdateSpaceDetailsSubspaces>(_onUpdateSpaceDetailsSubspaces);
on<UpdateSpaceDetailsProductAllocations>(
_onUpdateSpaceDetailsProductAllocations);
on<UpdateSpaceDetails>(_onUpdateSpaceDetails);
}
void _onUpdateSpaceDetailsIcon(
@ -43,11 +42,4 @@ class SpaceDetailsModelBloc extends Bloc<SpaceDetailsModelEvent, SpaceDetailsMod
) {
emit(state.copyWith(productAllocations: event.productAllocations));
}
void _onUpdateSpaceDetails(
UpdateSpaceDetails event,
Emitter<SpaceDetailsModel> emit,
) {
emit(event.space);
}
}

View File

@ -42,12 +42,3 @@ final class UpdateSpaceDetailsProductAllocations extends SpaceDetailsModelEvent
@override
List<Object> get props => [productAllocations];
}
final class UpdateSpaceDetails extends SpaceDetailsModelEvent {
const UpdateSpaceDetails(this.space);
final SpaceDetailsModel space;
@override
List<Object> get props => [space];
}

View File

@ -1,5 +1,5 @@
import 'dart:convert';
import 'package:syncrow_web/pages/access_management/booking_system/presentation/model/password_model.dart';
import 'package:syncrow_web/pages/access_management/model/password_model.dart';
import 'package:syncrow_web/pages/visitor_password/model/device_model.dart';
import 'package:syncrow_web/pages/visitor_password/model/schedule_model.dart';
import 'package:syncrow_web/services/api/http_service.dart';

View File

@ -84,5 +84,4 @@ abstract class ColorsManager {
static const Color minBlue = Color(0xFF93AAFD);
static const Color minBlueDot = Color(0xFF023DFE);
static const Color grey25 = Color(0xFFF9F9F9);
static const Color grey50 = Color(0xFF718096);
}

View File

@ -46,8 +46,7 @@ abstract class ApiEndpoints {
// Community Module
static const String createCommunity = '/projects/{projectId}/communities';
static const String getCommunityList = '/projects/{projectId}/communities';
static const String getCommunityListv2 =
'/projects/{projectId}/communities/v2';
static const String getCommunityListv2 = '/projects/{projectId}/communities/v2';
static const String getCommunityById =
'/projects/{projectId}/communities/{communityId}';
static const String updateCommunity =
@ -139,6 +138,4 @@ abstract class ApiEndpoints {
static const String assignDeviceToRoom =
'/projects/{projectUuid}/communities/{communityUuid}/spaces/{spaceUuid}/subspaces/{subSpaceUuid}/devices/{deviceUuid}';
static const String saveSchedule = '/schedule/{deviceUuid}';
static const String getBookableSpaces = '/bookable-spaces';
}

View File

@ -63,9 +63,6 @@ dependencies:
bloc: ^9.0.0
geocoding: ^4.0.0
gauge_indicator: ^0.4.3
calendar_view: ^1.4.0
calendar_date_picker2: ^2.0.1
dev_dependencies: