Refactor booking system: replace individual parameters with LoadBookableSpacesParam for improved clarity and maintainability

This commit is contained in:
mohammad
2025-07-10 10:56:10 +03:00
parent 2d16bda61d
commit 3e95bf4473
6 changed files with 244 additions and 199 deletions

View File

@ -1,6 +1,7 @@
import 'package:dio/dio.dart'; import 'package:dio/dio.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/models/paginated_bookable_spaces.dart';
import 'package:syncrow_web/pages/access_management/booking_system/domain/services/booking_system_service.dart'; import 'package:syncrow_web/pages/access_management/booking_system/domain/services/booking_system_service.dart';
import 'package:syncrow_web/pages/space_management_v2/modules/communities/domain/params/load_communities_param.dart';
import 'package:syncrow_web/services/api/api_exception.dart'; import 'package:syncrow_web/services/api/api_exception.dart';
import 'package:syncrow_web/services/api/http_service.dart'; import 'package:syncrow_web/services/api/http_service.dart';
import 'package:syncrow_web/utils/constants/api_const.dart'; import 'package:syncrow_web/utils/constants/api_const.dart';
@ -13,25 +14,29 @@ class BookableSpacesService implements BookingSystemService {
@override @override
Future<PaginatedBookableSpaces> getBookableSpaces({ Future<PaginatedBookableSpaces> getBookableSpaces({
required int page, required LoadCommunitiesParam param,
required int size,
required String search,
}) async { }) async {
try { try {
final response = await _httpService.get( final response = await _httpService.get(
path: ApiEndpoints.getBookableSpaces, path: ApiEndpoints.getBookableSpaces,
queryParameters: { queryParameters: {
'page': page, 'page': param.page,
'size': size, 'size': param.size,
'active': true, 'active': true,
'configured': true, 'configured': true,
if (search.isNotEmpty && search != 'null') 'search': search, if (param.search != null &&
}, param.search.isNotEmpty &&
expectedResponseModel: (json) { param.search != 'null')
return PaginatedBookableSpaces.fromJson( 'search': param.search,
json as Map<String, dynamic>, if (param.includeSpaces != null)
); 'includeSpaces': param.includeSpaces,
}); },
expectedResponseModel: (json) {
return PaginatedBookableSpaces.fromJson(
json as Map<String, dynamic>,
);
},
);
return response; return response;
} on DioException catch (e) { } on DioException catch (e) {
final responseData = e.response?.data; final responseData = e.response?.data;

View File

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

View File

@ -1,6 +1,8 @@
import 'dart:async'; import 'dart:async';
import 'package:meta/meta.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/models/paginated_bookable_spaces.dart';
import 'package:syncrow_web/pages/access_management/booking_system/domain/services/booking_system_service.dart'; import 'package:syncrow_web/pages/access_management/booking_system/domain/services/booking_system_service.dart';
import 'package:syncrow_web/pages/space_management_v2/modules/communities/domain/params/load_communities_param.dart';
class DebouncedBookingSystemService implements BookingSystemService { class DebouncedBookingSystemService implements BookingSystemService {
final BookingSystemService _inner; final BookingSystemService _inner;
@ -9,11 +11,6 @@ class DebouncedBookingSystemService implements BookingSystemService {
Timer? _debounceTimer; Timer? _debounceTimer;
Completer<PaginatedBookableSpaces>? _lastCompleter; Completer<PaginatedBookableSpaces>? _lastCompleter;
int? _lastPage;
int? _lastSize;
bool? _lastIncludeSpaces;
String? _lastSearch;
DebouncedBookingSystemService( DebouncedBookingSystemService(
this._inner, { this._inner, {
this.debounceDuration = const Duration(milliseconds: 500), this.debounceDuration = const Duration(milliseconds: 500),
@ -21,27 +18,20 @@ class DebouncedBookingSystemService implements BookingSystemService {
@override @override
Future<PaginatedBookableSpaces> getBookableSpaces({ Future<PaginatedBookableSpaces> getBookableSpaces({
required int page, required LoadCommunitiesParam param,
required int size,
required String search,
}) { }) {
_debounceTimer?.cancel(); _debounceTimer?.cancel();
_lastCompleter?.completeError(StateError("Cancelled by new search")); if (_lastCompleter != null && !_lastCompleter!.isCompleted) {
_lastCompleter!
.completeError(StateError("Cancelled by new search"));
}
final completer = Completer<PaginatedBookableSpaces>(); final completer = Completer<PaginatedBookableSpaces>();
_lastCompleter = completer; _lastCompleter = completer;
_lastPage = page;
_lastSize = size;
_lastSearch = search;
_debounceTimer = Timer(debounceDuration, () async { _debounceTimer = Timer(debounceDuration, () async {
try { try {
final result = await _inner.getBookableSpaces( final result = await _inner.getBookableSpaces(param: param);
page: _lastPage!,
size: _lastSize!,
search: _lastSearch!,
);
if (!completer.isCompleted) { if (!completer.isCompleted) {
completer.complete(result); completer.complete(result);
} }

View File

@ -1,10 +1,8 @@
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/models/paginated_bookable_spaces.dart';
import 'package:syncrow_web/pages/space_management_v2/modules/communities/domain/params/load_communities_param.dart';
abstract class BookingSystemService { abstract class BookingSystemService {
Future<PaginatedBookableSpaces> getBookableSpaces({ Future<PaginatedBookableSpaces> getBookableSpaces({
required int page, required LoadCommunitiesParam param,
required int size,
required String search,
}); });
} }

View File

@ -3,10 +3,10 @@ import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:syncrow_web/pages/access_management/booking_system/domain/services/booking_system_service.dart'; import 'package:syncrow_web/pages/access_management/booking_system/domain/services/booking_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_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/bloc/sidebar/sidebar_state.dart';
import 'package:syncrow_web/pages/space_management_v2/modules/communities/domain/params/load_communities_param.dart';
class SidebarBloc extends Bloc<SidebarEvent, SidebarState> { class SidebarBloc extends Bloc<SidebarEvent, SidebarState> {
final BookingSystemService _bookingService; final BookingSystemService _bookingService;
Timer? _searchDebounce;
int _currentPage = 1; int _currentPage = 1;
final int _pageSize = 20; final int _pageSize = 20;
String _currentSearch = ''; String _currentSearch = '';
@ -35,9 +35,11 @@ class SidebarBloc extends Bloc<SidebarEvent, SidebarState> {
_currentSearch = ''; _currentSearch = '';
final paginatedSpaces = await _bookingService.getBookableSpaces( final paginatedSpaces = await _bookingService.getBookableSpaces(
page: _currentPage, param: LoadCommunitiesParam(
size: _pageSize, page: _currentPage,
search: _currentSearch, size: _pageSize,
search: _currentSearch,
),
); );
emit(state.copyWith( emit(state.copyWith(
@ -67,9 +69,12 @@ class SidebarBloc extends Bloc<SidebarEvent, SidebarState> {
_currentPage++; _currentPage++;
final paginatedSpaces = await _bookingService.getBookableSpaces( final paginatedSpaces = await _bookingService.getBookableSpaces(
page: _currentPage, param: LoadCommunitiesParam(
size: _pageSize, page: _currentPage,
search: _currentSearch, size: _pageSize,
search: _currentSearch,
// Add any other required params
),
); );
final updatedRooms = [...state.allRooms, ...paginatedSpaces.data]; final updatedRooms = [...state.allRooms, ...paginatedSpaces.data];
@ -79,6 +84,7 @@ class SidebarBloc extends Bloc<SidebarEvent, SidebarState> {
displayedRooms: updatedRooms, displayedRooms: updatedRooms,
isLoadingMore: false, isLoadingMore: false,
hasMore: paginatedSpaces.hasNext, hasMore: paginatedSpaces.hasNext,
totalPages: paginatedSpaces.totalPage,
currentPage: _currentPage, currentPage: _currentPage,
)); ));
} catch (e) { } catch (e) {
@ -99,11 +105,13 @@ class SidebarBloc extends Bloc<SidebarEvent, SidebarState> {
_currentPage = 1; _currentPage = 1;
emit(state.copyWith(isLoading: true, errorMessage: null)); emit(state.copyWith(isLoading: true, errorMessage: null));
final paginatedSpaces = await _bookingService.getBookableSpaces( final paginatedSpaces = await _bookingService.getBookableSpaces(
page: _currentPage, param: LoadCommunitiesParam(
size: _pageSize, page: _currentPage,
search: _currentSearch, size: _pageSize,
search: _currentSearch,
// Add other fields if required
),
); );
emit(state.copyWith( emit(state.copyWith(
allRooms: paginatedSpaces.data, allRooms: paginatedSpaces.data,
displayedRooms: paginatedSpaces.data, displayedRooms: paginatedSpaces.data,
@ -137,7 +145,6 @@ class SidebarBloc extends Bloc<SidebarEvent, SidebarState> {
@override @override
Future<void> close() { Future<void> close() {
_searchDebounce?.cancel();
return super.close(); return super.close();
} }
} }

View File

@ -40,181 +40,191 @@ class WeeklyCalendarPage extends StatelessWidget {
const double timeLineWidth = 80; const double timeLineWidth = 80;
const int totalDays = 7; const int totalDays = 7;
final double dayColumnWidth = final double dayColumnWidth =
(calendarWidth - timeLineWidth) / totalDays; (calendarWidth - timeLineWidth) / totalDays - 0.1;
final selectedDayIndex = final selectedDayIndex =
weekDays.indexWhere((d) => isSameDay(d, selectedDate)); weekDays.indexWhere((d) => isSameDay(d, selectedDate));
return Padding( return Padding(
padding: const EdgeInsets.only(left: 25.0, right: 25.0, top: 25), padding: const EdgeInsets.only(left: 25.0, right: 25.0, top: 25),
child: Stack( child: Stack(
children: [ children: [
WeekView( AbsorbPointer(
key: ValueKey(weekStart), absorbing: false,
controller: eventController, ignoringSemantics: false,
initialDay: weekStart, child: WeekView(
startHour: startHour - 1, scrollPhysics: const NeverScrollableScrollPhysics(),
endHour: endHour, key: ValueKey(weekStart),
heightPerMinute: 1.1, controller: eventController,
showLiveTimeLineInAllDays: false, initialDay: weekStart,
showVerticalLines: true, startHour: startHour - 1,
emulateVerticalOffsetBy: -80, endHour: endHour,
startDay: WeekDays.monday, heightPerMinute: 1.1,
liveTimeIndicatorSettings: const LiveTimeIndicatorSettings( showLiveTimeLineInAllDays: false,
showBullet: false, showVerticalLines: true,
height: 0, emulateVerticalOffsetBy: -80,
), startDay: WeekDays.monday,
weekDayBuilder: (date) { liveTimeIndicatorSettings: const LiveTimeIndicatorSettings(
final weekDays = _getWeekDays(weekStart); showBullet: false,
final selectedDayIndex = height: 0,
weekDays.indexWhere((d) => isSameDay(d, selectedDate));
final index = weekDays.indexWhere((d) => isSameDay(d, date));
final isSelectedDay = index == selectedDayIndex;
final isToday = isSameDay(date, DateTime.now());
return Container(
decoration: isSelectedDay
? BoxDecoration(
color: ColorsManager.blue1.withOpacity(0.2),
borderRadius: BorderRadius.circular(6),
)
: isToday
? BoxDecoration(
color: ColorsManager.blue1.withOpacity(0.08),
borderRadius: BorderRadius.circular(6),
)
: null,
child: 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,
),
),
],
), ),
); weekDayBuilder: (date) {
}, final weekDays = _getWeekDays(weekStart);
timeLineBuilder: (date) { final selectedDayIndex = weekDays
int hour = date.hour == 0 .indexWhere((d) => isSameDay(d, selectedDate));
? 12 final index =
: (date.hour > 12 ? date.hour - 12 : date.hour); weekDays.indexWhere((d) => isSameDay(d, date));
String period = date.hour >= 12 ? 'PM' : 'AM'; final isSelectedDay = index == selectedDayIndex;
return Container( final isToday = isSameDay(date, DateTime.now());
height: 60,
alignment: Alignment.center, return Column(
child: RichText(
text: TextSpan(
children: [ children: [
TextSpan( Text(
text: '$hour', DateFormat('EEE').format(date).toUpperCase(),
style: const TextStyle( style: TextStyle(
fontWeight: FontWeight.w700, fontWeight: FontWeight.w400,
fontSize: 24, fontSize: 14,
color: ColorsManager.blackColor, color: isSelectedDay ? Colors.blue : Colors.black,
), ),
), ),
WidgetSpan( Text(
child: Padding( DateFormat('d').format(date),
padding: const EdgeInsets.only(left: 2, top: 6), style: TextStyle(
child: Text( fontWeight: FontWeight.w700,
period, 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( style: const TextStyle(
fontWeight: FontWeight.w400, fontWeight: FontWeight.w700,
fontSize: 12, fontSize: 24,
color: ColorsManager.blackColor, color: ColorsManager.blackColor,
letterSpacing: 1,
), ),
), ),
), WidgetSpan(
alignment: PlaceholderAlignment.baseline, child: Padding(
baseline: TextBaseline.alphabetic, 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(
timeLineWidth: timeLineWidth, margin: const EdgeInsets.symmetric(
weekPageHeaderBuilder: (start, end) => Container(), vertical: 2, horizontal: 2),
weekTitleHeight: 60, child: Column(
weekNumberBuilder: (firstDayOfWeek) => Text( crossAxisAlignment: CrossAxisAlignment.start,
firstDayOfWeek.timeZoneName, children: events.map((event) {
style: Theme.of(context).textTheme.bodyMedium?.copyWith( final bool isEventEnded = event.endTime != null &&
color: Colors.black, event.endTime!.isBefore(DateTime.now());
fontWeight: FontWeight.bold, return Expanded(
), child: Container(
), width: double.infinity,
eventTileBuilder: (date, events, boundary, start, end) { padding: const EdgeInsets.all(6),
return Container( decoration: BoxDecoration(
margin: color: isEventEnded
const EdgeInsets.symmetric(vertical: 2, horizontal: 2), ? ColorsManager.lightGrayBorderColor
child: Column( : ColorsManager.blue1.withOpacity(0.25),
crossAxisAlignment: CrossAxisAlignment.start, borderRadius: BorderRadius.circular(6),
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.grayColor
: ColorsManager.lightGrayColor
.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), child: Column(
Text( crossAxisAlignment: CrossAxisAlignment.start,
event.title, children: [
style: const TextStyle( Text(
fontSize: 12, DateFormat('h:mm a')
color: ColorsManager.blackColor, .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(),
); ),
}).toList(), );
), },
); )),
},
),
if (selectedDayIndex >= 0) if (selectedDayIndex >= 0)
Positioned( Positioned(
left: timeLineWidth + dayColumnWidth * selectedDayIndex, left: (timeLineWidth + 3) +
(dayColumnWidth - 8) * (selectedDayIndex - 0.01),
top: 0, top: 0,
bottom: 0, bottom: 0,
width: dayColumnWidth, width: dayColumnWidth,
child: IgnorePointer( child: IgnorePointer(
child: Container( child: Container(
margin: const EdgeInsets.symmetric( margin: const EdgeInsets.symmetric(
vertical: 0, horizontal: 2), vertical: 0, horizontal: 4),
color: ColorsManager.blue1.withOpacity(0.1), color: ColorsManager.spaceColor.withOpacity(0.07),
), ),
), ),
), ),
@ -253,7 +263,6 @@ int _parseHour(String? time, {required int defaultValue}) {
} }
try { try {
return int.parse(time.split(':')[0]); return int.parse(time.split(':')[0]);
} catch (e) { } catch (e) {
return defaultValue; return defaultValue;
} }