mirror of
https://github.com/SyncrowIOT/web.git
synced 2025-08-25 14:39:41 +00:00
Merge branch 'dev' into SP-1601-FE-Community-and-Space-Dialog-Redesign-in-the-routine-tab
This commit is contained in:
@ -0,0 +1,63 @@
|
||||
import 'package:syncrow_web/pages/access_management/booking_system/data/services/remote_calendar_service.dart';
|
||||
import 'package:syncrow_web/pages/access_management/booking_system/domain/LoadEventsParam.dart';
|
||||
import 'package:syncrow_web/pages/access_management/booking_system/domain/models/calendar_event_booking.dart';
|
||||
import 'package:syncrow_web/pages/access_management/booking_system/domain/services/calendar_system_service.dart';
|
||||
|
||||
class MemoryCalendarService implements CalendarSystemService {
|
||||
final Map<String, CalendarEventsResponse> _eventsCache = {};
|
||||
|
||||
@override
|
||||
Future<CalendarEventsResponse> getCalendarEvents({
|
||||
required LoadEventsParam params,
|
||||
}) async {
|
||||
final key = params.generateKey();
|
||||
|
||||
return _eventsCache[key]!;
|
||||
}
|
||||
|
||||
void setEvents(
|
||||
LoadEventsParam param,
|
||||
CalendarEventsResponse events,
|
||||
) {
|
||||
final key = param.generateKey();
|
||||
_eventsCache[key] = events;
|
||||
}
|
||||
|
||||
void addEvent(LoadEventsParam param, CalendarEventsResponse event) {
|
||||
final key = param.generateKey();
|
||||
|
||||
_eventsCache[key] = event;
|
||||
}
|
||||
|
||||
void clear() {
|
||||
_eventsCache.clear();
|
||||
}
|
||||
}
|
||||
|
||||
class MemoryCalendarServiceWithRemoteFallback implements CalendarSystemService {
|
||||
final MemoryCalendarService memoryService;
|
||||
final RemoteCalendarService remoteService;
|
||||
|
||||
MemoryCalendarServiceWithRemoteFallback({
|
||||
required this.memoryService,
|
||||
required this.remoteService,
|
||||
});
|
||||
|
||||
@override
|
||||
Future<CalendarEventsResponse> getCalendarEvents({
|
||||
required LoadEventsParam params,
|
||||
}) async {
|
||||
final key = params.generateKey();
|
||||
final doesExistInMemory = memoryService._eventsCache.containsKey(key);
|
||||
|
||||
if (doesExistInMemory) {
|
||||
return memoryService.getCalendarEvents(params: params);
|
||||
} else {
|
||||
final remoteResult =
|
||||
await remoteService.getCalendarEvents(params: params);
|
||||
memoryService.setEvents(params, remoteResult);
|
||||
|
||||
return remoteResult;
|
||||
}
|
||||
}
|
||||
}
|
@ -18,7 +18,7 @@ class RemoteBookableSpacesService implements BookableSystemService {
|
||||
}) async {
|
||||
try {
|
||||
final response = await _httpService.get(
|
||||
path: ApiEndpoints.getBookableSpaces,
|
||||
path: ApiEndpoints.bookableSpaces,
|
||||
queryParameters: {
|
||||
'page': param.page,
|
||||
'size': param.size,
|
||||
|
@ -1,4 +1,5 @@
|
||||
import 'package:dio/dio.dart';
|
||||
import 'package:syncrow_web/pages/access_management/booking_system/domain/LoadEventsParam.dart';
|
||||
import 'package:syncrow_web/pages/access_management/booking_system/domain/models/calendar_event_booking.dart';
|
||||
import 'package:syncrow_web/pages/access_management/booking_system/domain/services/calendar_system_service.dart';
|
||||
import 'package:syncrow_web/services/api/api_exception.dart';
|
||||
@ -13,147 +14,21 @@ class RemoteCalendarService implements CalendarSystemService {
|
||||
|
||||
@override
|
||||
Future<CalendarEventsResponse> getCalendarEvents({
|
||||
required String spaceId,
|
||||
required LoadEventsParam params,
|
||||
}) async {
|
||||
final month = params.startDate.month.toString().padLeft(2, '0');
|
||||
final year = params.startDate.year.toString();
|
||||
|
||||
try {
|
||||
final response = await _httpService.get(
|
||||
path: ApiEndpoints.getCalendarEvents,
|
||||
queryParameters: {
|
||||
'spaceId': spaceId,
|
||||
},
|
||||
return await _httpService.get<CalendarEventsResponse>(
|
||||
path: ApiEndpoints.getBookings
|
||||
.replaceAll('{mm}', month)
|
||||
.replaceAll('{yyyy}', year)
|
||||
.replaceAll('{space}', params.id),
|
||||
expectedResponseModel: (json) {
|
||||
return CalendarEventsResponse.fromJson(
|
||||
json as Map<String, dynamic>,
|
||||
);
|
||||
return CalendarEventsResponse.fromJson(json as Map<String, dynamic>);
|
||||
},
|
||||
);
|
||||
|
||||
return CalendarEventsResponse.fromJson(response as Map<String, dynamic>);
|
||||
} on DioException catch (e) {
|
||||
final responseData = e.response?.data;
|
||||
if (responseData is Map<String, dynamic>) {
|
||||
final errorMessage = responseData['error']?['message'] as String? ??
|
||||
responseData['message'] as String? ??
|
||||
_defaultErrorMessage;
|
||||
throw APIException(errorMessage);
|
||||
}
|
||||
throw APIException(_defaultErrorMessage);
|
||||
} catch (e) {
|
||||
throw APIException('$_defaultErrorMessage: ${e.toString()}');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class FakeRemoteCalendarService implements CalendarSystemService {
|
||||
const FakeRemoteCalendarService(this._httpService, {this.useDummy = false});
|
||||
|
||||
final HTTPService _httpService;
|
||||
final bool useDummy;
|
||||
static const _defaultErrorMessage = 'Failed to load Calendar';
|
||||
|
||||
@override
|
||||
Future<CalendarEventsResponse> getCalendarEvents({
|
||||
required String spaceId,
|
||||
}) async {
|
||||
if (useDummy) {
|
||||
final dummyJson = {
|
||||
'statusCode': 200,
|
||||
'message': 'Successfully fetched all bookings',
|
||||
'data': [
|
||||
{
|
||||
'uuid': 'd4553fa6-a0c9-4f42-81c9-99a13a57bf80',
|
||||
'date': '2025-07-11T10:22:00.626Z',
|
||||
'startTime': '09:00:00',
|
||||
'endTime': '12:00:00',
|
||||
'cost': 10,
|
||||
'user': {
|
||||
'uuid': '784394ff-3197-4c39-9f07-48dc44920b1e',
|
||||
'firstName': 'salsabeel',
|
||||
'lastName': 'abuzaid',
|
||||
'email': 'test@test.com',
|
||||
'companyName': null
|
||||
},
|
||||
'space': {
|
||||
'uuid': '000f4d81-43e4-4ad7-865c-0f8b04b7081e',
|
||||
'spaceName': '2(1)'
|
||||
}
|
||||
},
|
||||
{
|
||||
'uuid': 'e9b27af0-b963-4d98-9657-454c4ba78561',
|
||||
'date': '2025-07-11T10:22:00.626Z',
|
||||
'startTime': '12:00:00',
|
||||
'endTime': '13:00:00',
|
||||
'cost': 10,
|
||||
'user': {
|
||||
'uuid': '784394ff-3197-4c39-9f07-48dc44920b1e',
|
||||
'firstName': 'salsabeel',
|
||||
'lastName': 'abuzaid',
|
||||
'email': 'test@test.com',
|
||||
'companyName': null
|
||||
},
|
||||
'space': {
|
||||
'uuid': '000f4d81-43e4-4ad7-865c-0f8b04b7081e',
|
||||
'spaceName': '2(1)'
|
||||
}
|
||||
},
|
||||
{
|
||||
'uuid': 'e9b27af0-b963-4d98-9657-454c4ba78561',
|
||||
'date': '2025-07-13T10:22:00.626Z',
|
||||
'startTime': '15:30:00',
|
||||
'endTime': '19:00:00',
|
||||
'cost': 20,
|
||||
'user': {
|
||||
'uuid': '784394ff-3197-4c39-9f07-48dc44920b1e',
|
||||
'firstName': 'salsabeel',
|
||||
'lastName': 'abuzaid',
|
||||
'email': 'test@test.com',
|
||||
'companyName': null
|
||||
},
|
||||
'space': {
|
||||
'uuid': '000f4d81-43e4-4ad7-865c-0f8b04b7081e',
|
||||
'spaceName': '2(1)'
|
||||
}
|
||||
}
|
||||
],
|
||||
'success': true
|
||||
};
|
||||
final response = CalendarEventsResponse.fromJson(dummyJson);
|
||||
|
||||
// Filter events by spaceId
|
||||
final filteredData = response.data.where((event) {
|
||||
return event.space.uuid == spaceId;
|
||||
}).toList();
|
||||
print('Filtering events for spaceId: $spaceId');
|
||||
print('Found ${filteredData.length} matching events');
|
||||
return filteredData.isNotEmpty
|
||||
? CalendarEventsResponse(
|
||||
statusCode: response.statusCode,
|
||||
message: response.message,
|
||||
data: filteredData,
|
||||
success: response.success,
|
||||
)
|
||||
: CalendarEventsResponse(
|
||||
statusCode: 404,
|
||||
message: 'No events found for spaceId: $spaceId',
|
||||
data: [],
|
||||
success: false,
|
||||
);
|
||||
}
|
||||
|
||||
try {
|
||||
final response = await _httpService.get(
|
||||
path: ApiEndpoints.getCalendarEvents,
|
||||
queryParameters: {
|
||||
'spaceId': spaceId,
|
||||
},
|
||||
expectedResponseModel: (json) {
|
||||
return CalendarEventsResponse.fromJson(
|
||||
json as Map<String, dynamic>,
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
return CalendarEventsResponse.fromJson(response as Map<String, dynamic>);
|
||||
} on DioException catch (e) {
|
||||
final responseData = e.response?.data;
|
||||
if (responseData is Map<String, dynamic>) {
|
||||
|
@ -0,0 +1,34 @@
|
||||
import 'package:equatable/equatable.dart';
|
||||
|
||||
class LoadEventsParam extends Equatable {
|
||||
final DateTime startDate;
|
||||
final DateTime endDate;
|
||||
final String id;
|
||||
|
||||
const LoadEventsParam({
|
||||
required this.startDate,
|
||||
required this.endDate,
|
||||
required this.id,
|
||||
});
|
||||
|
||||
@override
|
||||
List<Object?> get props => [startDate, endDate, id];
|
||||
|
||||
LoadEventsParam copyWith({
|
||||
DateTime? startDate,
|
||||
DateTime? endDate,
|
||||
String? id,
|
||||
}) {
|
||||
return LoadEventsParam(
|
||||
startDate: startDate ?? this.startDate,
|
||||
endDate: endDate ?? this.endDate,
|
||||
id: id ?? this.id,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
extension KeyGenerator on LoadEventsParam {
|
||||
String generateKey() {
|
||||
return '$id-${startDate.year}-${startDate.month.toString().padLeft(2, '0')}';
|
||||
}
|
||||
}
|
@ -1,7 +1,8 @@
|
||||
import 'package:syncrow_web/pages/access_management/booking_system/domain/LoadEventsParam.dart';
|
||||
import 'package:syncrow_web/pages/access_management/booking_system/domain/models/calendar_event_booking.dart';
|
||||
|
||||
abstract class CalendarSystemService {
|
||||
Future<CalendarEventsResponse> getCalendarEvents({
|
||||
required String spaceId,
|
||||
required LoadEventsParam params,
|
||||
});
|
||||
}
|
||||
|
@ -2,37 +2,48 @@ import 'dart:async';
|
||||
import 'package:bloc/bloc.dart';
|
||||
import 'package:calendar_view/calendar_view.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:syncrow_web/pages/access_management/booking_system/domain/LoadEventsParam.dart';
|
||||
import 'package:syncrow_web/pages/access_management/booking_system/domain/models/calendar_event_booking.dart';
|
||||
import 'package:syncrow_web/pages/access_management/booking_system/domain/services/calendar_system_service.dart';
|
||||
|
||||
import 'package:syncrow_web/pages/access_management/booking_system/data/services/memory_bookable_space_service.dart';
|
||||
part 'events_event.dart';
|
||||
part 'events_state.dart';
|
||||
|
||||
class CalendarEventsBloc extends Bloc<CalendarEventsEvent, CalendarEventState> {
|
||||
final EventController eventController = EventController();
|
||||
EventController eventController = EventController();
|
||||
final CalendarSystemService calendarService;
|
||||
|
||||
CalendarEventsBloc({required this.calendarService}) : super(EventsInitial()) {
|
||||
CalendarEventsBloc({
|
||||
required this.calendarService,
|
||||
}) : super(EventsInitial()) {
|
||||
on<LoadEvents>(_onLoadEvents);
|
||||
on<AddEvent>(_onAddEvent);
|
||||
on<StartTimer>(_onStartTimer);
|
||||
on<DisposeResources>(_onDisposeResources);
|
||||
on<GoToWeek>(_onGoToWeek);
|
||||
on<ResetEvents>(_onResetEvents);
|
||||
}
|
||||
|
||||
Future<void> _onLoadEvents(
|
||||
LoadEvents event,
|
||||
Emitter<CalendarEventState> emit,
|
||||
) async {
|
||||
final param = event.param;
|
||||
final month = param.endDate.month;
|
||||
final year = param.endDate.year;
|
||||
final spaceId = param.id;
|
||||
|
||||
emit(EventsLoading());
|
||||
try {
|
||||
final response = await calendarService.getCalendarEvents(
|
||||
spaceId: event.spaceId,
|
||||
);
|
||||
final events =
|
||||
response.data.map<CalendarEventData>(_toCalendarEventData).toList();
|
||||
final response = await calendarService.getCalendarEvents(params: param);
|
||||
|
||||
final events = response.data.map(_toCalendarEventData).toList();
|
||||
eventController.addAll(events);
|
||||
emit(EventsLoaded(events: events));
|
||||
emit(EventsLoaded(
|
||||
events: events,
|
||||
spaceId: spaceId,
|
||||
month: month,
|
||||
year: year,
|
||||
));
|
||||
} catch (e) {
|
||||
emit(EventsError('Failed to load events'));
|
||||
}
|
||||
@ -40,16 +51,19 @@ class CalendarEventsBloc extends Bloc<CalendarEventsEvent, CalendarEventState> {
|
||||
|
||||
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],
|
||||
spaceId: loaded.spaceId,
|
||||
month: loaded.month,
|
||||
year: loaded.year,
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
void _onStartTimer(StartTimer event, Emitter<CalendarEventState> emit) {}
|
||||
|
||||
void _onDisposeResources(
|
||||
DisposeResources event, Emitter<CalendarEventState> emit) {
|
||||
eventController.dispose();
|
||||
@ -61,6 +75,9 @@ class CalendarEventsBloc extends Bloc<CalendarEventsEvent, CalendarEventState> {
|
||||
final newWeekDays = _getWeekDays(event.weekDate);
|
||||
emit(EventsLoaded(
|
||||
events: loaded.events,
|
||||
spaceId: loaded.spaceId,
|
||||
month: loaded.month,
|
||||
year: loaded.year,
|
||||
));
|
||||
}
|
||||
}
|
||||
@ -90,14 +107,13 @@ class CalendarEventsBloc extends Bloc<CalendarEventsEvent, CalendarEventState> {
|
||||
);
|
||||
|
||||
return CalendarEventData(
|
||||
date: startTime,
|
||||
date: startTime,
|
||||
startTime: startTime,
|
||||
endTime: endTime,
|
||||
title:
|
||||
'${booking.space.spaceName} - ${booking.user.firstName} ${booking.user.lastName}',
|
||||
title: '${booking.user.firstName} ${booking.user.lastName}',
|
||||
description: 'Cost: ${booking.cost}',
|
||||
color: Colors.blue,
|
||||
event: booking,
|
||||
event: booking,
|
||||
);
|
||||
}
|
||||
|
||||
@ -112,4 +128,18 @@ class CalendarEventsBloc extends Bloc<CalendarEventsEvent, CalendarEventState> {
|
||||
eventController.dispose();
|
||||
return super.close();
|
||||
}
|
||||
|
||||
void _onResetEvents(
|
||||
ResetEvents event,
|
||||
Emitter<CalendarEventState> emit,
|
||||
) {
|
||||
if (calendarService is MemoryCalendarServiceWithRemoteFallback) {
|
||||
(calendarService as MemoryCalendarServiceWithRemoteFallback)
|
||||
.memoryService
|
||||
.clear();
|
||||
}
|
||||
eventController.dispose();
|
||||
eventController = EventController();
|
||||
emit(EventsInitial());
|
||||
}
|
||||
}
|
||||
|
@ -6,17 +6,11 @@ abstract class CalendarEventsEvent {
|
||||
}
|
||||
|
||||
class LoadEvents extends CalendarEventsEvent {
|
||||
final String spaceId;
|
||||
final DateTime weekStart;
|
||||
final DateTime weekEnd;
|
||||
|
||||
const LoadEvents({
|
||||
required this.spaceId,
|
||||
required this.weekStart,
|
||||
required this.weekEnd,
|
||||
});
|
||||
final LoadEventsParam param;
|
||||
const LoadEvents(this.param);
|
||||
}
|
||||
|
||||
|
||||
class AddEvent extends CalendarEventsEvent {
|
||||
final CalendarEventData event;
|
||||
const AddEvent(this.event);
|
||||
@ -35,3 +29,10 @@ class CheckWeekHasEvents extends CalendarEventsEvent {
|
||||
final DateTime weekStart;
|
||||
const CheckWeekHasEvents(this.weekStart);
|
||||
}
|
||||
|
||||
class ResetEvents extends CalendarEventsEvent {
|
||||
const ResetEvents();
|
||||
|
||||
@override
|
||||
List<Object?> get props => [];
|
||||
}
|
@ -7,11 +7,17 @@ class EventsInitial extends CalendarEventState {}
|
||||
|
||||
class EventsLoading extends CalendarEventState {}
|
||||
|
||||
class EventsLoaded extends CalendarEventState {
|
||||
final class EventsLoaded extends CalendarEventState {
|
||||
final List<CalendarEventData> events;
|
||||
final String spaceId;
|
||||
final int month;
|
||||
final int year;
|
||||
|
||||
EventsLoaded({
|
||||
required this.events,
|
||||
required this.spaceId,
|
||||
required this.month,
|
||||
required this.year,
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -1,12 +1,15 @@
|
||||
// booking_page.dart
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:calendar_view/calendar_view.dart';
|
||||
import 'package:syncrow_web/pages/access_management/booking_system/data/services/remote_calendar_service.dart';
|
||||
import 'package:syncrow_web/pages/access_management/booking_system/domain/LoadEventsParam.dart';
|
||||
import 'package:syncrow_web/pages/access_management/booking_system/presentation/bloc/calendar/events_bloc.dart';
|
||||
import 'package:syncrow_web/pages/access_management/booking_system/presentation/bloc/date_selection/date_selection_bloc.dart';
|
||||
import 'package:syncrow_web/pages/access_management/booking_system/presentation/bloc/date_selection/date_selection_event.dart';
|
||||
import 'package:syncrow_web/pages/access_management/booking_system/presentation/bloc/date_selection/date_selection_state.dart';
|
||||
import 'package:syncrow_web/pages/access_management/booking_system/presentation/bloc/selected_bookable_space_bloc/selected_bookable_space_bloc.dart';
|
||||
import 'package:syncrow_web/pages/access_management/booking_system/data/services/memory_bookable_space_service.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';
|
||||
@ -24,7 +27,33 @@ class BookingPage extends StatefulWidget {
|
||||
}
|
||||
|
||||
class _BookingPageState extends State<BookingPage> {
|
||||
late final EventController _eventController;
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return MultiBlocProvider(
|
||||
providers: [
|
||||
BlocProvider(create: (_) => SelectedBookableSpaceBloc()),
|
||||
BlocProvider(create: (_) => DateSelectionBloc()),
|
||||
BlocProvider(
|
||||
create: (_) => CalendarEventsBloc(
|
||||
calendarService: MemoryCalendarServiceWithRemoteFallback(
|
||||
remoteService: RemoteCalendarService(HTTPService()),
|
||||
memoryService: MemoryCalendarService(),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
child: _BookingPageContent(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _BookingPageContent extends StatefulWidget {
|
||||
@override
|
||||
State<_BookingPageContent> createState() => _BookingPageContentState();
|
||||
}
|
||||
|
||||
class _BookingPageContentState extends State<_BookingPageContent> {
|
||||
late EventController _eventController;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
@ -38,7 +67,7 @@ class _BookingPageState extends State<BookingPage> {
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
void _dispatchLoadEvents(BuildContext context) {
|
||||
void _loadEvents(BuildContext context) {
|
||||
final selectedRoom =
|
||||
context.read<SelectedBookableSpaceBloc>().state.selectedBookableSpace;
|
||||
final dateState = context.read<DateSelectionBloc>().state;
|
||||
@ -46,9 +75,11 @@ class _BookingPageState extends State<BookingPage> {
|
||||
if (selectedRoom != null) {
|
||||
context.read<CalendarEventsBloc>().add(
|
||||
LoadEvents(
|
||||
spaceId: selectedRoom.uuid,
|
||||
weekStart: dateState.weekStart,
|
||||
weekEnd: dateState.weekStart.add(const Duration(days: 6)),
|
||||
LoadEventsParam(
|
||||
startDate: dateState.weekStart,
|
||||
endDate: dateState.weekStart.add(const Duration(days: 6)),
|
||||
id: selectedRoom.uuid,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
@ -56,182 +87,170 @@ class _BookingPageState extends State<BookingPage> {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return MultiBlocProvider(
|
||||
providers: [
|
||||
BlocProvider(create: (_) => SelectedBookableSpaceBloc()),
|
||||
BlocProvider(create: (_) => DateSelectionBloc()),
|
||||
BlocProvider(
|
||||
create: (_) => CalendarEventsBloc(
|
||||
calendarService:
|
||||
FakeRemoteCalendarService(HTTPService(), useDummy: true),
|
||||
),
|
||||
),
|
||||
],
|
||||
child: Builder(
|
||||
builder: (context) =>
|
||||
BlocListener<CalendarEventsBloc, CalendarEventState>(
|
||||
listenWhen: (prev, curr) => curr is EventsLoaded,
|
||||
return BlocListener<SelectedBookableSpaceBloc, SelectedBookableSpaceState>(
|
||||
listener: (context, state) {
|
||||
if (state.selectedBookableSpace != null) {
|
||||
// Reset events and clear cache when room changes
|
||||
context.read<CalendarEventsBloc>().add(ResetEvents());
|
||||
_loadEvents(context);
|
||||
}
|
||||
},
|
||||
child: BlocListener<DateSelectionBloc, DateSelectionState>(
|
||||
listener: (context, state) {
|
||||
_loadEvents(context);
|
||||
},
|
||||
child: BlocListener<CalendarEventsBloc, CalendarEventState>(
|
||||
listener: (context, state) {
|
||||
if (state is EventsLoaded) {
|
||||
_eventController.removeWhere((_) => true);
|
||||
_eventController.addAll(state.events);
|
||||
}
|
||||
},
|
||||
child: BlocListener<SelectedBookableSpaceBloc,
|
||||
SelectedBookableSpaceState>(
|
||||
listener: (context, state) => _dispatchLoadEvents(context),
|
||||
child: BlocListener<DateSelectionBloc, DateSelectionState>(
|
||||
listener: (context, state) => _dispatchLoadEvents(context),
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Expanded(
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
color: ColorsManager.whiteColors,
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: ColorsManager.blackColor.withOpacity(0.1),
|
||||
offset: const Offset(3, 0),
|
||||
blurRadius: 6,
|
||||
spreadRadius: 0,
|
||||
),
|
||||
],
|
||||
child: 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,
|
||||
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: 5,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(20.0),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
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: () {},
|
||||
),
|
||||
],
|
||||
SvgTextButton(
|
||||
svgAsset: Assets.homeIcon,
|
||||
label: 'Manage Bookable Spaces',
|
||||
onPressed: () {},
|
||||
),
|
||||
BlocBuilder<DateSelectionBloc,
|
||||
DateSelectionState>(
|
||||
builder: (context, state) {
|
||||
final weekStart = state.weekStart;
|
||||
final weekEnd =
|
||||
weekStart.add(const Duration(days: 6));
|
||||
return WeekNavigation(
|
||||
weekStart: weekStart,
|
||||
weekEnd: weekEnd,
|
||||
onPreviousWeek: () {
|
||||
context
|
||||
.read<DateSelectionBloc>()
|
||||
.add(PreviousWeek());
|
||||
},
|
||||
onNextWeek: () {
|
||||
context
|
||||
.read<DateSelectionBloc>()
|
||||
.add(NextWeek());
|
||||
},
|
||||
);
|
||||
},
|
||||
const SizedBox(width: 20),
|
||||
SvgTextButton(
|
||||
svgAsset: Assets.groupIcon,
|
||||
label: 'Manage Users',
|
||||
onPressed: () {},
|
||||
),
|
||||
],
|
||||
),
|
||||
Expanded(
|
||||
child: BlocBuilder<SelectedBookableSpaceBloc,
|
||||
SelectedBookableSpaceState>(
|
||||
builder: (context, roomState) {
|
||||
final selectedRoom =
|
||||
roomState.selectedBookableSpace;
|
||||
return BlocBuilder<DateSelectionBloc,
|
||||
DateSelectionState>(
|
||||
builder: (context, dateState) {
|
||||
return BlocListener<CalendarEventsBloc,
|
||||
CalendarEventState>(
|
||||
listenWhen: (prev, curr) =>
|
||||
curr is EventsLoaded,
|
||||
listener: (context, state) {
|
||||
if (state is EventsLoaded) {
|
||||
_eventController
|
||||
.removeWhere((_) => true);
|
||||
_eventController.addAll(state.events);
|
||||
}
|
||||
},
|
||||
child: WeeklyCalendarPage(
|
||||
startTime: selectedRoom
|
||||
?.bookableConfig.startTime,
|
||||
endTime: selectedRoom
|
||||
?.bookableConfig.endTime,
|
||||
weekStart: dateState.weekStart,
|
||||
selectedDate: dateState.selectedDate,
|
||||
eventController: _eventController,
|
||||
selectedDateFromSideBarCalender: context
|
||||
.watch<DateSelectionBloc>()
|
||||
.state
|
||||
.selectedDateFromSideBarCalender,
|
||||
),
|
||||
BlocBuilder<DateSelectionBloc, DateSelectionState>(
|
||||
builder: (context, state) {
|
||||
final weekStart = state.weekStart;
|
||||
final weekEnd =
|
||||
weekStart.add(const Duration(days: 6));
|
||||
return WeekNavigation(
|
||||
weekStart: weekStart,
|
||||
weekEnd: weekEnd,
|
||||
onPreviousWeek: () {
|
||||
context
|
||||
.read<DateSelectionBloc>()
|
||||
.add(PreviousWeek());
|
||||
},
|
||||
onNextWeek: () {
|
||||
context
|
||||
.read<DateSelectionBloc>()
|
||||
.add(NextWeek());
|
||||
},
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
Expanded(
|
||||
flex: 5,
|
||||
child: BlocBuilder<SelectedBookableSpaceBloc,
|
||||
SelectedBookableSpaceState>(
|
||||
builder: (context, roomState) {
|
||||
final selectedRoom =
|
||||
roomState.selectedBookableSpace;
|
||||
return BlocBuilder<DateSelectionBloc,
|
||||
DateSelectionState>(
|
||||
builder: (context, dateState) {
|
||||
return BlocBuilder<CalendarEventsBloc,
|
||||
CalendarEventState>(
|
||||
builder: (context, eventState) {
|
||||
return WeeklyCalendarPage(
|
||||
key: ValueKey(
|
||||
selectedRoom?.uuid ?? 'no-room'),
|
||||
startTime: selectedRoom
|
||||
?.bookableConfig.startTime,
|
||||
endTime:
|
||||
selectedRoom?.bookableConfig.endTime,
|
||||
weekStart: dateState.weekStart,
|
||||
selectedDate: dateState.selectedDate,
|
||||
eventController: _eventController,
|
||||
selectedDateFromSideBarCalender: context
|
||||
.watch<DateSelectionBloc>()
|
||||
.state
|
||||
.selectedDateFromSideBarCalender,
|
||||
// isLoading: eventState is EventsLoading,
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
|
@ -72,29 +72,33 @@ class __SidebarContentState extends State<_SidebarContent> {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return BlocConsumer<SidebarBloc, SidebarState>(
|
||||
listener: (context, state) {
|
||||
if (state.currentPage == 1 && searchController.text.isNotEmpty) {
|
||||
searchController.clear();
|
||||
}
|
||||
},
|
||||
listener: (context, state) {},
|
||||
builder: (context, state) {
|
||||
return Column(
|
||||
children: [
|
||||
const _SidebarHeader(title: 'Spaces'),
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(top: 10.0, bottom: 10.0),
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
color: ColorsManager.whiteColors,
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: ColorsManager.blackColor.withOpacity(0.1),
|
||||
offset: const Offset(0, 4),
|
||||
blurRadius: 4,
|
||||
spreadRadius: 0,
|
||||
),
|
||||
],
|
||||
),
|
||||
child: _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),
|
||||
offset: const Offset(0, 4),
|
||||
blurRadius: 4,
|
||||
spreadRadius: 0,
|
||||
),
|
||||
@ -147,6 +151,7 @@ class __SidebarContentState extends State<_SidebarContent> {
|
||||
IconButton(
|
||||
icon: const Icon(Icons.close),
|
||||
onPressed: () {
|
||||
searchController.clear();
|
||||
context.read<SidebarBloc>().add(ResetSearch());
|
||||
},
|
||||
),
|
||||
@ -223,7 +228,7 @@ class _SidebarHeader extends StatelessWidget {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.all(16.0),
|
||||
padding: const EdgeInsets.all(10.0),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
|
@ -66,18 +66,28 @@ class _CustomCalendarPageState extends State<CustomCalendarPage> {
|
||||
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);
|
||||
}
|
||||
},
|
||||
return Container(
|
||||
decoration: const BoxDecoration(
|
||||
border: Border(
|
||||
top: BorderSide(
|
||||
color: ColorsManager.textGray,
|
||||
width: 1.0,
|
||||
),
|
||||
),
|
||||
),
|
||||
child: 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);
|
||||
}
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -1,16 +1,15 @@
|
||||
import 'package:calendar_view/calendar_view.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:intl/intl.dart';
|
||||
import 'package:syncrow_web/pages/access_management/booking_system/domain/models/calendar_event_booking.dart';
|
||||
import 'package:syncrow_web/utils/color_manager.dart';
|
||||
|
||||
class EventTileWidget extends StatelessWidget {
|
||||
final List<CalendarEventData<Object?>> events;
|
||||
|
||||
const EventTileWidget({
|
||||
super.key,
|
||||
required this.events,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
@ -18,39 +17,88 @@ class EventTileWidget extends StatelessWidget {
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: events.map((event) {
|
||||
final bool isEventEnded =
|
||||
final booking = event.event is CalendarEventBooking
|
||||
? event.event! as CalendarEventBooking
|
||||
: null;
|
||||
|
||||
final companyName = booking?.user.companyName ?? 'Unknown Company';
|
||||
final startTime = DateFormat('hh:mm a').format(event.startTime!);
|
||||
final endTime = DateFormat('hh:mm a').format(event.endTime!);
|
||||
final isEventEnded =
|
||||
event.endTime != null && event.endTime!.isBefore(DateTime.now());
|
||||
|
||||
final duration = event.endTime!.difference(event.startTime!);
|
||||
final bool isLongEnough = duration.inMinutes >= 31;
|
||||
return Expanded(
|
||||
child: Container(
|
||||
width: double.infinity,
|
||||
padding: const EdgeInsets.all(6),
|
||||
padding: const EdgeInsets.all(5),
|
||||
decoration: BoxDecoration(
|
||||
color: isEventEnded
|
||||
? ColorsManager.lightGrayBorderColor
|
||||
: ColorsManager.blue1.withOpacity(0.25),
|
||||
? ColorsManager.grayColor.withOpacity(0.1)
|
||||
: ColorsManager.blue1.withOpacity(0.1),
|
||||
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,
|
||||
),
|
||||
border: Border(
|
||||
left: BorderSide(
|
||||
color: isEventEnded
|
||||
? ColorsManager.grayColor
|
||||
: ColorsManager.secondaryColor,
|
||||
width: 4,
|
||||
),
|
||||
const SizedBox(height: 2),
|
||||
Text(
|
||||
event.title,
|
||||
style: const TextStyle(
|
||||
fontSize: 12,
|
||||
color: ColorsManager.blackColor,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
child: isLongEnough
|
||||
? Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
'$startTime - $endTime',
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
color: isEventEnded
|
||||
? ColorsManager.grayColor.withOpacity(0.9)
|
||||
: ColorsManager.blackColor,
|
||||
fontWeight: FontWeight.w400,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 2),
|
||||
Text(
|
||||
event.title,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
color: isEventEnded
|
||||
? ColorsManager.grayColor
|
||||
: ColorsManager.blackColor,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 2),
|
||||
Text(
|
||||
companyName,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
color: isEventEnded
|
||||
? ColorsManager.grayColor.withOpacity(0.9)
|
||||
: ColorsManager.blackColor,
|
||||
fontWeight: FontWeight.w400,
|
||||
),
|
||||
),
|
||||
],
|
||||
)
|
||||
: Text(
|
||||
event.title,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
color: isEventEnded
|
||||
? ColorsManager.grayColor
|
||||
: ColorsManager.blackColor,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}).toList(),
|
||||
|
@ -4,6 +4,7 @@ import 'package:syncrow_web/utils/color_manager.dart';
|
||||
|
||||
class SvgTextButton extends StatelessWidget {
|
||||
final String svgAsset;
|
||||
final EdgeInsets? padding;
|
||||
final String label;
|
||||
final VoidCallback onPressed;
|
||||
final Color backgroundColor;
|
||||
@ -12,16 +13,20 @@ class SvgTextButton extends StatelessWidget {
|
||||
final double borderRadius;
|
||||
final List<BoxShadow> boxShadow;
|
||||
final double svgSize;
|
||||
|
||||
final double? fontSize;
|
||||
final FontWeight? fontWeight;
|
||||
const SvgTextButton({
|
||||
super.key,
|
||||
required this.svgAsset,
|
||||
this.fontSize,
|
||||
this.fontWeight,
|
||||
required this.label,
|
||||
required this.onPressed,
|
||||
this.backgroundColor = ColorsManager.circleRolesBackground,
|
||||
this.svgColor = const Color(0xFF496EFF),
|
||||
this.labelColor = Colors.black,
|
||||
this.borderRadius = 10.0,
|
||||
this.padding,
|
||||
this.boxShadow = const [
|
||||
BoxShadow(
|
||||
color: ColorsManager.lightGrayColor,
|
||||
@ -40,7 +45,8 @@ class SvgTextButton extends StatelessWidget {
|
||||
borderRadius: BorderRadius.circular(borderRadius),
|
||||
onTap: onPressed,
|
||||
child: Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 12),
|
||||
padding: padding ??
|
||||
const EdgeInsets.symmetric(horizontal: 24, vertical: 12),
|
||||
decoration: BoxDecoration(
|
||||
color: backgroundColor,
|
||||
borderRadius: BorderRadius.circular(borderRadius),
|
||||
|
@ -21,20 +21,24 @@ class RoomListItem extends StatelessWidget {
|
||||
groupValue: isSelected ? room.uuid : null,
|
||||
visualDensity: const VisualDensity(vertical: -4),
|
||||
onChanged: (value) => onTap(),
|
||||
activeColor: ColorsManager.primaryColor,
|
||||
activeColor: ColorsManager.secondaryColor,
|
||||
title: Text(
|
||||
room.spaceName,
|
||||
maxLines: 2,
|
||||
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
|
||||
color: ColorsManager.lightGrayColor,
|
||||
fontWeight: FontWeight.w700,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
fontSize: 12),
|
||||
),
|
||||
subtitle: Text(
|
||||
room.virtualLocation,
|
||||
maxLines: 2,
|
||||
style: Theme.of(context).textTheme.bodySmall?.copyWith(
|
||||
fontSize: 10,
|
||||
fontWeight: FontWeight.w400,
|
||||
color: ColorsManager.textGray,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
),
|
||||
);
|
||||
|
@ -14,26 +14,33 @@ class WeekDayHeader extends StatelessWidget {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Column(
|
||||
children: [
|
||||
Text(
|
||||
DateFormat('EEE').format(date).toUpperCase(),
|
||||
style: TextStyle(
|
||||
fontWeight: FontWeight.w400,
|
||||
fontSize: 14,
|
||||
color: isSelectedDay ? Colors.blue : Colors.black,
|
||||
return ColoredBox(
|
||||
color: isSelectedDay
|
||||
? ColorsManager.secondaryColor.withOpacity(0.1)
|
||||
: Colors.transparent,
|
||||
child: Column(
|
||||
children: [
|
||||
const SizedBox(
|
||||
height: 10,
|
||||
),
|
||||
),
|
||||
Text(
|
||||
DateFormat('d').format(date),
|
||||
style: TextStyle(
|
||||
fontWeight: FontWeight.w700,
|
||||
fontSize: 20,
|
||||
color:
|
||||
isSelectedDay ? ColorsManager.blue1 : ColorsManager.blackColor,
|
||||
Text(
|
||||
DateFormat('EEE').format(date).toUpperCase(),
|
||||
style: const TextStyle(
|
||||
fontWeight: FontWeight.w400,
|
||||
fontSize: 14,
|
||||
color: ColorsManager.blackColor,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
Text(
|
||||
DateFormat('d').format(date),
|
||||
style: const TextStyle(
|
||||
fontWeight: FontWeight.w700,
|
||||
fontSize: 30,
|
||||
color: ColorsManager.blackColor,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -1,7 +1,6 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:calendar_view/calendar_view.dart';
|
||||
import 'package:syncrow_web/pages/access_management/booking_system/presentation/view/widgets/event_tile_widget.dart';
|
||||
import 'package:syncrow_web/pages/access_management/booking_system/presentation/view/widgets/hatched_column_background.dart';
|
||||
import 'package:syncrow_web/pages/access_management/booking_system/presentation/view/widgets/time_line_widget.dart';
|
||||
import 'package:syncrow_web/pages/access_management/booking_system/presentation/view/widgets/week_day_header.dart';
|
||||
import 'package:syncrow_web/utils/color_manager.dart';
|
||||
@ -23,6 +22,12 @@ class WeeklyCalendarPage extends StatelessWidget {
|
||||
this.endTime,
|
||||
this.selectedDateFromSideBarCalender,
|
||||
});
|
||||
static const double timeLineWidth = 65;
|
||||
static const int totalDays = 7;
|
||||
static const double dayColumnWidth = 220;
|
||||
|
||||
final double calendarContentWidth =
|
||||
timeLineWidth + (totalDays * dayColumnWidth);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
@ -52,154 +57,152 @@ class WeeklyCalendarPage extends StatelessWidget {
|
||||
);
|
||||
}
|
||||
|
||||
final weekDays = _getWeekDays(weekStart);
|
||||
const double timeLineWidth = 90;
|
||||
|
||||
final selectedDayIndex =
|
||||
weekDays.indexWhere((d) => isSameDay(d, selectedDate));
|
||||
final selectedSidebarIndex = selectedDateFromSideBarCalender == null
|
||||
? -1
|
||||
: weekDays
|
||||
.indexWhere((d) => isSameDay(d, selectedDateFromSideBarCalender!));
|
||||
|
||||
const double timeLineWidth = 80;
|
||||
const int totalDays = 7;
|
||||
final DateTime highlightStart = DateTime(2025, 7, 10);
|
||||
final DateTime highlightEnd = DateTime(2025, 7, 19);
|
||||
return LayoutBuilder(
|
||||
builder: (context, constraints) {
|
||||
final double calendarWidth = constraints.maxWidth;
|
||||
final double dayColumnWidth =
|
||||
(calendarWidth - timeLineWidth) / totalDays - 0.1;
|
||||
bool isInRange(DateTime date, DateTime start, DateTime end) {
|
||||
return !date.isBefore(start) && !date.isAfter(end);
|
||||
!date.isBefore(start) && !date.isAfter(end);
|
||||
// remove this line and Check if the date is within the range
|
||||
return false;
|
||||
}
|
||||
|
||||
return Padding(
|
||||
padding: const EdgeInsets.only(left: 25.0, right: 25.0, top: 25),
|
||||
child: Stack(
|
||||
children: [
|
||||
WeekView(
|
||||
weekDetectorBuilder: ({
|
||||
required date,
|
||||
required height,
|
||||
required heightPerMinute,
|
||||
required minuteSlotSize,
|
||||
required width,
|
||||
}) {
|
||||
return isInRange(date, highlightStart, highlightEnd)
|
||||
? HatchedColumnBackground(
|
||||
backgroundColor: ColorsManager.grey800,
|
||||
lineColor: ColorsManager.textGray,
|
||||
opacity: 0.3,
|
||||
stripeSpacing: 12,
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
)
|
||||
: const SizedBox();
|
||||
},
|
||||
pageViewPhysics: const NeverScrollableScrollPhysics(),
|
||||
key: ValueKey(weekStart),
|
||||
controller: eventController,
|
||||
initialDay: weekStart,
|
||||
startHour: startHour - 1,
|
||||
endHour: endHour,
|
||||
heightPerMinute: 1.1,
|
||||
showLiveTimeLineInAllDays: false,
|
||||
showVerticalLines: true,
|
||||
emulateVerticalOffsetBy: -80,
|
||||
startDay: WeekDays.monday,
|
||||
liveTimeIndicatorSettings: const LiveTimeIndicatorSettings(
|
||||
showBullet: false,
|
||||
height: 0,
|
||||
),
|
||||
weekDayBuilder: (date) {
|
||||
return WeekDayHeader(
|
||||
date: date,
|
||||
isSelectedDay: isSameDay(date, selectedDate),
|
||||
);
|
||||
},
|
||||
timeLineBuilder: (date) {
|
||||
return TimeLineWidget(date: date);
|
||||
},
|
||||
timeLineWidth: timeLineWidth,
|
||||
weekPageHeaderBuilder: (start, end) => Container(),
|
||||
weekTitleHeight: 60,
|
||||
weekNumberBuilder: (firstDayOfWeek) => Padding(
|
||||
padding: const EdgeInsets.only(right: 15, bottom: 10),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.end,
|
||||
mainAxisAlignment: MainAxisAlignment.end,
|
||||
children: [
|
||||
Text(
|
||||
firstDayOfWeek.timeZoneName.replaceAll(':00', ''),
|
||||
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
|
||||
fontSize: 12,
|
||||
color: ColorsManager.blackColor,
|
||||
fontWeight: FontWeight.w400,
|
||||
),
|
||||
return SingleChildScrollView(
|
||||
scrollDirection: Axis.horizontal,
|
||||
child: SizedBox(
|
||||
width: calendarContentWidth,
|
||||
child: Padding(
|
||||
padding:
|
||||
const EdgeInsets.only(left: 25.0, right: 25.0, top: 25),
|
||||
child: Stack(
|
||||
children: [
|
||||
Container(
|
||||
child: WeekView(
|
||||
minuteSlotSize: MinuteSlotSize.minutes15,
|
||||
weekDetectorBuilder: ({
|
||||
required date,
|
||||
required height,
|
||||
required heightPerMinute,
|
||||
required minuteSlotSize,
|
||||
required width,
|
||||
}) {
|
||||
final isSelected = isSameDay(date, selectedDate);
|
||||
final isSidebarSelected =
|
||||
selectedDateFromSideBarCalender != null &&
|
||||
isSameDay(
|
||||
date, selectedDateFromSideBarCalender!);
|
||||
if (isSidebarSelected && !isSelected) {
|
||||
return Container(
|
||||
height: height,
|
||||
width: width,
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.orange.withOpacity(0.13),
|
||||
),
|
||||
);
|
||||
} else if (isSelected) {
|
||||
return Container(
|
||||
height: height,
|
||||
width: width,
|
||||
decoration: BoxDecoration(
|
||||
color:
|
||||
ColorsManager.spaceColor.withOpacity(0.07),
|
||||
),
|
||||
);
|
||||
}
|
||||
return const SizedBox.shrink();
|
||||
},
|
||||
|
||||
// weekDetectorBuilder: ({
|
||||
// required date,
|
||||
// required height,
|
||||
// required heightPerMinute,
|
||||
// required minuteSlotSize,
|
||||
// required width,
|
||||
// }) {
|
||||
// return isInRange(date, highlightStart, highlightEnd)
|
||||
// ? HatchedColumnBackground(
|
||||
// backgroundColor: ColorsManager.grey800,
|
||||
// lineColor: ColorsManager.textGray,
|
||||
// opacity: 0.3,
|
||||
// stripeSpacing: 12,
|
||||
// borderRadius: BorderRadius.circular(8),
|
||||
// )
|
||||
// : const SizedBox();
|
||||
// },
|
||||
pageViewPhysics: const NeverScrollableScrollPhysics(),
|
||||
key: ValueKey(weekStart),
|
||||
controller: eventController,
|
||||
initialDay: weekStart,
|
||||
startHour: startHour - 1,
|
||||
endHour: endHour,
|
||||
heightPerMinute: 1.7,
|
||||
showLiveTimeLineInAllDays: false,
|
||||
showVerticalLines: true,
|
||||
emulateVerticalOffsetBy: -95,
|
||||
startDay: WeekDays.monday,
|
||||
liveTimeIndicatorSettings:
|
||||
const LiveTimeIndicatorSettings(
|
||||
showBullet: false,
|
||||
height: 0,
|
||||
),
|
||||
weekDayBuilder: (date) {
|
||||
return WeekDayHeader(
|
||||
date: date,
|
||||
isSelectedDay: isSameDay(date, selectedDate),
|
||||
);
|
||||
},
|
||||
timeLineBuilder: (date) {
|
||||
return TimeLineWidget(date: date);
|
||||
},
|
||||
timeLineWidth: timeLineWidth,
|
||||
weekPageHeaderBuilder: (start, end) => Container(),
|
||||
weekTitleHeight: 90,
|
||||
weekNumberBuilder: (firstDayOfWeek) => Padding(
|
||||
padding: const EdgeInsets.only(right: 15, bottom: 10),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.end,
|
||||
mainAxisAlignment: MainAxisAlignment.end,
|
||||
children: [
|
||||
Text(
|
||||
firstDayOfWeek.timeZoneName
|
||||
.replaceAll(':00', ''),
|
||||
style: Theme.of(context)
|
||||
.textTheme
|
||||
.bodyMedium
|
||||
?.copyWith(
|
||||
fontSize: 12,
|
||||
color: ColorsManager.blackColor,
|
||||
fontWeight: FontWeight.w400,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
eventTileBuilder: (date, events, boundary, start, end) {
|
||||
return EventTileWidget(
|
||||
events: events,
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
eventTileBuilder: (date, events, boundary, start, end) {
|
||||
return EventTileWidget(
|
||||
events: events,
|
||||
);
|
||||
},
|
||||
),
|
||||
if (selectedDayIndex >= 0)
|
||||
Positioned(
|
||||
left: (timeLineWidth + 3) +
|
||||
(dayColumnWidth - 8) * (selectedDayIndex - 0.01),
|
||||
top: 0,
|
||||
bottom: 0,
|
||||
width: dayColumnWidth,
|
||||
child: IgnorePointer(
|
||||
child: Container(
|
||||
margin: const EdgeInsets.symmetric(
|
||||
vertical: 0, horizontal: 4),
|
||||
color: ColorsManager.spaceColor.withOpacity(0.07),
|
||||
),
|
||||
),
|
||||
),
|
||||
if (selectedSidebarIndex >= 0 &&
|
||||
selectedSidebarIndex != selectedDayIndex)
|
||||
Positioned(
|
||||
left: (timeLineWidth + 3) +
|
||||
(dayColumnWidth - 8) * (selectedSidebarIndex - 0.01),
|
||||
top: 0,
|
||||
bottom: 0,
|
||||
width: dayColumnWidth,
|
||||
child: IgnorePointer(
|
||||
child: Container(
|
||||
margin: const EdgeInsets.symmetric(
|
||||
vertical: 0, horizontal: 4),
|
||||
color: Colors.orange.withOpacity(0.14),
|
||||
Positioned(
|
||||
right: 0,
|
||||
top: 50,
|
||||
bottom: 0,
|
||||
child: IgnorePointer(
|
||||
child: Container(
|
||||
width: 1,
|
||||
color: Theme.of(context).scaffoldBackgroundColor,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
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) {
|
||||
|
@ -0,0 +1,57 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:syncrow_web/pages/access_management/booking_system/presentation/view/widgets/icon_text_button.dart';
|
||||
|
||||
import 'package:syncrow_web/utils/constants/assets.dart';
|
||||
|
||||
class BookingPage extends StatelessWidget {
|
||||
final PageController pageController;
|
||||
const BookingPage({
|
||||
super.key,
|
||||
required this.pageController,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return 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: () {
|
||||
pageController.jumpToPage(2);
|
||||
}),
|
||||
const SizedBox(width: 20),
|
||||
SvgTextButton(
|
||||
svgAsset: Assets.groupIcon,
|
||||
label: 'Manage Users',
|
||||
onPressed: () {})
|
||||
],
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
))
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
@ -0,0 +1,32 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:syncrow_web/pages/access_management/manage_bookable_spaces/domain/models/bookable_space_model.dart';
|
||||
import 'package:syncrow_web/pages/access_management/manage_bookable_spaces/domain/params/non_bookable_spaces_params.dart';
|
||||
import 'package:syncrow_web/pages/access_management/manage_bookable_spaces/domain/service/non_bookable_spaces_service.dart';
|
||||
import 'package:syncrow_web/pages/space_management_v2/main_module/shared/models/paginated_data_model.dart';
|
||||
|
||||
class NonBookableSpacesDebouncerDecoratorService
|
||||
implements NonBookableSpacesService {
|
||||
final NonBookableSpacesService _delegate;
|
||||
Timer? _debounce;
|
||||
|
||||
NonBookableSpacesDebouncerDecoratorService(this._delegate);
|
||||
|
||||
@override
|
||||
Future<PaginatedDataModel<BookableSpacemodel>> load(
|
||||
NonBookableSpacesParams params) {
|
||||
final completer = Completer<PaginatedDataModel<BookableSpacemodel>>();
|
||||
|
||||
_debounce?.cancel();
|
||||
_debounce = Timer(const Duration(milliseconds: 500), () async {
|
||||
try {
|
||||
final result = await _delegate.load(params);
|
||||
completer.complete(result);
|
||||
} catch (e) {
|
||||
completer.completeError(e);
|
||||
}
|
||||
});
|
||||
|
||||
return completer.future;
|
||||
}
|
||||
}
|
@ -0,0 +1,47 @@
|
||||
import 'package:dio/dio.dart';
|
||||
import 'package:syncrow_web/pages/access_management/manage_bookable_spaces/domain/models/bookable_space_model.dart';
|
||||
import 'package:syncrow_web/pages/access_management/manage_bookable_spaces/domain/params/bookable_spaces_params.dart';
|
||||
import 'package:syncrow_web/pages/access_management/manage_bookable_spaces/domain/service/bookable_spaces_service.dart';
|
||||
import 'package:syncrow_web/pages/space_management_v2/main_module/shared/models/paginated_data_model.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 BookableSpacesService {
|
||||
final HTTPService _httpService;
|
||||
RemoteBookableSpacesService(this._httpService);
|
||||
static const _defaultErrorMessage = 'Failed to load Bookable Spaces';
|
||||
@override
|
||||
Future<PaginatedDataModel<BookableSpacemodel>> load(
|
||||
BookableSpacesParam param) async {
|
||||
try {
|
||||
final response = await _httpService.get(
|
||||
path: ApiEndpoints.bookableSpaces,
|
||||
queryParameters: {
|
||||
'configured': true,
|
||||
'page': param.currentPage,
|
||||
},
|
||||
expectedResponseModel: (json) {
|
||||
final result = json as Map<String, dynamic>;
|
||||
return PaginatedDataModel.fromJson(
|
||||
result,
|
||||
BookableSpacemodel.fromJsonList,
|
||||
);
|
||||
},
|
||||
);
|
||||
return response;
|
||||
} on DioException catch (e) {
|
||||
final message = e.response?.data as Map<String, dynamic>?;
|
||||
final error = message?['error'] as Map<String, dynamic>?;
|
||||
final errorMessage = error?['error'] as String? ?? '';
|
||||
final formattedErrorMessage = [
|
||||
_defaultErrorMessage,
|
||||
errorMessage,
|
||||
].join(': ');
|
||||
throw APIException(formattedErrorMessage);
|
||||
} catch (e) {
|
||||
final formattedErrorMessage = [_defaultErrorMessage, '$e'].join(': ');
|
||||
throw APIException(formattedErrorMessage);
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,53 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:dio/dio.dart';
|
||||
import 'package:syncrow_web/pages/access_management/manage_bookable_spaces/domain/models/bookable_space_model.dart';
|
||||
import 'package:syncrow_web/pages/access_management/manage_bookable_spaces/domain/params/non_bookable_spaces_params.dart';
|
||||
import 'package:syncrow_web/pages/access_management/manage_bookable_spaces/domain/service/non_bookable_spaces_service.dart';
|
||||
import 'package:syncrow_web/pages/space_management_v2/main_module/shared/models/paginated_data_model.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 RemoteNonBookableSpaces implements NonBookableSpacesService {
|
||||
final HTTPService _httpService;
|
||||
|
||||
RemoteNonBookableSpaces(this._httpService);
|
||||
|
||||
static const _defaultErrorMessage = 'Failed to load Spaces';
|
||||
|
||||
@override
|
||||
Future<PaginatedDataModel<BookableSpacemodel>> load(
|
||||
NonBookableSpacesParams params) async {
|
||||
try {
|
||||
final response = await _httpService.get(
|
||||
path: ApiEndpoints.bookableSpaces,
|
||||
queryParameters: {
|
||||
'configured': false,
|
||||
'page': params.currentPage,
|
||||
'search': params.searchedWords,
|
||||
},
|
||||
expectedResponseModel: (json) {
|
||||
final result = json as Map<String, dynamic>;
|
||||
return PaginatedDataModel.fromJson(
|
||||
result,
|
||||
BookableSpacemodel.fromJsonList,
|
||||
);
|
||||
},
|
||||
);
|
||||
return response;
|
||||
} on DioException catch (e) {
|
||||
final message = e.response?.data as Map<String, dynamic>?;
|
||||
final error = message?['error'] as Map<String, dynamic>?;
|
||||
final errorMessage = error?['error'] as String? ?? '';
|
||||
final formattedErrorMessage = [
|
||||
_defaultErrorMessage,
|
||||
errorMessage,
|
||||
].join(': ');
|
||||
throw APIException(formattedErrorMessage);
|
||||
} catch (e) {
|
||||
final formattedErrorMessage = [_defaultErrorMessage, '$e'].join(': ');
|
||||
throw APIException(formattedErrorMessage);
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,35 @@
|
||||
import 'package:dio/dio.dart';
|
||||
import 'package:syncrow_web/pages/access_management/manage_bookable_spaces/domain/params/send_bookable_spaces_to_api_params.dart';
|
||||
import 'package:syncrow_web/pages/access_management/manage_bookable_spaces/domain/service/send_bookable_spaces_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 RemoteSendBookableSpaces implements SendBookableSpacesService {
|
||||
final HTTPService _httpService;
|
||||
RemoteSendBookableSpaces(this._httpService);
|
||||
static const _defaultErrorMessage = 'Failed to load Spaces';
|
||||
@override
|
||||
Future<void> sendBookableSpacesToApi(
|
||||
SendBookableSpacesToApiParams params) async {
|
||||
try {
|
||||
await _httpService.post(
|
||||
path: ApiEndpoints.bookableSpaces,
|
||||
body: params.toJson(),
|
||||
expectedResponseModel: (p0) {},
|
||||
);
|
||||
} on DioException catch (e) {
|
||||
final message = e.response?.data as Map<String, dynamic>?;
|
||||
final error = message?['error'] as Map<String, dynamic>?;
|
||||
final errorMessage = error?['error'] as String? ?? '';
|
||||
final formattedErrorMessage = [
|
||||
_defaultErrorMessage,
|
||||
errorMessage,
|
||||
].join(': ');
|
||||
throw APIException(formattedErrorMessage);
|
||||
} catch (e) {
|
||||
final formattedErrorMessage = [_defaultErrorMessage, '$e'].join(': ');
|
||||
throw APIException(formattedErrorMessage);
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,40 @@
|
||||
import 'package:dio/dio.dart';
|
||||
import 'package:syncrow_web/pages/access_management/manage_bookable_spaces/domain/models/bookable_space_config.dart';
|
||||
import 'package:syncrow_web/pages/access_management/manage_bookable_spaces/domain/params/update_bookable_space_param.dart';
|
||||
import 'package:syncrow_web/pages/access_management/manage_bookable_spaces/domain/service/update_bookable_space_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 RemoteUpdateBookableSpaceService implements UpdateBookableSpaceService {
|
||||
final HTTPService _httpService;
|
||||
RemoteUpdateBookableSpaceService(this._httpService);
|
||||
static const _defaultErrorMessage = 'Failed to load Bookable Spaces';
|
||||
@override
|
||||
Future<BookableSpaceConfig> update(
|
||||
UpdateBookableSpaceParam updateParam) async {
|
||||
try {
|
||||
final response = await _httpService.put(
|
||||
path: '${ApiEndpoints.bookableSpaces}/${updateParam.spaceUuid}',
|
||||
body: updateParam.toJson(),
|
||||
expectedResponseModel: (json) {
|
||||
return BookableSpaceConfig.fromJson(
|
||||
json['data'] as Map<String, dynamic>);
|
||||
},
|
||||
);
|
||||
return response;
|
||||
} on DioException catch (e) {
|
||||
final message = e.response?.data as Map<String, dynamic>?;
|
||||
final error = message?['error'] as Map<String, dynamic>?;
|
||||
final errorMessage = error?['error'] as String? ?? '';
|
||||
final formattedErrorMessage = [
|
||||
_defaultErrorMessage,
|
||||
errorMessage,
|
||||
].join(': ');
|
||||
throw APIException(formattedErrorMessage);
|
||||
} catch (e) {
|
||||
final formattedErrorMessage = [_defaultErrorMessage, '$e'].join(': ');
|
||||
throw APIException(formattedErrorMessage);
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,63 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class BookableSpaceConfig {
|
||||
final String configUuid;
|
||||
final List<String> bookableDays;
|
||||
final TimeOfDay? bookingStartTime;
|
||||
final TimeOfDay? bookingEndTime;
|
||||
final int cost;
|
||||
final bool availability;
|
||||
BookableSpaceConfig({
|
||||
required this.configUuid,
|
||||
required this.availability,
|
||||
required this.bookableDays,
|
||||
this.bookingEndTime,
|
||||
this.bookingStartTime,
|
||||
required this.cost,
|
||||
});
|
||||
factory BookableSpaceConfig.zero() => BookableSpaceConfig(
|
||||
configUuid: '',
|
||||
bookableDays: [],
|
||||
availability: false,
|
||||
cost: -1,
|
||||
);
|
||||
factory BookableSpaceConfig.fromJson(Map<String, dynamic> json) =>
|
||||
BookableSpaceConfig(
|
||||
configUuid: json['uuid'] as String,
|
||||
bookableDays: (json['daysAvailable'] as List).cast<String>(),
|
||||
availability: (json['active'] as bool?) ?? false,
|
||||
bookingStartTime: parseTimeOfDay(json['startTime'] as String),
|
||||
bookingEndTime: parseTimeOfDay(json['endTime'] as String),
|
||||
cost: json['points'] as int,
|
||||
);
|
||||
|
||||
static TimeOfDay parseTimeOfDay(String timeString) {
|
||||
final parts = timeString.split(':');
|
||||
final hour = int.parse(parts[0]);
|
||||
final minute = int.parse(parts[1]);
|
||||
return TimeOfDay(hour: hour, minute: minute);
|
||||
}
|
||||
|
||||
bool get isValid =>
|
||||
bookableDays.isNotEmpty &&
|
||||
cost >= 0 &&
|
||||
bookingStartTime != null &&
|
||||
bookingEndTime != null;
|
||||
|
||||
BookableSpaceConfig copyWith({
|
||||
List<String>? bookableDays,
|
||||
TimeOfDay? bookingStartTime,
|
||||
TimeOfDay? bookingEndTime,
|
||||
int? cost,
|
||||
bool? availability,
|
||||
}) {
|
||||
return BookableSpaceConfig(
|
||||
configUuid: configUuid,
|
||||
availability: availability ?? this.availability,
|
||||
bookableDays: bookableDays ?? this.bookableDays,
|
||||
cost: cost ?? this.cost,
|
||||
bookingEndTime: bookingEndTime ?? this.bookingEndTime,
|
||||
bookingStartTime: bookingStartTime ?? this.bookingStartTime,
|
||||
);
|
||||
}
|
||||
}
|
@ -0,0 +1,58 @@
|
||||
import 'package:syncrow_web/pages/access_management/manage_bookable_spaces/domain/models/bookable_space_config.dart';
|
||||
|
||||
class BookableSpacemodel {
|
||||
final String spaceUuid;
|
||||
final String spaceName;
|
||||
final BookableSpaceConfig? spaceConfig;
|
||||
final String spaceVirtualAddress;
|
||||
|
||||
BookableSpacemodel({
|
||||
required this.spaceUuid,
|
||||
required this.spaceName,
|
||||
this.spaceConfig,
|
||||
required this.spaceVirtualAddress,
|
||||
});
|
||||
factory BookableSpacemodel.zero() => BookableSpacemodel(
|
||||
spaceUuid: '',
|
||||
spaceName: '',
|
||||
spaceVirtualAddress: '',
|
||||
);
|
||||
factory BookableSpacemodel.fromJson(Map<String, dynamic> json) =>
|
||||
BookableSpacemodel(
|
||||
spaceUuid: json['uuid'] as String,
|
||||
spaceName: json['spaceName'] as String,
|
||||
spaceConfig: json['bookableConfig'] == null
|
||||
? BookableSpaceConfig.zero()
|
||||
: BookableSpaceConfig.fromJson(
|
||||
json['bookableConfig'] as Map<String, dynamic>),
|
||||
spaceVirtualAddress: json['virtualLocation'] as String,
|
||||
);
|
||||
|
||||
static List<BookableSpacemodel> fromJsonList(List<dynamic> jsonList) =>
|
||||
jsonList
|
||||
.map(
|
||||
(e) => BookableSpacemodel.fromJson(e as Map<String, dynamic>),
|
||||
)
|
||||
.toList();
|
||||
|
||||
bool get isValid =>
|
||||
spaceUuid.isNotEmpty &&
|
||||
spaceName.isNotEmpty &&
|
||||
spaceVirtualAddress.isNotEmpty &&
|
||||
spaceConfig != null &&
|
||||
spaceConfig!.isValid;
|
||||
|
||||
BookableSpacemodel copyWith({
|
||||
String? spaceUuid,
|
||||
String? spaceName,
|
||||
BookableSpaceConfig? spaceConfig,
|
||||
String? spaceVirtualAddress,
|
||||
}) {
|
||||
return BookableSpacemodel(
|
||||
spaceUuid: spaceUuid ?? this.spaceUuid,
|
||||
spaceName: spaceName ?? this.spaceName,
|
||||
spaceConfig: spaceConfig ?? this.spaceConfig,
|
||||
spaceVirtualAddress: spaceVirtualAddress ?? this.spaceVirtualAddress,
|
||||
);
|
||||
}
|
||||
}
|
@ -0,0 +1,10 @@
|
||||
class BookableSpacesParam {
|
||||
final int currentPage;
|
||||
BookableSpacesParam({
|
||||
required this.currentPage,
|
||||
});
|
||||
|
||||
Map<String, dynamic> toJson() => {
|
||||
'page': currentPage,
|
||||
};
|
||||
}
|
@ -0,0 +1,12 @@
|
||||
class NonBookableSpacesParams {
|
||||
final int currentPage;
|
||||
final String? searchedWords;
|
||||
NonBookableSpacesParams({
|
||||
required this.currentPage,
|
||||
this.searchedWords,
|
||||
});
|
||||
|
||||
Map<String, dynamic> toJson() => {
|
||||
'page': currentPage,
|
||||
};
|
||||
}
|
@ -0,0 +1,41 @@
|
||||
import 'package:syncrow_web/pages/access_management/manage_bookable_spaces/domain/models/bookable_space_model.dart';
|
||||
import 'package:syncrow_web/utils/string_utils.dart';
|
||||
|
||||
class SendBookableSpacesToApiParams {
|
||||
final List<String> spaceUuids;
|
||||
final List<String> daysAvailable;
|
||||
final String startTime;
|
||||
final String endTime;
|
||||
final int points;
|
||||
SendBookableSpacesToApiParams({
|
||||
required this.spaceUuids,
|
||||
required this.daysAvailable,
|
||||
required this.startTime,
|
||||
required this.endTime,
|
||||
required this.points,
|
||||
});
|
||||
|
||||
static SendBookableSpacesToApiParams fromBookableSpacesModel(
|
||||
List<BookableSpacemodel> bookableSpaces) {
|
||||
return SendBookableSpacesToApiParams(
|
||||
spaceUuids: bookableSpaces.map((space) => space.spaceUuid).toList(),
|
||||
daysAvailable: bookableSpaces
|
||||
.expand((space) => space.spaceConfig!.bookableDays)
|
||||
.toSet()
|
||||
.toList(),
|
||||
startTime: formatTimeOfDayTo24HourString(
|
||||
bookableSpaces.first.spaceConfig!.bookingStartTime!),
|
||||
endTime: formatTimeOfDayTo24HourString(
|
||||
bookableSpaces.first.spaceConfig!.bookingEndTime!),
|
||||
points: bookableSpaces.first.spaceConfig!.cost,
|
||||
);
|
||||
}
|
||||
|
||||
Map<String, dynamic> toJson() => {
|
||||
'spaceUuids': spaceUuids,
|
||||
'daysAvailable': daysAvailable,
|
||||
'startTime': startTime,
|
||||
'endTime': endTime,
|
||||
'points': points
|
||||
};
|
||||
}
|
@ -0,0 +1,40 @@
|
||||
import 'package:syncrow_web/pages/access_management/manage_bookable_spaces/domain/models/bookable_space_model.dart';
|
||||
import 'package:syncrow_web/utils/string_utils.dart';
|
||||
|
||||
class UpdateBookableSpaceParam {
|
||||
final String spaceUuid;
|
||||
final List<String>? bookableDays;
|
||||
final String? bookingStartTime;
|
||||
final String? bookingEndTime;
|
||||
final int? cost;
|
||||
final bool? availability;
|
||||
UpdateBookableSpaceParam({
|
||||
required this.spaceUuid,
|
||||
this.bookingStartTime,
|
||||
this.bookingEndTime,
|
||||
this.bookableDays,
|
||||
this.availability,
|
||||
this.cost,
|
||||
});
|
||||
factory UpdateBookableSpaceParam.fromBookableModel(
|
||||
BookableSpacemodel bookableSpace) {
|
||||
return UpdateBookableSpaceParam(
|
||||
spaceUuid: bookableSpace.spaceUuid,
|
||||
availability: bookableSpace.spaceConfig!.availability,
|
||||
bookableDays: bookableSpace.spaceConfig!.bookableDays,
|
||||
cost: bookableSpace.spaceConfig!.cost,
|
||||
bookingStartTime: formatTimeOfDayTo24HourString(
|
||||
bookableSpace.spaceConfig!.bookingStartTime!),
|
||||
bookingEndTime: formatTimeOfDayTo24HourString(
|
||||
bookableSpace.spaceConfig!.bookingEndTime!),
|
||||
);
|
||||
}
|
||||
|
||||
Map<String, dynamic> toJson() => {
|
||||
if (bookableDays != null) 'daysAvailable': bookableDays,
|
||||
if (bookingStartTime != null) 'startTime': bookingStartTime,
|
||||
if (bookingEndTime != null) 'endTime': bookingEndTime,
|
||||
if (cost != null) 'points': cost,
|
||||
if (availability != null) 'active': availability,
|
||||
};
|
||||
}
|
@ -0,0 +1,8 @@
|
||||
import 'package:syncrow_web/pages/access_management/manage_bookable_spaces/domain/models/bookable_space_model.dart';
|
||||
import 'package:syncrow_web/pages/access_management/manage_bookable_spaces/domain/params/bookable_spaces_params.dart';
|
||||
import 'package:syncrow_web/pages/space_management_v2/main_module/shared/models/paginated_data_model.dart';
|
||||
|
||||
abstract class BookableSpacesService {
|
||||
Future<PaginatedDataModel<BookableSpacemodel>> load(
|
||||
BookableSpacesParam param);
|
||||
}
|
@ -0,0 +1,8 @@
|
||||
import 'package:syncrow_web/pages/access_management/manage_bookable_spaces/domain/models/bookable_space_model.dart';
|
||||
import 'package:syncrow_web/pages/access_management/manage_bookable_spaces/domain/params/non_bookable_spaces_params.dart';
|
||||
import 'package:syncrow_web/pages/space_management_v2/main_module/shared/models/paginated_data_model.dart';
|
||||
|
||||
abstract class NonBookableSpacesService {
|
||||
Future<PaginatedDataModel<BookableSpacemodel>> load(
|
||||
NonBookableSpacesParams params);
|
||||
}
|
@ -0,0 +1,5 @@
|
||||
import 'package:syncrow_web/pages/access_management/manage_bookable_spaces/domain/params/send_bookable_spaces_to_api_params.dart';
|
||||
|
||||
abstract class SendBookableSpacesService{
|
||||
Future<void> sendBookableSpacesToApi(SendBookableSpacesToApiParams params);
|
||||
}
|
@ -0,0 +1,6 @@
|
||||
import 'package:syncrow_web/pages/access_management/manage_bookable_spaces/domain/models/bookable_space_config.dart';
|
||||
import 'package:syncrow_web/pages/access_management/manage_bookable_spaces/domain/params/update_bookable_space_param.dart';
|
||||
|
||||
abstract class UpdateBookableSpaceService {
|
||||
Future<BookableSpaceConfig> update(UpdateBookableSpaceParam updateParam);
|
||||
}
|
@ -0,0 +1,66 @@
|
||||
import 'package:bloc/bloc.dart';
|
||||
import 'package:equatable/equatable.dart';
|
||||
import 'package:syncrow_web/pages/access_management/manage_bookable_spaces/domain/models/bookable_space_config.dart';
|
||||
import 'package:syncrow_web/pages/access_management/manage_bookable_spaces/domain/models/bookable_space_model.dart';
|
||||
import 'package:syncrow_web/pages/access_management/manage_bookable_spaces/domain/params/bookable_spaces_params.dart';
|
||||
import 'package:syncrow_web/pages/access_management/manage_bookable_spaces/domain/service/bookable_spaces_service.dart';
|
||||
import 'package:syncrow_web/pages/space_management_v2/main_module/shared/models/paginated_data_model.dart';
|
||||
import 'package:syncrow_web/services/api/api_exception.dart';
|
||||
|
||||
part 'bookable_spaces_event.dart';
|
||||
part 'bookable_spaces_state.dart';
|
||||
|
||||
class BookableSpacesBloc
|
||||
extends Bloc<BookableSpacesEvent, BookableSpacesState> {
|
||||
final BookableSpacesService bookableSpacesService;
|
||||
BookableSpacesBloc(this.bookableSpacesService)
|
||||
: super(BookableSpacesInitial()) {
|
||||
on<LoadBookableSpacesEvent>(_onLoadBookableSpaces);
|
||||
on<InsertUpdatedSpaceEvent>(_onInsertUpdatedSpaceEven);
|
||||
}
|
||||
|
||||
Future<void> _onLoadBookableSpaces(
|
||||
LoadBookableSpacesEvent event, Emitter<BookableSpacesState> emit) async {
|
||||
emit(BookableSpacesLoading());
|
||||
try {
|
||||
final bookableSpaces = await bookableSpacesService.load(event.param);
|
||||
emit(BookableSpacesLoaded(bookableSpacesList: bookableSpaces));
|
||||
} on APIException catch (e) {
|
||||
emit(BookableSpacesError(error: e.message));
|
||||
} catch (e) {
|
||||
emit(
|
||||
BookableSpacesError(error: e.toString()),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
void _onInsertUpdatedSpaceEven(
|
||||
InsertUpdatedSpaceEvent event, Emitter<BookableSpacesState> emit) {
|
||||
emit(InsertingUpdatedSpaceState());
|
||||
|
||||
if (event.bookableSpace.spaceConfig!.configUuid ==
|
||||
event.updatedBookableSpaceConfig.configUuid) {
|
||||
final index = event.bookableSpaces.data.indexWhere(
|
||||
(element) => element.spaceUuid == event.bookableSpace.spaceUuid,
|
||||
);
|
||||
|
||||
if (index != -1) {
|
||||
final original = event.bookableSpaces.data[index];
|
||||
|
||||
final updatedConfig = original.spaceConfig!.copyWith(
|
||||
availability: event.updatedBookableSpaceConfig.availability,
|
||||
bookableDays: event.updatedBookableSpaceConfig.bookableDays,
|
||||
bookingEndTime: event.updatedBookableSpaceConfig.bookingEndTime,
|
||||
bookingStartTime: event.updatedBookableSpaceConfig.bookingStartTime,
|
||||
cost: event.updatedBookableSpaceConfig.cost,
|
||||
);
|
||||
|
||||
final updatedSpace = original.copyWith(spaceConfig: updatedConfig);
|
||||
|
||||
event.bookableSpaces.data[index] = updatedSpace;
|
||||
}
|
||||
}
|
||||
|
||||
emit(BookableSpacesLoaded(bookableSpacesList: event.bookableSpaces));
|
||||
}
|
||||
}
|
@ -0,0 +1,24 @@
|
||||
part of 'bookable_spaces_bloc.dart';
|
||||
|
||||
sealed class BookableSpacesEvent extends Equatable {
|
||||
const BookableSpacesEvent();
|
||||
|
||||
@override
|
||||
List<Object> get props => [];
|
||||
}
|
||||
|
||||
class LoadBookableSpacesEvent extends BookableSpacesEvent {
|
||||
final BookableSpacesParam param;
|
||||
const LoadBookableSpacesEvent(this.param);
|
||||
}
|
||||
|
||||
class InsertUpdatedSpaceEvent extends BookableSpacesEvent {
|
||||
final PaginatedDataModel<BookableSpacemodel> bookableSpaces;
|
||||
final BookableSpacemodel bookableSpace;
|
||||
final BookableSpaceConfig updatedBookableSpaceConfig;
|
||||
const InsertUpdatedSpaceEvent({
|
||||
required this.bookableSpaces,
|
||||
required this.bookableSpace,
|
||||
required this.updatedBookableSpaceConfig,
|
||||
});
|
||||
}
|
@ -0,0 +1,28 @@
|
||||
part of 'bookable_spaces_bloc.dart';
|
||||
|
||||
sealed class BookableSpacesState extends Equatable {
|
||||
const BookableSpacesState();
|
||||
|
||||
@override
|
||||
List<Object> get props => [];
|
||||
}
|
||||
|
||||
final class BookableSpacesInitial extends BookableSpacesState {}
|
||||
|
||||
final class BookableSpacesLoading extends BookableSpacesState {}
|
||||
|
||||
final class BookableSpacesLoaded extends BookableSpacesState {
|
||||
final PaginatedDataModel<BookableSpacemodel> bookableSpacesList;
|
||||
const BookableSpacesLoaded({
|
||||
required this.bookableSpacesList,
|
||||
});
|
||||
}
|
||||
|
||||
final class BookableSpacesError extends BookableSpacesState {
|
||||
final String error;
|
||||
const BookableSpacesError({
|
||||
required this.error,
|
||||
});
|
||||
}
|
||||
|
||||
class InsertingUpdatedSpaceState extends BookableSpacesState {}
|
@ -0,0 +1,65 @@
|
||||
import 'package:bloc/bloc.dart';
|
||||
import 'package:equatable/equatable.dart';
|
||||
import 'package:syncrow_web/pages/access_management/manage_bookable_spaces/domain/models/bookable_space_model.dart';
|
||||
import 'package:syncrow_web/pages/access_management/manage_bookable_spaces/domain/params/non_bookable_spaces_params.dart';
|
||||
import 'package:syncrow_web/pages/access_management/manage_bookable_spaces/domain/service/non_bookable_spaces_service.dart';
|
||||
import 'package:syncrow_web/pages/space_management_v2/main_module/shared/models/paginated_data_model.dart';
|
||||
|
||||
part 'non_bookaable_spaces_event.dart';
|
||||
part 'non_bookaable_spaces_state.dart';
|
||||
|
||||
class NonBookableSpacesBloc
|
||||
extends Bloc<NonBookableSpacesEvent, NonBookableSpacesState> {
|
||||
NonBookableSpacesService nonBookableSpacesService;
|
||||
|
||||
NonBookableSpacesBloc(this.nonBookableSpacesService)
|
||||
: super(NonBookableSpacesInitial()) {
|
||||
on<CallInitStateEvent>(_onCallInitStateEvent);
|
||||
on<LoadUnBookableSpacesEvent>(_onLoadUnBookableSpacesEvent);
|
||||
}
|
||||
|
||||
void _onCallInitStateEvent(
|
||||
CallInitStateEvent event, Emitter<NonBookableSpacesState> emit) {
|
||||
emit(NonBookableSpacesInitial());
|
||||
}
|
||||
|
||||
Future<void> _onLoadUnBookableSpacesEvent(LoadUnBookableSpacesEvent event,
|
||||
Emitter<NonBookableSpacesState> emit) async {
|
||||
if (state is NonBookableSpacesLoaded) {
|
||||
final currState = state as NonBookableSpacesLoaded;
|
||||
try {
|
||||
emit(NonBookableSpacesLoading(
|
||||
lastNonBookableSpaces: currState.nonBookableSpaces));
|
||||
|
||||
final nonBookableSpacesList = await nonBookableSpacesService.load(
|
||||
event.nonBookableSpacesParams,
|
||||
);
|
||||
nonBookableSpacesList.data.addAll(currState.nonBookableSpaces.data);
|
||||
|
||||
emit(
|
||||
NonBookableSpacesLoaded(
|
||||
nonBookableSpaces: nonBookableSpacesList,
|
||||
),
|
||||
);
|
||||
} catch (e) {
|
||||
emit(
|
||||
NonBookableSpacesError(e.toString()),
|
||||
);
|
||||
}
|
||||
} else {
|
||||
try {
|
||||
emit(const NonBookableSpacesLoading());
|
||||
final nonBookableSpacesList = await nonBookableSpacesService.load(
|
||||
event.nonBookableSpacesParams,
|
||||
);
|
||||
emit(
|
||||
NonBookableSpacesLoaded(nonBookableSpaces: nonBookableSpacesList),
|
||||
);
|
||||
} catch (e) {
|
||||
emit(
|
||||
NonBookableSpacesError(e.toString()),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,17 @@
|
||||
part of 'non_bookaable_spaces_bloc.dart';
|
||||
|
||||
sealed class NonBookableSpacesEvent extends Equatable {
|
||||
const NonBookableSpacesEvent();
|
||||
|
||||
@override
|
||||
List<Object> get props => [];
|
||||
}
|
||||
|
||||
class CallInitStateEvent extends NonBookableSpacesEvent {}
|
||||
|
||||
class LoadUnBookableSpacesEvent extends NonBookableSpacesEvent {
|
||||
final NonBookableSpacesParams nonBookableSpacesParams;
|
||||
const LoadUnBookableSpacesEvent({
|
||||
required this.nonBookableSpacesParams,
|
||||
});
|
||||
}
|
@ -0,0 +1,32 @@
|
||||
part of 'non_bookaable_spaces_bloc.dart';
|
||||
|
||||
sealed class NonBookableSpacesState extends Equatable {
|
||||
const NonBookableSpacesState();
|
||||
|
||||
@override
|
||||
List<Object> get props => [];
|
||||
}
|
||||
|
||||
final class NonBookableSpacesInitial extends NonBookableSpacesState {}
|
||||
|
||||
class NonBookableSpacesLoading extends NonBookableSpacesState {
|
||||
final PaginatedDataModel<BookableSpacemodel>? lastNonBookableSpaces;
|
||||
const NonBookableSpacesLoading({
|
||||
this.lastNonBookableSpaces,
|
||||
});
|
||||
}
|
||||
|
||||
class NonBookableSpacesLoaded extends NonBookableSpacesState {
|
||||
final PaginatedDataModel<BookableSpacemodel> nonBookableSpaces;
|
||||
final List<BookableSpacemodel> selectedBookableSpaces;
|
||||
const NonBookableSpacesLoaded({
|
||||
required this.nonBookableSpaces,
|
||||
this.selectedBookableSpaces = const [],
|
||||
});
|
||||
}
|
||||
|
||||
class NonBookableSpacesError extends NonBookableSpacesState {
|
||||
final String error;
|
||||
const NonBookableSpacesError(this.error);
|
||||
}
|
||||
|
@ -0,0 +1,33 @@
|
||||
import 'package:bloc/bloc.dart';
|
||||
import 'package:equatable/equatable.dart';
|
||||
import 'package:syncrow_web/pages/access_management/manage_bookable_spaces/domain/models/bookable_space_model.dart';
|
||||
import 'package:syncrow_web/pages/access_management/manage_bookable_spaces/domain/params/send_bookable_spaces_to_api_params.dart';
|
||||
import 'package:syncrow_web/pages/access_management/manage_bookable_spaces/domain/service/send_bookable_spaces_service.dart';
|
||||
|
||||
part 'send_bookable_spaces_event.dart';
|
||||
part 'send_bookable_spaces_state.dart';
|
||||
|
||||
class SendBookableSpacesBloc
|
||||
extends Bloc<SendBookableSpacesEvent, SendBookableSpacesState> {
|
||||
SendBookableSpacesService sendBookableSpacesService;
|
||||
SendBookableSpacesBloc(
|
||||
this.sendBookableSpacesService,
|
||||
) : super(SendBookableSpacesInitial()) {
|
||||
on<SendBookableSpacesToApi>(_onSendBookableSpacesToApi);
|
||||
}
|
||||
Future<void> _onSendBookableSpacesToApi(SendBookableSpacesToApi event,
|
||||
Emitter<SendBookableSpacesState> emit) async {
|
||||
emit(SendBookableSpacesLoading());
|
||||
try {
|
||||
await sendBookableSpacesService.sendBookableSpacesToApi(
|
||||
SendBookableSpacesToApiParams.fromBookableSpacesModel(
|
||||
event.selectedBookableSpaces),
|
||||
);
|
||||
emit(SendBookableSpacesSuccess());
|
||||
} catch (e) {
|
||||
emit(
|
||||
SendBookableSpacesError(e.toString()),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,13 @@
|
||||
part of 'send_bookable_spaces_bloc.dart';
|
||||
|
||||
sealed class SendBookableSpacesEvent extends Equatable {
|
||||
const SendBookableSpacesEvent();
|
||||
|
||||
@override
|
||||
List<Object> get props => [];
|
||||
}
|
||||
|
||||
class SendBookableSpacesToApi extends SendBookableSpacesEvent {
|
||||
final List<BookableSpacemodel> selectedBookableSpaces;
|
||||
const SendBookableSpacesToApi({required this.selectedBookableSpaces});
|
||||
}
|
@ -0,0 +1,19 @@
|
||||
part of 'send_bookable_spaces_bloc.dart';
|
||||
|
||||
sealed class SendBookableSpacesState extends Equatable {
|
||||
const SendBookableSpacesState();
|
||||
|
||||
@override
|
||||
List<Object> get props => [];
|
||||
}
|
||||
|
||||
final class SendBookableSpacesInitial extends SendBookableSpacesState {}
|
||||
|
||||
class SendBookableSpacesLoading extends SendBookableSpacesState {}
|
||||
|
||||
class SendBookableSpacesSuccess extends SendBookableSpacesState {}
|
||||
|
||||
class SendBookableSpacesError extends SendBookableSpacesState {
|
||||
final String error;
|
||||
const SendBookableSpacesError(this.error);
|
||||
}
|
@ -0,0 +1,132 @@
|
||||
import 'package:bloc/bloc.dart';
|
||||
import 'package:equatable/equatable.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:syncrow_web/pages/access_management/manage_bookable_spaces/domain/models/bookable_space_model.dart';
|
||||
import 'package:syncrow_web/pages/access_management/manage_bookable_spaces/domain/service/non_bookable_spaces_service.dart';
|
||||
|
||||
part 'setup_bookable_spaces_event.dart';
|
||||
part 'setup_bookable_spaces_state.dart';
|
||||
|
||||
class SetupBookableSpacesBloc
|
||||
extends Bloc<SetupBookableSpacesEvent, SetupBookableSpacesState> {
|
||||
NonBookableSpacesService nonBookableSpacesService;
|
||||
|
||||
SetupBookableSpacesBloc(this.nonBookableSpacesService)
|
||||
: super(const SetupBookableSpacesInitial(bookableSpaces: [])) {
|
||||
on<AddToBookableSpaceEvent>(_onAddToBookableSpaceEvent);
|
||||
on<RemoveFromBookableSpaceEvent>(_onRemoveFromBookableSpaceEvent);
|
||||
on<AddBookableDaysEvent>(_onAddBookableDays);
|
||||
on<ChangeStartTimeEvent>(_onChangeStartTimeEvent);
|
||||
on<ChangeEndTimeEvent>(_onChangeEndTimeEvent);
|
||||
on<ChangeCostEvent>(_onChangeCostEvent);
|
||||
|
||||
on<CheckConfigurValidityEvent>(_onCheckConfigurValidityEvent);
|
||||
on<EditModeSelected>(_onEditModeSelected);
|
||||
}
|
||||
List<BookableSpacemodel> get currentBookableSpaces {
|
||||
return switch (state) {
|
||||
AddNonBookableSpaceIntoBookableState(:final bookableSpaces) =>
|
||||
bookableSpaces,
|
||||
RemoveBookableSpaceIntoNonBookableState(:final bookableSpaces) =>
|
||||
bookableSpaces,
|
||||
SetupBookableSpacesInitial(:final bookableSpaces) => bookableSpaces,
|
||||
InProgressState(:final bookableSpaces) => bookableSpaces,
|
||||
ValidSaveButtonState(:final bookableSpaces) => bookableSpaces,
|
||||
UnValidSaveButtonState(:final bookableSpaces) => bookableSpaces,
|
||||
};
|
||||
}
|
||||
|
||||
void _onAddToBookableSpaceEvent(
|
||||
AddToBookableSpaceEvent event,
|
||||
Emitter<SetupBookableSpacesState> emit,
|
||||
) {
|
||||
emit(InProgressState(bookableSpaces: state.bookableSpaces));
|
||||
final updatedSpaces = List<BookableSpacemodel>.from(state.bookableSpaces);
|
||||
|
||||
updatedSpaces.add(event.nonBookableSpace);
|
||||
|
||||
emit(AddNonBookableSpaceIntoBookableState(bookableSpaces: updatedSpaces));
|
||||
}
|
||||
|
||||
void _onRemoveFromBookableSpaceEvent(RemoveFromBookableSpaceEvent event,
|
||||
Emitter<SetupBookableSpacesState> emit) {
|
||||
emit(InProgressState(bookableSpaces: state.bookableSpaces));
|
||||
state.bookableSpaces.remove(event.bookableSpace);
|
||||
emit(RemoveBookableSpaceIntoNonBookableState(
|
||||
bookableSpaces: state.bookableSpaces));
|
||||
}
|
||||
|
||||
void _onAddBookableDays(
|
||||
AddBookableDaysEvent event, Emitter<SetupBookableSpacesState> emit) {
|
||||
final updatedSpaces = state.bookableSpaces.map((space) {
|
||||
final updatedConfig = space.spaceConfig?.copyWith(
|
||||
bookableDays: event.bookableDays,
|
||||
);
|
||||
|
||||
return space.copyWith(spaceConfig: updatedConfig);
|
||||
}).toList();
|
||||
|
||||
emit(SetupBookableSpacesInitial(bookableSpaces: updatedSpaces));
|
||||
}
|
||||
|
||||
void _onChangeStartTimeEvent(
|
||||
ChangeStartTimeEvent event, Emitter<SetupBookableSpacesState> emit) {
|
||||
final updatedSpaces = state.bookableSpaces.map((space) {
|
||||
final updatedConfig = space.spaceConfig?.copyWith(
|
||||
bookingStartTime: event.startTime,
|
||||
);
|
||||
|
||||
return space.copyWith(spaceConfig: updatedConfig);
|
||||
}).toList();
|
||||
|
||||
emit(SetupBookableSpacesInitial(bookableSpaces: updatedSpaces));
|
||||
}
|
||||
|
||||
void _onChangeEndTimeEvent(
|
||||
ChangeEndTimeEvent event, Emitter<SetupBookableSpacesState> emit) {
|
||||
final updatedSpaces = state.bookableSpaces.map((space) {
|
||||
final updatedConfig = space.spaceConfig?.copyWith(
|
||||
bookingEndTime: event.endTime,
|
||||
);
|
||||
|
||||
return space.copyWith(spaceConfig: updatedConfig);
|
||||
}).toList();
|
||||
|
||||
emit(SetupBookableSpacesInitial(bookableSpaces: updatedSpaces));
|
||||
}
|
||||
|
||||
void _onChangeCostEvent(
|
||||
ChangeCostEvent event, Emitter<SetupBookableSpacesState> emit) {
|
||||
final updatedSpaces = state.bookableSpaces.map((space) {
|
||||
final updatedConfig = space.spaceConfig?.copyWith(
|
||||
cost: event.cost,
|
||||
);
|
||||
|
||||
return space.copyWith(spaceConfig: updatedConfig);
|
||||
}).toList();
|
||||
|
||||
emit(SetupBookableSpacesInitial(bookableSpaces: updatedSpaces));
|
||||
}
|
||||
|
||||
void _onCheckConfigurValidityEvent(CheckConfigurValidityEvent event,
|
||||
Emitter<SetupBookableSpacesState> emit) {
|
||||
if (state.bookableSpaces.first.spaceConfig!.isValid) {
|
||||
emit(ValidSaveButtonState(
|
||||
bookableSpaces: state.bookableSpaces,
|
||||
));
|
||||
} else {
|
||||
emit(UnValidSaveButtonState(
|
||||
bookableSpaces: state.bookableSpaces,
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
void _onEditModeSelected(
|
||||
EditModeSelected event,
|
||||
Emitter<SetupBookableSpacesState> emit,
|
||||
) {
|
||||
final updatedList = [event.editingBookableSpace];
|
||||
|
||||
emit(SetupBookableSpacesInitial(bookableSpaces: updatedList));
|
||||
}
|
||||
}
|
@ -0,0 +1,59 @@
|
||||
part of 'setup_bookable_spaces_bloc.dart';
|
||||
|
||||
sealed class SetupBookableSpacesEvent extends Equatable {
|
||||
const SetupBookableSpacesEvent();
|
||||
|
||||
@override
|
||||
List<Object> get props => [];
|
||||
}
|
||||
|
||||
class AddToBookableSpaceEvent extends SetupBookableSpacesEvent {
|
||||
final BookableSpacemodel nonBookableSpace;
|
||||
const AddToBookableSpaceEvent({
|
||||
required this.nonBookableSpace,
|
||||
});
|
||||
}
|
||||
|
||||
class RemoveFromBookableSpaceEvent extends SetupBookableSpacesEvent {
|
||||
final BookableSpacemodel bookableSpace;
|
||||
const RemoveFromBookableSpaceEvent({
|
||||
required this.bookableSpace,
|
||||
});
|
||||
}
|
||||
|
||||
class AddBookableDaysEvent extends SetupBookableSpacesEvent {
|
||||
final List<String> bookableDays;
|
||||
const AddBookableDaysEvent({
|
||||
required this.bookableDays,
|
||||
});
|
||||
}
|
||||
|
||||
class ChangeCostEvent extends SetupBookableSpacesEvent {
|
||||
final int cost;
|
||||
const ChangeCostEvent({
|
||||
required this.cost,
|
||||
});
|
||||
}
|
||||
|
||||
class ChangeStartTimeEvent extends SetupBookableSpacesEvent {
|
||||
final TimeOfDay startTime;
|
||||
const ChangeStartTimeEvent({
|
||||
required this.startTime,
|
||||
});
|
||||
}
|
||||
|
||||
class ChangeEndTimeEvent extends SetupBookableSpacesEvent {
|
||||
final TimeOfDay endTime;
|
||||
const ChangeEndTimeEvent({
|
||||
required this.endTime,
|
||||
});
|
||||
}
|
||||
|
||||
class CheckConfigurValidityEvent extends SetupBookableSpacesEvent {}
|
||||
|
||||
class EditModeSelected extends SetupBookableSpacesEvent {
|
||||
final BookableSpacemodel editingBookableSpace;
|
||||
const EditModeSelected({
|
||||
required this.editingBookableSpace,
|
||||
});
|
||||
}
|
@ -0,0 +1,37 @@
|
||||
part of 'setup_bookable_spaces_bloc.dart';
|
||||
|
||||
sealed class SetupBookableSpacesState extends Equatable {
|
||||
final List<BookableSpacemodel> bookableSpaces;
|
||||
const SetupBookableSpacesState({required this.bookableSpaces});
|
||||
TimeOfDay? get startTime =>
|
||||
bookableSpaces.first.spaceConfig!.bookingStartTime;
|
||||
TimeOfDay? get endTime => bookableSpaces.first.spaceConfig!.bookingEndTime;
|
||||
@override
|
||||
List<Object> get props => [];
|
||||
}
|
||||
|
||||
final class SetupBookableSpacesInitial extends SetupBookableSpacesState {
|
||||
const SetupBookableSpacesInitial({required super.bookableSpaces});
|
||||
}
|
||||
|
||||
class AddNonBookableSpaceIntoBookableState extends SetupBookableSpacesState {
|
||||
const AddNonBookableSpaceIntoBookableState({required super.bookableSpaces});
|
||||
}
|
||||
|
||||
class InProgressState extends SetupBookableSpacesState {
|
||||
const InProgressState({required super.bookableSpaces});
|
||||
}
|
||||
|
||||
class RemoveBookableSpaceIntoNonBookableState extends SetupBookableSpacesState {
|
||||
const RemoveBookableSpaceIntoNonBookableState({
|
||||
required super.bookableSpaces,
|
||||
});
|
||||
}
|
||||
|
||||
class ValidSaveButtonState extends SetupBookableSpacesState {
|
||||
const ValidSaveButtonState({required super.bookableSpaces});
|
||||
}
|
||||
|
||||
class UnValidSaveButtonState extends SetupBookableSpacesState {
|
||||
const UnValidSaveButtonState({required super.bookableSpaces});
|
||||
}
|
@ -0,0 +1,22 @@
|
||||
import 'package:bloc/bloc.dart';
|
||||
import 'package:equatable/equatable.dart';
|
||||
|
||||
part 'steps_state.dart';
|
||||
|
||||
class StepsCubit extends Cubit<StepsState> {
|
||||
StepsCubit() : super(StepOneState());
|
||||
|
||||
void initDialogValue() {
|
||||
emit(StepOneState());
|
||||
}
|
||||
|
||||
void editValueInit() {
|
||||
emit(StepTwoState());
|
||||
}
|
||||
|
||||
void goToNextStep() {
|
||||
if (state is StepOneState) {
|
||||
emit(StepTwoState());
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,12 @@
|
||||
part of 'steps_cubit.dart';
|
||||
|
||||
sealed class StepsState extends Equatable {
|
||||
const StepsState();
|
||||
|
||||
@override
|
||||
List<Object> get props => [];
|
||||
}
|
||||
|
||||
final class StepOneState extends StepsState {}
|
||||
|
||||
final class StepTwoState extends StepsState {}
|
@ -0,0 +1,16 @@
|
||||
import 'package:bloc/bloc.dart';
|
||||
import 'package:equatable/equatable.dart';
|
||||
|
||||
part 'toggle_points_switch_state.dart';
|
||||
|
||||
class TogglePointsSwitchCubit extends Cubit<TogglePointsSwitchState> {
|
||||
TogglePointsSwitchCubit() : super(UnActivatePointsSwitch());
|
||||
|
||||
void activateSwitch() {
|
||||
emit(ActivatePointsSwitch());
|
||||
}
|
||||
|
||||
void unActivateSwitch() {
|
||||
emit(UnActivatePointsSwitch());
|
||||
}
|
||||
}
|
@ -0,0 +1,13 @@
|
||||
part of 'toggle_points_switch_cubit.dart';
|
||||
|
||||
sealed class TogglePointsSwitchState extends Equatable {
|
||||
const TogglePointsSwitchState();
|
||||
|
||||
@override
|
||||
List<Object> get props => [];
|
||||
}
|
||||
|
||||
|
||||
class ActivatePointsSwitch extends TogglePointsSwitchState {}
|
||||
|
||||
class UnActivatePointsSwitch extends TogglePointsSwitchState {}
|
@ -0,0 +1,36 @@
|
||||
import 'package:bloc/bloc.dart';
|
||||
import 'package:equatable/equatable.dart';
|
||||
import 'package:syncrow_web/pages/access_management/manage_bookable_spaces/domain/models/bookable_space_config.dart';
|
||||
import 'package:syncrow_web/pages/access_management/manage_bookable_spaces/domain/params/update_bookable_space_param.dart';
|
||||
import 'package:syncrow_web/pages/access_management/manage_bookable_spaces/domain/service/update_bookable_space_service.dart';
|
||||
import 'package:syncrow_web/services/api/api_exception.dart';
|
||||
|
||||
part 'update_bookable_spaces_event.dart';
|
||||
part 'update_bookable_spaces_state.dart';
|
||||
|
||||
class UpdateBookableSpacesBloc
|
||||
extends Bloc<UpdateBookableSpaceEvent, UpdateBookableSpacesState> {
|
||||
final UpdateBookableSpaceService updateBookableSpaceService;
|
||||
UpdateBookableSpacesBloc(this.updateBookableSpaceService)
|
||||
: super(UpdateBookableSpacesInitial()) {
|
||||
on<UpdateBookableSpace>(_onUpdateBookableSpace);
|
||||
}
|
||||
|
||||
Future<void> _onUpdateBookableSpace(UpdateBookableSpace event,
|
||||
Emitter<UpdateBookableSpacesState> emit) async {
|
||||
emit(UpdateBookableSpaceLoading(event.updatedParam.spaceUuid));
|
||||
try {
|
||||
final updatedSpace =
|
||||
await updateBookableSpaceService.update(event.updatedParam);
|
||||
|
||||
emit(UpdateBookableSpaceSuccess(bookableSpaceConfig: updatedSpace));
|
||||
event.onSuccess?.call();
|
||||
} on APIException catch (e) {
|
||||
emit(UpdateBookableSpaceFailure(error: e.message));
|
||||
} catch (e) {
|
||||
emit(
|
||||
UpdateBookableSpaceFailure(error: e.toString()),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,17 @@
|
||||
part of 'update_bookable_spaces_bloc.dart';
|
||||
|
||||
sealed class UpdateBookableSpaceEvent extends Equatable {
|
||||
const UpdateBookableSpaceEvent();
|
||||
|
||||
@override
|
||||
List<Object> get props => [];
|
||||
}
|
||||
|
||||
class UpdateBookableSpace extends UpdateBookableSpaceEvent {
|
||||
final void Function()? onSuccess;
|
||||
final UpdateBookableSpaceParam updatedParam;
|
||||
const UpdateBookableSpace({
|
||||
required this.updatedParam,
|
||||
this.onSuccess,
|
||||
});
|
||||
}
|
@ -0,0 +1,30 @@
|
||||
part of 'update_bookable_spaces_bloc.dart';
|
||||
|
||||
sealed class UpdateBookableSpacesState extends Equatable {
|
||||
const UpdateBookableSpacesState();
|
||||
|
||||
@override
|
||||
List<Object> get props => [];
|
||||
}
|
||||
|
||||
final class UpdateBookableSpacesInitial extends UpdateBookableSpacesState {}
|
||||
|
||||
final class UpdateBookableSpaceLoading extends UpdateBookableSpacesState {
|
||||
final String updatingSpaceUuid;
|
||||
|
||||
const UpdateBookableSpaceLoading(this.updatingSpaceUuid);
|
||||
}
|
||||
|
||||
final class UpdateBookableSpaceSuccess extends UpdateBookableSpacesState {
|
||||
final BookableSpaceConfig bookableSpaceConfig;
|
||||
const UpdateBookableSpaceSuccess({
|
||||
required this.bookableSpaceConfig,
|
||||
});
|
||||
}
|
||||
|
||||
final class UpdateBookableSpaceFailure extends UpdateBookableSpacesState {
|
||||
final String error;
|
||||
const UpdateBookableSpaceFailure({
|
||||
required this.error,
|
||||
});
|
||||
}
|
@ -0,0 +1,91 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:syncrow_web/pages/access_management/manage_bookable_spaces/data/remote_bookable_spaces_service.dart';
|
||||
import 'package:syncrow_web/pages/access_management/manage_bookable_spaces/data/remote_update_bookable_space_service.dart';
|
||||
import 'package:syncrow_web/pages/access_management/manage_bookable_spaces/domain/params/bookable_spaces_params.dart';
|
||||
import 'package:syncrow_web/pages/access_management/manage_bookable_spaces/presentation/blocs/bookable_spaces_bloc/bookable_spaces_bloc.dart';
|
||||
import 'package:syncrow_web/pages/access_management/manage_bookable_spaces/presentation/blocs/update_bookable_spaces/update_bookable_spaces_bloc.dart';
|
||||
import 'package:syncrow_web/pages/access_management/manage_bookable_spaces/presentation/widgets/main_manage_bookable_widgets/bottom_pagination_part_widget.dart';
|
||||
import 'package:syncrow_web/pages/access_management/manage_bookable_spaces/presentation/widgets/main_manage_bookable_widgets/table_part_widget.dart';
|
||||
import 'package:syncrow_web/pages/access_management/manage_bookable_spaces/presentation/widgets/main_manage_bookable_widgets/top_part_widget.dart';
|
||||
|
||||
import 'package:syncrow_web/services/api/http_service.dart';
|
||||
|
||||
class ManageBookableSpacesPage extends StatefulWidget {
|
||||
final PageController pageController;
|
||||
const ManageBookableSpacesPage({
|
||||
super.key,
|
||||
required this.pageController,
|
||||
});
|
||||
|
||||
@override
|
||||
State<ManageBookableSpacesPage> createState() =>
|
||||
_ManageBookableSpacesPageState();
|
||||
}
|
||||
|
||||
class _ManageBookableSpacesPageState extends State<ManageBookableSpacesPage> {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return MultiBlocProvider(
|
||||
providers: [
|
||||
BlocProvider(
|
||||
create: (context) => BookableSpacesBloc(
|
||||
RemoteBookableSpacesService(HTTPService()),
|
||||
)..add(
|
||||
LoadBookableSpacesEvent(
|
||||
BookableSpacesParam(currentPage: 1),
|
||||
),
|
||||
),
|
||||
),
|
||||
BlocProvider(
|
||||
create: (context) => UpdateBookableSpacesBloc(
|
||||
RemoteUpdateBookableSpaceService(HTTPService()),
|
||||
),
|
||||
)
|
||||
],
|
||||
child: ManageBookableSpacesWidget(
|
||||
pageController: widget.pageController,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class ManageBookableSpacesWidget extends StatelessWidget {
|
||||
final PageController pageController;
|
||||
|
||||
const ManageBookableSpacesWidget({
|
||||
super.key,
|
||||
required this.pageController,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
vertical: 10,
|
||||
horizontal: 35,
|
||||
),
|
||||
child: Column(
|
||||
children: [
|
||||
Expanded(
|
||||
flex: 10,
|
||||
child: RowOfButtonsTitleWidget(pageController: pageController)),
|
||||
const SizedBox(
|
||||
height: 10,
|
||||
),
|
||||
const Expanded(
|
||||
flex: 85,
|
||||
child: TableOfBookableSpacesWidget(),
|
||||
),
|
||||
const SizedBox(
|
||||
height: 15,
|
||||
),
|
||||
const Expanded(
|
||||
flex: 5,
|
||||
child: PaginationButtonsWidget(),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
@ -0,0 +1,134 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:syncrow_web/pages/access_management/manage_bookable_spaces/data/non_bookable_spaces_decorator.dart';
|
||||
import 'package:syncrow_web/pages/access_management/manage_bookable_spaces/data/remote_non_bookable_spaces.dart';
|
||||
import 'package:syncrow_web/pages/access_management/manage_bookable_spaces/data/remote_send_bookable_spaces.dart';
|
||||
import 'package:syncrow_web/pages/access_management/manage_bookable_spaces/domain/models/bookable_space_model.dart';
|
||||
import 'package:syncrow_web/pages/access_management/manage_bookable_spaces/domain/params/non_bookable_spaces_params.dart';
|
||||
import 'package:syncrow_web/pages/access_management/manage_bookable_spaces/presentation/blocs/non_bookable_spaces_bloc/non_bookaable_spaces_bloc.dart';
|
||||
import 'package:syncrow_web/pages/access_management/manage_bookable_spaces/presentation/blocs/send_bookable_spaces_bloc/send_bookable_spaces_bloc.dart';
|
||||
import 'package:syncrow_web/pages/access_management/manage_bookable_spaces/presentation/blocs/setup_bookable_spaces_bloc/setup_bookable_spaces_bloc.dart';
|
||||
import 'package:syncrow_web/pages/access_management/manage_bookable_spaces/presentation/blocs/steps_cubit/cubit/steps_cubit.dart';
|
||||
import 'package:syncrow_web/pages/access_management/manage_bookable_spaces/presentation/widgets/details_steps_widget.dart';
|
||||
import 'package:syncrow_web/pages/access_management/manage_bookable_spaces/presentation/widgets/next_first_step_button.dart';
|
||||
import 'package:syncrow_web/pages/access_management/manage_bookable_spaces/presentation/widgets/save_second_step_button.dart';
|
||||
import 'package:syncrow_web/pages/access_management/manage_bookable_spaces/presentation/widgets/stepper_part_widget.dart';
|
||||
import 'package:syncrow_web/services/api/http_service.dart';
|
||||
import 'package:syncrow_web/utils/color_manager.dart';
|
||||
|
||||
class SetupBookableSpacesDialog extends StatefulWidget {
|
||||
final BookableSpacemodel? editingBookableSpace;
|
||||
SetupBookableSpacesDialog({
|
||||
super.key,
|
||||
this.editingBookableSpace,
|
||||
});
|
||||
|
||||
@override
|
||||
State<SetupBookableSpacesDialog> createState() =>
|
||||
_SetupBookableSpacesDialogState();
|
||||
}
|
||||
|
||||
class _SetupBookableSpacesDialogState extends State<SetupBookableSpacesDialog> {
|
||||
final TextEditingController pointsController = TextEditingController();
|
||||
@override
|
||||
void dispose() {
|
||||
pointsController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return MultiBlocProvider(
|
||||
providers: [
|
||||
BlocProvider<StepsCubit>(
|
||||
create: widget.editingBookableSpace == null
|
||||
? (context) => StepsCubit()..initDialogValue()
|
||||
: (context) => StepsCubit()..editValueInit(),
|
||||
),
|
||||
BlocProvider<NonBookableSpacesBloc>(
|
||||
create: (context) => NonBookableSpacesBloc(
|
||||
NonBookableSpacesDebouncerDecoratorService(
|
||||
RemoteNonBookableSpaces(HTTPService()),
|
||||
),
|
||||
)..add(
|
||||
LoadUnBookableSpacesEvent(
|
||||
nonBookableSpacesParams:
|
||||
NonBookableSpacesParams(currentPage: 1),
|
||||
),
|
||||
),
|
||||
),
|
||||
BlocProvider<SetupBookableSpacesBloc>(
|
||||
create: widget.editingBookableSpace == null
|
||||
? (context) => SetupBookableSpacesBloc(
|
||||
RemoteNonBookableSpaces(HTTPService()))
|
||||
: (context) => SetupBookableSpacesBloc(
|
||||
RemoteNonBookableSpaces(HTTPService()))
|
||||
..add(
|
||||
EditModeSelected(
|
||||
editingBookableSpace: widget.editingBookableSpace!),
|
||||
),
|
||||
),
|
||||
BlocProvider(
|
||||
create: (context) => SendBookableSpacesBloc(
|
||||
RemoteSendBookableSpaces(HTTPService()),
|
||||
),
|
||||
)
|
||||
],
|
||||
child: AlertDialog(
|
||||
backgroundColor: ColorsManager.whiteColors,
|
||||
contentPadding: EdgeInsets.zero,
|
||||
title: Center(
|
||||
child: Text(
|
||||
'Set Up a Bookable Spaces',
|
||||
style: TextStyle(
|
||||
fontWeight: FontWeight.w700,
|
||||
color: ColorsManager.dialogBlueTitle,
|
||||
fontSize: 15,
|
||||
),
|
||||
),
|
||||
),
|
||||
content: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
const Divider(),
|
||||
Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const Expanded(
|
||||
flex: 3,
|
||||
child: StepperPartWidget(),
|
||||
),
|
||||
const SizedBox(
|
||||
height: 588,
|
||||
child: VerticalDivider(
|
||||
thickness: 0.5,
|
||||
width: 1,
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
flex: 7,
|
||||
child: DetailsStepsWidget(
|
||||
pointsController: pointsController,
|
||||
editingBookableSpace: widget.editingBookableSpace,
|
||||
),
|
||||
)
|
||||
],
|
||||
),
|
||||
Builder(builder: (context) {
|
||||
final stepsState = context.watch<StepsCubit>().state;
|
||||
final bookableSpaces =
|
||||
context.watch<SetupBookableSpacesBloc>().state.bookableSpaces;
|
||||
return stepsState is StepOneState
|
||||
? const NextFirstStepButton()
|
||||
: SaveSecondStepButton(
|
||||
pointsController: pointsController,
|
||||
isEditingMode: widget.editingBookableSpace != null,
|
||||
bookableSpaces: bookableSpaces,
|
||||
);
|
||||
}),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
@ -0,0 +1,71 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:syncrow_web/pages/access_management/manage_bookable_spaces/domain/models/bookable_space_model.dart';
|
||||
import 'package:syncrow_web/pages/access_management/manage_bookable_spaces/domain/params/update_bookable_space_param.dart';
|
||||
import 'package:syncrow_web/pages/access_management/manage_bookable_spaces/presentation/blocs/bookable_spaces_bloc/bookable_spaces_bloc.dart';
|
||||
import 'package:syncrow_web/pages/access_management/manage_bookable_spaces/presentation/blocs/update_bookable_spaces/update_bookable_spaces_bloc.dart';
|
||||
import 'package:syncrow_web/pages/space_management_v2/main_module/shared/models/paginated_data_model.dart';
|
||||
import 'package:syncrow_web/utils/color_manager.dart';
|
||||
|
||||
class BookableSpaceSwitchActivationWidget extends StatelessWidget {
|
||||
final PaginatedDataModel<BookableSpacemodel> bookableSpaces;
|
||||
final BookableSpacemodel space;
|
||||
const BookableSpaceSwitchActivationWidget({
|
||||
super.key,
|
||||
required this.bookableSpaces,
|
||||
required this.space,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Center(
|
||||
child: Transform.scale(
|
||||
scale: 0.7,
|
||||
child:
|
||||
BlocConsumer<UpdateBookableSpacesBloc, UpdateBookableSpacesState>(
|
||||
listener: (context, updateState) {
|
||||
if (updateState is UpdateBookableSpaceSuccess) {
|
||||
context.read<BookableSpacesBloc>().add(
|
||||
InsertUpdatedSpaceEvent(
|
||||
bookableSpaces: bookableSpaces,
|
||||
bookableSpace: space,
|
||||
updatedBookableSpaceConfig:
|
||||
updateState.bookableSpaceConfig,
|
||||
),
|
||||
);
|
||||
}
|
||||
},
|
||||
builder: (context, updateState) {
|
||||
final isLoading = updateState is UpdateBookableSpaceLoading &&
|
||||
updateState.updatingSpaceUuid == space.spaceUuid;
|
||||
if (isLoading) {
|
||||
return const Center(child: CircularProgressIndicator());
|
||||
}
|
||||
return Switch(
|
||||
trackOutlineColor: WidgetStateProperty.resolveWith<Color>(
|
||||
(Set<WidgetState> states) {
|
||||
return ColorsManager.whiteColors;
|
||||
}),
|
||||
value: space.spaceConfig!.availability,
|
||||
activeTrackColor: ColorsManager.dialogBlueTitle,
|
||||
inactiveTrackColor: ColorsManager.grayBorder,
|
||||
thumbColor: WidgetStateProperty.resolveWith<Color>(
|
||||
(Set<WidgetState> states) {
|
||||
return ColorsManager.whiteColors;
|
||||
}),
|
||||
onChanged: (value) {
|
||||
context.read<UpdateBookableSpacesBloc>().add(
|
||||
UpdateBookableSpace(
|
||||
updatedParam: UpdateBookableSpaceParam(
|
||||
spaceUuid: space.spaceUuid,
|
||||
availability: value,
|
||||
)),
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
@ -0,0 +1,141 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:flutter_svg/svg.dart';
|
||||
import 'package:syncrow_web/pages/access_management/manage_bookable_spaces/domain/models/bookable_space_model.dart';
|
||||
import 'package:syncrow_web/pages/access_management/manage_bookable_spaces/presentation/blocs/setup_bookable_spaces_bloc/setup_bookable_spaces_bloc.dart';
|
||||
import 'package:syncrow_web/pages/access_management/manage_bookable_spaces/presentation/widgets/time_picker_widget.dart';
|
||||
import 'package:syncrow_web/utils/color_manager.dart';
|
||||
import 'package:syncrow_web/utils/constants/assets.dart';
|
||||
import 'package:syncrow_web/utils/string_utils.dart';
|
||||
|
||||
class BookingPeriodWidget extends StatelessWidget {
|
||||
final BookableSpacemodel? editingBookableSpace;
|
||||
const BookingPeriodWidget({
|
||||
super.key,
|
||||
this.editingBookableSpace,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final state = context.watch<SetupBookableSpacesBloc>().state;
|
||||
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
Text(
|
||||
'* ',
|
||||
style: Theme.of(context)
|
||||
.textTheme
|
||||
.bodyMedium!
|
||||
.copyWith(color: Colors.red),
|
||||
),
|
||||
const Text('Booking Period'),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 5),
|
||||
Container(
|
||||
width: 230,
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
color: ColorsManager.circleRolesBackground,
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
offset: Offset.zero,
|
||||
blurRadius: 4,
|
||||
spreadRadius: 0,
|
||||
color: ColorsManager.timePickerColor.withValues(alpha: 0.15),
|
||||
)
|
||||
],
|
||||
),
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
TimePickerWidget(
|
||||
title: editingBookableSpace?.spaceConfig?.bookingStartTime
|
||||
?.format(context) ??
|
||||
'Start Time',
|
||||
onTimePicked: (pickedStartTime) {
|
||||
if (pickedStartTime == null) return;
|
||||
|
||||
if (state.endTime != null &&
|
||||
isEndTimeAfterStartTime(
|
||||
pickedStartTime, state.endTime!)) {
|
||||
_showInvalidSnackBar(
|
||||
context, "You can't choose Start Time after End Time");
|
||||
return;
|
||||
}
|
||||
|
||||
context.read<SetupBookableSpacesBloc>().add(
|
||||
ChangeStartTimeEvent(startTime: pickedStartTime),
|
||||
);
|
||||
context.read<SetupBookableSpacesBloc>().add(
|
||||
CheckConfigurValidityEvent(),
|
||||
);
|
||||
},
|
||||
),
|
||||
const SizedBox(width: 10),
|
||||
const Icon(
|
||||
Icons.arrow_right_alt,
|
||||
color: ColorsManager.grayColor,
|
||||
size: 13,
|
||||
),
|
||||
TimePickerWidget(
|
||||
title: editingBookableSpace?.spaceConfig?.bookingEndTime
|
||||
?.format(context) ??
|
||||
'End Time',
|
||||
onTimePicked: (pickedEndTime) {
|
||||
if (pickedEndTime == null) return;
|
||||
|
||||
if (state.startTime != null &&
|
||||
isEndTimeAfterStartTime(
|
||||
state.startTime!, pickedEndTime)) {
|
||||
_showInvalidSnackBar(
|
||||
context, "You can't choose End Time before Start Time");
|
||||
return;
|
||||
}
|
||||
|
||||
context.read<SetupBookableSpacesBloc>().add(
|
||||
ChangeEndTimeEvent(endTime: pickedEndTime),
|
||||
);
|
||||
context.read<SetupBookableSpacesBloc>().add(
|
||||
CheckConfigurValidityEvent(),
|
||||
);
|
||||
},
|
||||
),
|
||||
const SizedBox(width: 15),
|
||||
Container(
|
||||
width: 30,
|
||||
height: 32,
|
||||
alignment: Alignment.center,
|
||||
decoration: const BoxDecoration(
|
||||
borderRadius: BorderRadius.only(
|
||||
topLeft: Radius.circular(10),
|
||||
bottomLeft: Radius.circular(10),
|
||||
),
|
||||
),
|
||||
child: SvgPicture.asset(
|
||||
Assets.clockIcon,
|
||||
height: 18,
|
||||
color: ColorsManager.blackColor.withValues(alpha: 0.4),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
void _showInvalidSnackBar(BuildContext context, String message) {
|
||||
ScaffoldMessenger.of(context).clearSnackBars();
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(message),
|
||||
duration: const Duration(seconds: 2),
|
||||
backgroundColor: ColorsManager.red,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
@ -0,0 +1,105 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
import 'package:syncrow_web/pages/access_management/manage_bookable_spaces/domain/params/bookable_spaces_params.dart';
|
||||
import 'package:syncrow_web/pages/access_management/manage_bookable_spaces/presentation/blocs/bookable_spaces_bloc/bookable_spaces_bloc.dart';
|
||||
import 'package:syncrow_web/pages/access_management/manage_bookable_spaces/presentation/blocs/non_bookable_spaces_bloc/non_bookaable_spaces_bloc.dart';
|
||||
import 'package:syncrow_web/utils/color_manager.dart';
|
||||
|
||||
class ButtonsDividerBottomDialogWidget extends StatelessWidget {
|
||||
final String title;
|
||||
final void Function()? onNextPressed;
|
||||
final void Function() onCancelPressed;
|
||||
const ButtonsDividerBottomDialogWidget({
|
||||
super.key,
|
||||
required this.title,
|
||||
required this.onNextPressed,
|
||||
required this.onCancelPressed,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Column(
|
||||
children: [
|
||||
const Divider(
|
||||
thickness: 0.5,
|
||||
height: 1,
|
||||
),
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: InkWell(
|
||||
borderRadius: const BorderRadius.only(
|
||||
bottomLeft: Radius.circular(26),
|
||||
),
|
||||
onTap: onCancelPressed,
|
||||
child: Container(
|
||||
height: 40,
|
||||
alignment: Alignment.center,
|
||||
decoration: const BoxDecoration(
|
||||
border: Border(
|
||||
right: BorderSide(
|
||||
color: ColorsManager.grayBorder,
|
||||
),
|
||||
),
|
||||
borderRadius: BorderRadius.only(
|
||||
bottomLeft: Radius.circular(26),
|
||||
),
|
||||
),
|
||||
child: const Text(
|
||||
'Cancel',
|
||||
style: TextStyle(color: ColorsManager.blackColor),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
child:
|
||||
BlocConsumer<NonBookableSpacesBloc, NonBookableSpacesState>(
|
||||
listener: (context, nonBookableState) {
|
||||
if (nonBookableState is NonBookableSpacesInitial) {
|
||||
context.pop();
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(
|
||||
content: Text(
|
||||
'Operation Done Successfully',
|
||||
style: TextStyle(color: ColorsManager.activeGreen),
|
||||
),
|
||||
duration: Duration(seconds: 2),
|
||||
behavior: SnackBarBehavior.floating,
|
||||
),
|
||||
);
|
||||
context.read<BookableSpacesBloc>().add(
|
||||
LoadBookableSpacesEvent(
|
||||
BookableSpacesParam(currentPage: 1),
|
||||
),
|
||||
);
|
||||
} else if (nonBookableState is NonBookableSpacesError) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(
|
||||
nonBookableState.error,
|
||||
style: const TextStyle(color: ColorsManager.red),
|
||||
),
|
||||
duration: const Duration(seconds: 2),
|
||||
behavior: SnackBarBehavior.floating,
|
||||
),
|
||||
);
|
||||
}
|
||||
},
|
||||
builder: (context, nonBookableState) {
|
||||
return TextButton(
|
||||
onPressed: onNextPressed,
|
||||
child: Text(
|
||||
title,
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
)
|
||||
],
|
||||
)
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
@ -0,0 +1,78 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:syncrow_web/pages/access_management/manage_bookable_spaces/domain/models/bookable_space_model.dart';
|
||||
import 'package:syncrow_web/pages/access_management/manage_bookable_spaces/presentation/blocs/setup_bookable_spaces_bloc/setup_bookable_spaces_bloc.dart';
|
||||
import 'package:syncrow_web/pages/access_management/manage_bookable_spaces/presentation/widgets/custom_checkbox_widget.dart';
|
||||
import 'package:syncrow_web/utils/color_manager.dart';
|
||||
|
||||
class CheckBoxSpaceWidget extends StatelessWidget {
|
||||
final BookableSpacemodel nonBookableSpace;
|
||||
|
||||
const CheckBoxSpaceWidget({
|
||||
super.key,
|
||||
required this.nonBookableSpace,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Row(
|
||||
children: [
|
||||
BlocBuilder<SetupBookableSpacesBloc, SetupBookableSpacesState>(
|
||||
builder: (context, state) {
|
||||
final isChecked = switch (state) {
|
||||
AddNonBookableSpaceIntoBookableState(
|
||||
bookableSpaces: final spaces
|
||||
) =>
|
||||
spaces.any((s) => s.spaceUuid == nonBookableSpace.spaceUuid),
|
||||
RemoveBookableSpaceIntoNonBookableState(
|
||||
bookableSpaces: final spaces
|
||||
) =>
|
||||
spaces.any((s) => s.spaceUuid == nonBookableSpace.spaceUuid),
|
||||
_ => false,
|
||||
};
|
||||
|
||||
return CustomCheckboxWidget(
|
||||
value: isChecked,
|
||||
onChanged: (value) {
|
||||
final bloc = context.read<SetupBookableSpacesBloc>();
|
||||
|
||||
if (value ?? false) {
|
||||
bloc.add(AddToBookableSpaceEvent(
|
||||
nonBookableSpace: nonBookableSpace,
|
||||
));
|
||||
} else {
|
||||
bloc.add(RemoveFromBookableSpaceEvent(
|
||||
bookableSpace: nonBookableSpace,
|
||||
));
|
||||
}
|
||||
},
|
||||
);
|
||||
},
|
||||
),
|
||||
const SizedBox(width: 15),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
nonBookableSpace.spaceName,
|
||||
style: const TextStyle(
|
||||
fontWeight: FontWeight.w700,
|
||||
fontSize: 12,
|
||||
color: ColorsManager.titleGray,
|
||||
),
|
||||
),
|
||||
Text(
|
||||
nonBookableSpace.spaceVirtualAddress,
|
||||
style: const TextStyle(
|
||||
fontWeight: FontWeight.w400,
|
||||
fontSize: 10,
|
||||
color: ColorsManager.titleGray,
|
||||
),
|
||||
),
|
||||
],
|
||||
)),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
@ -0,0 +1,40 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:syncrow_web/utils/color_manager.dart';
|
||||
|
||||
class ColumnTitleWidget extends StatelessWidget {
|
||||
final bool isFirst;
|
||||
final bool isLast;
|
||||
final String title;
|
||||
const ColumnTitleWidget({
|
||||
super.key,
|
||||
required this.title,
|
||||
required this.isFirst,
|
||||
required this.isLast,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
alignment: Alignment.centerLeft,
|
||||
padding: const EdgeInsets.only(left: 10),
|
||||
decoration: BoxDecoration(
|
||||
color: ColorsManager.graysColor,
|
||||
borderRadius: isFirst
|
||||
? const BorderRadius.only(
|
||||
topLeft: Radius.circular(12),
|
||||
)
|
||||
: isLast
|
||||
? const BorderRadius.only(
|
||||
topRight: Radius.circular(12),
|
||||
)
|
||||
: null,
|
||||
),
|
||||
child: Text(
|
||||
title,
|
||||
style: const TextStyle(
|
||||
color: ColorsManager.grayColor,
|
||||
fontSize: 12,
|
||||
),
|
||||
));
|
||||
}
|
||||
}
|
@ -0,0 +1,53 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:syncrow_web/utils/color_manager.dart';
|
||||
|
||||
class CustomCheckboxWidget extends StatelessWidget {
|
||||
final bool value;
|
||||
final ValueChanged<bool?> onChanged;
|
||||
final double? outHeight;
|
||||
final double? outWidth;
|
||||
final double? iconSize;
|
||||
const CustomCheckboxWidget({
|
||||
super.key,
|
||||
required this.value,
|
||||
required this.onChanged,
|
||||
this.outWidth,
|
||||
this.outHeight,
|
||||
this.iconSize,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return GestureDetector(
|
||||
onTap: () => onChanged(!value),
|
||||
child: Container(
|
||||
width: outWidth ?? 17,
|
||||
height: outHeight ?? 17,
|
||||
decoration: BoxDecoration(
|
||||
color: value ? Colors.white : ColorsManager.checkBoxFillColor,
|
||||
border: value
|
||||
? Border.all(color: ColorsManager.secondaryColor, width: 1)
|
||||
: Border.all(color: ColorsManager.checkBoxBorderGray, width: 1),
|
||||
borderRadius: BorderRadius.circular(4),
|
||||
),
|
||||
child: value
|
||||
? Center(
|
||||
child: Container(
|
||||
width: outWidth != null ? outWidth! - 4 : 13,
|
||||
height: outHeight != null ? outHeight! - 4 : 13,
|
||||
decoration: BoxDecoration(
|
||||
color: ColorsManager.secondaryColor,
|
||||
borderRadius: BorderRadius.circular(2),
|
||||
),
|
||||
child: const Icon(
|
||||
Icons.check,
|
||||
size: 12,
|
||||
color: Colors.white,
|
||||
),
|
||||
),
|
||||
)
|
||||
: null,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
@ -0,0 +1,55 @@
|
||||
import 'package:data_table_2/data_table_2.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_svg/flutter_svg.dart';
|
||||
import 'package:syncrow_web/pages/access_management/manage_bookable_spaces/presentation/widgets/column_title_widget.dart';
|
||||
import 'package:syncrow_web/utils/color_manager.dart';
|
||||
import 'package:syncrow_web/utils/constants/assets.dart';
|
||||
|
||||
class CustomDataTable<T> extends StatelessWidget {
|
||||
final List<String> columnsTitles;
|
||||
final List<DataCell> Function(T item) cellsWidgets;
|
||||
final List<T> items;
|
||||
|
||||
const CustomDataTable({
|
||||
super.key,
|
||||
required this.items,
|
||||
required this.cellsWidgets,
|
||||
required this.columnsTitles,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return DataTable2(
|
||||
dividerThickness: 0.5,
|
||||
columnSpacing: 2,
|
||||
horizontalMargin: 0,
|
||||
empty: SvgPicture.asset(Assets.emptyDataTable),
|
||||
decoration: BoxDecoration(
|
||||
color: ColorsManager.circleRolesBackground,
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
boxShadow: const [
|
||||
BoxShadow(
|
||||
color: ColorsManager.textGray,
|
||||
blurRadius: 12,
|
||||
offset: Offset(0, 4),
|
||||
),
|
||||
],
|
||||
),
|
||||
columns: columnsTitles.asMap().entries.map((entry) {
|
||||
final index = entry.key;
|
||||
final title = entry.value;
|
||||
|
||||
return DataColumn(
|
||||
label: ColumnTitleWidget(
|
||||
title: title,
|
||||
isFirst: index == 0,
|
||||
isLast: index == columnsTitles.length - 1,
|
||||
),
|
||||
);
|
||||
}).toList(),
|
||||
rows: items.map((item) {
|
||||
return DataRow(cells: cellsWidgets(item));
|
||||
}).toList(),
|
||||
);
|
||||
}
|
||||
}
|
@ -0,0 +1,32 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:syncrow_web/pages/access_management/manage_bookable_spaces/domain/models/bookable_space_model.dart';
|
||||
import 'package:syncrow_web/pages/access_management/manage_bookable_spaces/presentation/blocs/steps_cubit/cubit/steps_cubit.dart';
|
||||
import 'package:syncrow_web/pages/access_management/manage_bookable_spaces/presentation/widgets/space_step_part_widget.dart';
|
||||
import 'package:syncrow_web/pages/access_management/manage_bookable_spaces/presentation/widgets/step_two_details_widget.dart';
|
||||
|
||||
class DetailsStepsWidget extends StatelessWidget {
|
||||
final TextEditingController pointsController;
|
||||
final BookableSpacemodel? editingBookableSpace;
|
||||
const DetailsStepsWidget({
|
||||
super.key,
|
||||
required this.pointsController,
|
||||
this.editingBookableSpace,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 30, vertical: 20),
|
||||
child: BlocBuilder<StepsCubit, StepsState>(builder: (context, state) {
|
||||
return switch (state) {
|
||||
StepOneState() => const SpacesStepDetailsWidget(),
|
||||
StepTwoState() => StepTwoDetailsWidget(
|
||||
pointsController: pointsController,
|
||||
editingBookableSpace: editingBookableSpace,
|
||||
),
|
||||
};
|
||||
}),
|
||||
);
|
||||
}
|
||||
}
|
@ -0,0 +1,74 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:flutter_svg/svg.dart';
|
||||
import 'package:syncrow_web/pages/access_management/manage_bookable_spaces/data/remote_update_bookable_space_service.dart';
|
||||
import 'package:syncrow_web/pages/access_management/manage_bookable_spaces/domain/models/bookable_space_model.dart';
|
||||
import 'package:syncrow_web/pages/access_management/manage_bookable_spaces/presentation/blocs/bookable_spaces_bloc/bookable_spaces_bloc.dart';
|
||||
import 'package:syncrow_web/pages/access_management/manage_bookable_spaces/presentation/blocs/update_bookable_spaces/update_bookable_spaces_bloc.dart';
|
||||
import 'package:syncrow_web/pages/access_management/manage_bookable_spaces/presentation/screens/setup_bookable_spaces_dialog.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 EditBookableSpaceButtonWidget extends StatelessWidget {
|
||||
final BookableSpacemodel? space;
|
||||
const EditBookableSpaceButtonWidget({
|
||||
super.key,
|
||||
required this.space,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Center(
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
offset: Offset.zero,
|
||||
blurRadius: 3,
|
||||
spreadRadius: 0,
|
||||
color: ColorsManager.timePickerColor.withValues(
|
||||
alpha: 0.3,
|
||||
),
|
||||
)
|
||||
],
|
||||
),
|
||||
child: ElevatedButton(
|
||||
onPressed: () {
|
||||
final bookableBloc = context.read<BookableSpacesBloc>();
|
||||
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (context) => MultiBlocProvider(
|
||||
providers: [
|
||||
BlocProvider.value(
|
||||
value: bookableBloc,
|
||||
),
|
||||
BlocProvider(
|
||||
create: (context) => UpdateBookableSpacesBloc(
|
||||
RemoteUpdateBookableSpaceService(HTTPService()),
|
||||
),
|
||||
),
|
||||
],
|
||||
child: SetupBookableSpacesDialog(
|
||||
editingBookableSpace: space,
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
style: ElevatedButton.styleFrom(
|
||||
padding: EdgeInsets.zero,
|
||||
minimumSize: const Size(45, 30),
|
||||
elevation: 0,
|
||||
),
|
||||
child: SvgPicture.asset(
|
||||
Assets.settings,
|
||||
height: 13,
|
||||
color: ColorsManager.blue1,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
@ -0,0 +1,159 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:syncrow_web/pages/access_management/manage_bookable_spaces/domain/params/bookable_spaces_params.dart';
|
||||
import 'package:syncrow_web/pages/access_management/manage_bookable_spaces/presentation/blocs/bookable_spaces_bloc/bookable_spaces_bloc.dart';
|
||||
import 'package:syncrow_web/utils/color_manager.dart';
|
||||
|
||||
class PaginationButtonsWidget extends StatelessWidget {
|
||||
const PaginationButtonsWidget({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return BlocBuilder<BookableSpacesBloc, BookableSpacesState>(
|
||||
builder: (context, state) {
|
||||
if (state is BookableSpacesLoaded) {
|
||||
final totalPages = state.bookableSpacesList.totalPages;
|
||||
final currentPage = state.bookableSpacesList.page;
|
||||
|
||||
List<Widget> paginationItems = [];
|
||||
|
||||
if (currentPage > 2) {
|
||||
paginationItems.add(
|
||||
_buildArrowButton(
|
||||
label: '«',
|
||||
onTap: () {
|
||||
context.read<BookableSpacesBloc>().add(
|
||||
LoadBookableSpacesEvent(
|
||||
BookableSpacesParam(currentPage: currentPage - 2),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
if (currentPage > 1) {
|
||||
paginationItems.add(
|
||||
_buildArrowButton(
|
||||
label: '<',
|
||||
onTap: () {
|
||||
context.read<BookableSpacesBloc>().add(
|
||||
LoadBookableSpacesEvent(
|
||||
BookableSpacesParam(currentPage: currentPage - 1),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
for (int i = 1; i <= totalPages; i++) {
|
||||
paginationItems.add(
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 4),
|
||||
child: GestureDetector(
|
||||
onTap: () {
|
||||
if (i != currentPage) {
|
||||
context.read<BookableSpacesBloc>().add(
|
||||
LoadBookableSpacesEvent(
|
||||
BookableSpacesParam(currentPage: i),
|
||||
),
|
||||
);
|
||||
}
|
||||
},
|
||||
child: Container(
|
||||
width: 30,
|
||||
height: 30,
|
||||
alignment: Alignment.center,
|
||||
decoration: BoxDecoration(
|
||||
color: i == currentPage
|
||||
? ColorsManager.dialogBlueTitle
|
||||
: ColorsManager.whiteColors,
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
border: Border.all(
|
||||
color: ColorsManager.lightGrayBorderColor,
|
||||
)),
|
||||
child: Text(
|
||||
'$i',
|
||||
style: TextStyle(
|
||||
color: i == currentPage ? Colors.white : Colors.black,
|
||||
fontWeight: i == currentPage
|
||||
? FontWeight.bold
|
||||
: FontWeight.normal,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
if (currentPage < totalPages) {
|
||||
paginationItems.add(
|
||||
_buildArrowButton(
|
||||
label: '>',
|
||||
onTap: () {
|
||||
context.read<BookableSpacesBloc>().add(
|
||||
LoadBookableSpacesEvent(
|
||||
BookableSpacesParam(currentPage: currentPage + 1),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
if (currentPage + 1 < totalPages) {
|
||||
paginationItems.add(
|
||||
_buildArrowButton(
|
||||
label: '»',
|
||||
onTap: () {
|
||||
context.read<BookableSpacesBloc>().add(
|
||||
LoadBookableSpacesEvent(
|
||||
BookableSpacesParam(currentPage: currentPage + 2),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
return Row(
|
||||
mainAxisAlignment: MainAxisAlignment.end,
|
||||
children: paginationItems,
|
||||
);
|
||||
} else {
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildArrowButton(
|
||||
{required String label, required VoidCallback onTap}) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 4),
|
||||
child: GestureDetector(
|
||||
onTap: onTap,
|
||||
child: Container(
|
||||
width: 30,
|
||||
height: 30,
|
||||
alignment: Alignment.center,
|
||||
decoration: BoxDecoration(
|
||||
color: ColorsManager.whiteColors,
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
border: Border.all(
|
||||
color: ColorsManager.lightGrayBorderColor,
|
||||
)),
|
||||
child: Text(
|
||||
label,
|
||||
style: const TextStyle(
|
||||
color: Colors.black,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
@ -0,0 +1,118 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:syncrow_web/pages/access_management/manage_bookable_spaces/domain/models/bookable_space_model.dart';
|
||||
import 'package:syncrow_web/pages/access_management/manage_bookable_spaces/domain/params/bookable_spaces_params.dart';
|
||||
import 'package:syncrow_web/pages/access_management/manage_bookable_spaces/presentation/blocs/bookable_spaces_bloc/bookable_spaces_bloc.dart';
|
||||
import 'package:syncrow_web/pages/access_management/manage_bookable_spaces/presentation/widgets/bookable_space_switch_activation_widget.dart';
|
||||
import 'package:syncrow_web/pages/access_management/manage_bookable_spaces/presentation/widgets/custom_data_table.dart';
|
||||
import 'package:syncrow_web/pages/access_management/manage_bookable_spaces/presentation/widgets/edit_bookable_space_button_widget.dart';
|
||||
|
||||
class TableOfBookableSpacesWidget extends StatelessWidget {
|
||||
const TableOfBookableSpacesWidget({
|
||||
super.key,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return BlocBuilder<BookableSpacesBloc, BookableSpacesState>(
|
||||
builder: (context, state) {
|
||||
if (state is BookableSpacesLoading) {
|
||||
return const Center(child: CircularProgressIndicator());
|
||||
} else if (state is BookableSpacesError) {
|
||||
return Column(mainAxisAlignment: MainAxisAlignment.center, children: [
|
||||
Text(state.error),
|
||||
const SizedBox(
|
||||
height: 5,
|
||||
),
|
||||
ElevatedButton(
|
||||
onPressed: () => context
|
||||
.read<BookableSpacesBloc>()
|
||||
.add(LoadBookableSpacesEvent(
|
||||
BookableSpacesParam(currentPage: 1),
|
||||
)),
|
||||
child: const Text('Try Again'))
|
||||
]);
|
||||
} else if (state is BookableSpacesLoaded) {
|
||||
return CustomDataTable<BookableSpacemodel>(
|
||||
items: state.bookableSpacesList.data,
|
||||
cellsWidgets: (space) => [
|
||||
DataCell(
|
||||
DataCellWidget(
|
||||
title: space.spaceName,
|
||||
),
|
||||
),
|
||||
DataCell(DataCellWidget(
|
||||
title: space.spaceVirtualAddress,
|
||||
)),
|
||||
DataCell(Container(
|
||||
padding: const EdgeInsetsGeometry.only(left: 10),
|
||||
width: 200,
|
||||
child: Wrap(
|
||||
spacing: 4,
|
||||
children: space.spaceConfig!.bookableDays
|
||||
.map(
|
||||
(day) => DataCellWidget(title: day),
|
||||
)
|
||||
.toList(),
|
||||
),
|
||||
)),
|
||||
DataCell(
|
||||
DataCellWidget(
|
||||
title: space.spaceConfig!.bookingStartTime!.format(context),
|
||||
),
|
||||
),
|
||||
DataCell(
|
||||
DataCellWidget(
|
||||
title: space.spaceConfig!.bookingEndTime!.format(context),
|
||||
),
|
||||
),
|
||||
DataCell(
|
||||
DataCellWidget(
|
||||
title: '${space.spaceConfig!.cost} Points',
|
||||
),
|
||||
),
|
||||
DataCell(BookableSpaceSwitchActivationWidget(
|
||||
bookableSpaces: state.bookableSpacesList,
|
||||
space: space,
|
||||
)),
|
||||
DataCell(EditBookableSpaceButtonWidget(
|
||||
space: space,
|
||||
)),
|
||||
],
|
||||
columnsTitles: const [
|
||||
'Space',
|
||||
'Space Virtual Address',
|
||||
'Bookable Days',
|
||||
'Booking Start Time',
|
||||
'Booking End Time',
|
||||
'Cost',
|
||||
'Availability',
|
||||
'Settings',
|
||||
],
|
||||
);
|
||||
} else {
|
||||
return const SizedBox();
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class DataCellWidget extends StatelessWidget {
|
||||
final String title;
|
||||
const DataCellWidget({
|
||||
super.key,
|
||||
required this.title,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Padding(
|
||||
padding: const EdgeInsetsGeometry.only(left: 10),
|
||||
child: Text(
|
||||
title,
|
||||
style: const TextStyle(fontSize: 11),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
@ -0,0 +1,94 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:flutter_svg/svg.dart';
|
||||
import 'package:syncrow_web/pages/access_management/booking_system/presentation/view/widgets/icon_text_button.dart';
|
||||
import 'package:syncrow_web/pages/access_management/manage_bookable_spaces/presentation/blocs/bookable_spaces_bloc/bookable_spaces_bloc.dart';
|
||||
import 'package:syncrow_web/pages/access_management/manage_bookable_spaces/presentation/screens/setup_bookable_spaces_dialog.dart';
|
||||
import 'package:syncrow_web/utils/color_manager.dart';
|
||||
import 'package:syncrow_web/utils/constants/assets.dart';
|
||||
|
||||
class RowOfButtonsTitleWidget extends StatelessWidget {
|
||||
const RowOfButtonsTitleWidget({
|
||||
super.key,
|
||||
required this.pageController,
|
||||
});
|
||||
|
||||
final PageController pageController;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Padding(
|
||||
padding: const EdgeInsetsGeometry.symmetric(vertical: 5),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
Container(
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
offset: Offset.zero,
|
||||
blurRadius: 3,
|
||||
spreadRadius: 0,
|
||||
color: ColorsManager.timePickerColor.withValues(
|
||||
alpha: 0.3,
|
||||
),
|
||||
)
|
||||
],
|
||||
),
|
||||
child: ElevatedButton(
|
||||
style: ElevatedButton.styleFrom(
|
||||
padding: EdgeInsets.zero,
|
||||
minimumSize: const Size(50, 40),
|
||||
elevation: 0,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
),
|
||||
child: SvgPicture.asset(
|
||||
Assets.backButtonIcon,
|
||||
height: 15,
|
||||
),
|
||||
onPressed: () {
|
||||
pageController.jumpToPage(1);
|
||||
}),
|
||||
),
|
||||
const SizedBox(
|
||||
width: 10,
|
||||
),
|
||||
Text(
|
||||
'Manage Bookable Spaces',
|
||||
style: TextStyle(
|
||||
fontSize: 18,
|
||||
color: ColorsManager.vividBlue.withValues(
|
||||
alpha: 0.7,
|
||||
),
|
||||
fontWeight: FontWeight.w700),
|
||||
)
|
||||
],
|
||||
),
|
||||
SvgTextButton(
|
||||
padding: const EdgeInsets.all(10),
|
||||
svgSize: 15,
|
||||
fontSize: 10,
|
||||
fontWeight: FontWeight.bold,
|
||||
svgAsset: Assets.addButtonIcon,
|
||||
label: 'Set Up a Bookable Spaces',
|
||||
onPressed: () {
|
||||
final bloc = context.read<BookableSpacesBloc>();
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (context) => BlocProvider.value(
|
||||
value: bloc,
|
||||
child: SetupBookableSpacesDialog(),
|
||||
),
|
||||
);
|
||||
},
|
||||
)
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
@ -0,0 +1,42 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
import 'package:syncrow_web/pages/access_management/manage_bookable_spaces/presentation/blocs/setup_bookable_spaces_bloc/setup_bookable_spaces_bloc.dart';
|
||||
import 'package:syncrow_web/pages/access_management/manage_bookable_spaces/presentation/blocs/steps_cubit/cubit/steps_cubit.dart';
|
||||
import 'package:syncrow_web/pages/access_management/manage_bookable_spaces/presentation/widgets/buttons_divider_bottom_dialog_widget.dart';
|
||||
|
||||
class NextFirstStepButton extends StatelessWidget {
|
||||
const NextFirstStepButton({
|
||||
super.key,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return BlocBuilder<SetupBookableSpacesBloc, SetupBookableSpacesState>(
|
||||
builder: (context, state) {
|
||||
return switch (state) {
|
||||
SetupBookableSpacesInitial() => ButtonsDividerBottomDialogWidget(
|
||||
title: 'Next',
|
||||
onNextPressed: null,
|
||||
onCancelPressed: () => context.pop(),
|
||||
),
|
||||
AddNonBookableSpaceIntoBookableState(:final bookableSpaces) ||
|
||||
RemoveBookableSpaceIntoNonBookableState(:final bookableSpaces) =>
|
||||
ButtonsDividerBottomDialogWidget(
|
||||
title: 'Next',
|
||||
onNextPressed: bookableSpaces.isEmpty
|
||||
? null
|
||||
: () {
|
||||
context.read<StepsCubit>().goToNextStep();
|
||||
context.read<SetupBookableSpacesBloc>().add(
|
||||
CheckConfigurValidityEvent(),
|
||||
);
|
||||
},
|
||||
onCancelPressed: () => context.pop(),
|
||||
),
|
||||
_ => const SizedBox(),
|
||||
};
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
@ -0,0 +1,121 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:syncrow_web/pages/access_management/manage_bookable_spaces/domain/models/bookable_space_model.dart';
|
||||
import 'package:syncrow_web/pages/access_management/manage_bookable_spaces/presentation/blocs/setup_bookable_spaces_bloc/setup_bookable_spaces_bloc.dart';
|
||||
import 'package:syncrow_web/pages/access_management/manage_bookable_spaces/presentation/blocs/toggle_cubit/toggle_points_switch_cubit.dart';
|
||||
import 'package:syncrow_web/pages/access_management/manage_bookable_spaces/presentation/widgets/search_unbookable_spaces_widget.dart';
|
||||
import 'package:syncrow_web/utils/color_manager.dart';
|
||||
|
||||
class PointsPartWidget extends StatefulWidget {
|
||||
final BookableSpacemodel? editingBookableSpace;
|
||||
final TextEditingController pointsController;
|
||||
|
||||
const PointsPartWidget({
|
||||
super.key,
|
||||
required this.pointsController,
|
||||
this.editingBookableSpace,
|
||||
});
|
||||
|
||||
@override
|
||||
State<PointsPartWidget> createState() => _PointsPartWidgetState();
|
||||
}
|
||||
|
||||
class _PointsPartWidgetState extends State<PointsPartWidget> {
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
|
||||
if (widget.editingBookableSpace != null) {
|
||||
widget.pointsController.text =
|
||||
widget.editingBookableSpace!.spaceConfig!.cost.toString();
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return BlocBuilder<TogglePointsSwitchCubit, TogglePointsSwitchState>(
|
||||
builder: (context, switchState) {
|
||||
final isSwitchOn = switchState is ActivatePointsSwitch;
|
||||
|
||||
return Column(
|
||||
children: [
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
if (isSwitchOn)
|
||||
Text(
|
||||
'* ',
|
||||
style: Theme.of(context)
|
||||
.textTheme
|
||||
.bodyMedium!
|
||||
.copyWith(color: Colors.red),
|
||||
)
|
||||
else
|
||||
const SizedBox(width: 11),
|
||||
const Text('Points/hrs'),
|
||||
],
|
||||
),
|
||||
Transform.scale(
|
||||
scale: 0.7,
|
||||
child: Switch(
|
||||
materialTapTargetSize: MaterialTapTargetSize.shrinkWrap,
|
||||
trackOutlineColor:
|
||||
WidgetStateProperty.all(ColorsManager.whiteColors),
|
||||
activeTrackColor: ColorsManager.dialogBlueTitle,
|
||||
inactiveTrackColor: ColorsManager.lightGrayBorderColor,
|
||||
thumbColor:
|
||||
WidgetStateProperty.all(ColorsManager.whiteColors),
|
||||
value: isSwitchOn,
|
||||
onChanged: (value) {
|
||||
final toggleCubit =
|
||||
context.read<TogglePointsSwitchCubit>();
|
||||
final bloc = context.read<SetupBookableSpacesBloc>();
|
||||
|
||||
final updatedCost = value ? -1 : 0;
|
||||
|
||||
if (value) {
|
||||
toggleCubit.activateSwitch();
|
||||
} else {
|
||||
toggleCubit.unActivateSwitch();
|
||||
widget.pointsController.clear();
|
||||
}
|
||||
bloc.add(ChangeCostEvent(cost: updatedCost));
|
||||
bloc.add(CheckConfigurValidityEvent());
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 5),
|
||||
if (isSwitchOn)
|
||||
SearchUnbookableSpacesWidget(
|
||||
title: 'Ex: 0',
|
||||
topPadding: 0,
|
||||
blur: 1,
|
||||
raduis: 10,
|
||||
height: 34,
|
||||
controller: widget.pointsController,
|
||||
suffix: const SizedBox(),
|
||||
inputFormatters: [FilteringTextInputFormatter.digitsOnly],
|
||||
onChanged: (_) {
|
||||
final updatedCost =
|
||||
int.tryParse(widget.pointsController.text) ?? 0;
|
||||
context
|
||||
.read<SetupBookableSpacesBloc>()
|
||||
.add(ChangeCostEvent(cost: updatedCost));
|
||||
context.read<SetupBookableSpacesBloc>().add(
|
||||
CheckConfigurValidityEvent(),
|
||||
);
|
||||
},
|
||||
)
|
||||
else
|
||||
const SizedBox(),
|
||||
],
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
@ -0,0 +1,77 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
import 'package:syncrow_web/pages/access_management/manage_bookable_spaces/domain/models/bookable_space_model.dart';
|
||||
import 'package:syncrow_web/pages/access_management/manage_bookable_spaces/domain/params/update_bookable_space_param.dart';
|
||||
import 'package:syncrow_web/pages/access_management/manage_bookable_spaces/presentation/blocs/non_bookable_spaces_bloc/non_bookaable_spaces_bloc.dart';
|
||||
import 'package:syncrow_web/pages/access_management/manage_bookable_spaces/presentation/blocs/send_bookable_spaces_bloc/send_bookable_spaces_bloc.dart';
|
||||
import 'package:syncrow_web/pages/access_management/manage_bookable_spaces/presentation/blocs/setup_bookable_spaces_bloc/setup_bookable_spaces_bloc.dart';
|
||||
import 'package:syncrow_web/pages/access_management/manage_bookable_spaces/presentation/blocs/update_bookable_spaces/update_bookable_spaces_bloc.dart';
|
||||
import 'package:syncrow_web/pages/access_management/manage_bookable_spaces/presentation/widgets/buttons_divider_bottom_dialog_widget.dart';
|
||||
|
||||
class SaveSecondStepButton extends StatelessWidget {
|
||||
final TextEditingController pointsController;
|
||||
final bool isEditingMode;
|
||||
final List<BookableSpacemodel> bookableSpaces;
|
||||
|
||||
const SaveSecondStepButton({
|
||||
super.key,
|
||||
required this.pointsController,
|
||||
required this.isEditingMode,
|
||||
required this.bookableSpaces,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return BlocListener<SendBookableSpacesBloc, SendBookableSpacesState>(
|
||||
listener: (context, state) {
|
||||
if (state is SendBookableSpacesSuccess) {
|
||||
context.read<NonBookableSpacesBloc>().add(CallInitStateEvent());
|
||||
} else if (state is SendBookableSpacesError) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(content: Text(state.error)),
|
||||
);
|
||||
}
|
||||
},
|
||||
child: BlocBuilder<SetupBookableSpacesBloc, SetupBookableSpacesState>(
|
||||
builder: (context, state) {
|
||||
return ButtonsDividerBottomDialogWidget(
|
||||
title: 'Save',
|
||||
onNextPressed: state is UnValidSaveButtonState
|
||||
? null
|
||||
: () {
|
||||
if (bookableSpaces.any((e) => e.isValid)) {
|
||||
if (isEditingMode) {
|
||||
callEditLogic(context);
|
||||
} else {
|
||||
context.read<SendBookableSpacesBloc>().add(
|
||||
SendBookableSpacesToApi(
|
||||
selectedBookableSpaces: bookableSpaces,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
},
|
||||
onCancelPressed: () => context.pop(),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void callEditLogic(BuildContext context) {
|
||||
print(bookableSpaces.first.spaceConfig!.cost);
|
||||
if (bookableSpaces.isNotEmpty) {
|
||||
context.read<UpdateBookableSpacesBloc>().add(
|
||||
UpdateBookableSpace(
|
||||
onSuccess: () => context
|
||||
.read<NonBookableSpacesBloc>()
|
||||
.add(CallInitStateEvent()),
|
||||
updatedParam: UpdateBookableSpaceParam.fromBookableModel(
|
||||
bookableSpaces.first,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,79 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_svg/svg.dart';
|
||||
import 'package:syncrow_web/utils/color_manager.dart';
|
||||
import 'package:syncrow_web/utils/constants/assets.dart';
|
||||
|
||||
class SearchUnbookableSpacesWidget extends StatelessWidget {
|
||||
final String title;
|
||||
final Widget? suffix;
|
||||
final double? height;
|
||||
final double? width;
|
||||
final double? blur;
|
||||
final double? raduis;
|
||||
final double? topPadding;
|
||||
final TextEditingController? controller;
|
||||
final List<TextInputFormatter>? inputFormatters;
|
||||
final void Function(String)? onChanged;
|
||||
const SearchUnbookableSpacesWidget({
|
||||
required this.title,
|
||||
this.controller,
|
||||
this.blur,
|
||||
this.onChanged,
|
||||
this.suffix,
|
||||
this.height,
|
||||
this.width,
|
||||
this.topPadding,
|
||||
this.raduis,
|
||||
this.inputFormatters,
|
||||
super.key,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
width: width ?? 480,
|
||||
height: height ?? 40,
|
||||
padding: const EdgeInsets.only(top: 4),
|
||||
decoration: BoxDecoration(
|
||||
color: ColorsManager.whiteColors,
|
||||
borderRadius: BorderRadius.circular(raduis ?? 15),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color:
|
||||
ColorsManager.shadowOfSearchTextfield.withValues(alpha: 0.15),
|
||||
offset: Offset.zero,
|
||||
blurRadius: blur ?? 5,
|
||||
spreadRadius: 0,
|
||||
),
|
||||
],
|
||||
),
|
||||
child: TextField(
|
||||
controller: controller,
|
||||
inputFormatters: inputFormatters,
|
||||
onChanged: onChanged,
|
||||
decoration: InputDecoration(
|
||||
contentPadding: EdgeInsets.symmetric(
|
||||
vertical: topPadding ?? 5,
|
||||
horizontal: 15,
|
||||
),
|
||||
hintText: title,
|
||||
hintStyle: const TextStyle(color: ColorsManager.hintTextGrey),
|
||||
border: InputBorder.none,
|
||||
suffixIcon: Padding(
|
||||
padding: const EdgeInsets.all(10),
|
||||
child: suffix ??
|
||||
SvgPicture.asset(
|
||||
Assets.searchIcon,
|
||||
height: 12,
|
||||
),
|
||||
),
|
||||
),
|
||||
style: const TextStyle(
|
||||
fontSize: 14,
|
||||
color: ColorsManager.hintTextGrey,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
@ -0,0 +1,173 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:syncrow_web/pages/access_management/manage_bookable_spaces/domain/params/non_bookable_spaces_params.dart';
|
||||
import 'package:syncrow_web/pages/access_management/manage_bookable_spaces/presentation/blocs/non_bookable_spaces_bloc/non_bookaable_spaces_bloc.dart';
|
||||
import 'package:syncrow_web/pages/access_management/manage_bookable_spaces/presentation/widgets/search_unbookable_spaces_widget.dart';
|
||||
import 'package:syncrow_web/pages/access_management/manage_bookable_spaces/presentation/widgets/unbookable_list_widget.dart';
|
||||
import 'package:syncrow_web/utils/color_manager.dart';
|
||||
|
||||
class SpacesStepDetailsWidget extends StatefulWidget {
|
||||
const SpacesStepDetailsWidget({
|
||||
super.key,
|
||||
});
|
||||
|
||||
@override
|
||||
State<SpacesStepDetailsWidget> createState() =>
|
||||
_SpacesStepDetailsWidgetState();
|
||||
}
|
||||
|
||||
class _SpacesStepDetailsWidgetState extends State<SpacesStepDetailsWidget> {
|
||||
ScrollController scrollController = ScrollController();
|
||||
int currentPage = 1;
|
||||
String? currentSearchTerm;
|
||||
bool isLoadingMore = false;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
|
||||
scrollController.addListener(() {
|
||||
if (scrollController.position.pixels >=
|
||||
scrollController.position.maxScrollExtent - 100) {
|
||||
final state = context.read<NonBookableSpacesBloc>().state;
|
||||
if (state is NonBookableSpacesLoaded &&
|
||||
state.nonBookableSpaces.hasNext &&
|
||||
!isLoadingMore) {
|
||||
isLoadingMore = true;
|
||||
currentPage++;
|
||||
context.read<NonBookableSpacesBloc>().add(
|
||||
LoadUnBookableSpacesEvent(
|
||||
nonBookableSpacesParams: NonBookableSpacesParams(
|
||||
currentPage: currentPage,
|
||||
searchedWords: currentSearchTerm,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
scrollController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const Text(
|
||||
'Select Space',
|
||||
style: TextStyle(
|
||||
fontWeight: FontWeight.w700,
|
||||
color: ColorsManager.blackColor,
|
||||
),
|
||||
),
|
||||
const SizedBox(
|
||||
height: 20,
|
||||
),
|
||||
Container(
|
||||
width: 450,
|
||||
height: 480,
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
boxShadow: const [
|
||||
BoxShadow(
|
||||
color: ColorsManager.shadowOfDetailsContainer,
|
||||
offset: Offset.zero,
|
||||
blurRadius: 5,
|
||||
),
|
||||
],
|
||||
),
|
||||
child: Column(
|
||||
children: [
|
||||
Container(
|
||||
width: 520,
|
||||
height: 70,
|
||||
padding:
|
||||
const EdgeInsets.symmetric(vertical: 15, horizontal: 20),
|
||||
decoration: const BoxDecoration(
|
||||
color: ColorsManager.circleRolesBackground,
|
||||
borderRadius: BorderRadius.vertical(
|
||||
top: Radius.circular(20),
|
||||
),
|
||||
),
|
||||
child: SearchUnbookableSpacesWidget(
|
||||
title: 'Search',
|
||||
onChanged: (p0) {
|
||||
currentSearchTerm = p0;
|
||||
currentPage = 1;
|
||||
context.read<NonBookableSpacesBloc>().add(
|
||||
LoadUnBookableSpacesEvent(
|
||||
nonBookableSpacesParams: NonBookableSpacesParams(
|
||||
currentPage: currentPage,
|
||||
searchedWords: currentSearchTerm,
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
child:
|
||||
BlocConsumer<NonBookableSpacesBloc, NonBookableSpacesState>(
|
||||
listener: (context, state) {
|
||||
if (state is NonBookableSpacesLoaded) {
|
||||
isLoadingMore = false;
|
||||
}
|
||||
},
|
||||
builder: (context, state) {
|
||||
return switch (state) {
|
||||
NonBookableSpacesError(error: final error) => Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Text(error),
|
||||
const SizedBox(height: 5),
|
||||
ElevatedButton(
|
||||
onPressed: () {
|
||||
context.read<NonBookableSpacesBloc>().add(
|
||||
LoadUnBookableSpacesEvent(
|
||||
nonBookableSpacesParams:
|
||||
NonBookableSpacesParams(
|
||||
currentPage: currentPage,
|
||||
searchedWords: currentSearchTerm,
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
child: const Text('Try Again'),
|
||||
),
|
||||
],
|
||||
),
|
||||
NonBookableSpacesLoading(lastNonBookableSpaces: null) =>
|
||||
const Center(child: CircularProgressIndicator()),
|
||||
NonBookableSpacesLoading(
|
||||
lastNonBookableSpaces: final spaces
|
||||
) =>
|
||||
UnbookableListWidget(
|
||||
scrollController: scrollController,
|
||||
nonBookableSpaces: spaces!,
|
||||
),
|
||||
NonBookableSpacesLoaded(
|
||||
nonBookableSpaces: final spaces
|
||||
) =>
|
||||
UnbookableListWidget(
|
||||
scrollController: scrollController,
|
||||
nonBookableSpaces: spaces,
|
||||
),
|
||||
_ => const SizedBox(),
|
||||
};
|
||||
},
|
||||
),
|
||||
)
|
||||
],
|
||||
),
|
||||
)
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
@ -0,0 +1,51 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:syncrow_web/pages/access_management/manage_bookable_spaces/domain/models/bookable_space_model.dart';
|
||||
import 'package:syncrow_web/pages/access_management/manage_bookable_spaces/presentation/blocs/toggle_cubit/toggle_points_switch_cubit.dart';
|
||||
import 'package:syncrow_web/pages/access_management/manage_bookable_spaces/presentation/widgets/booking_period_widget.dart';
|
||||
import 'package:syncrow_web/pages/access_management/manage_bookable_spaces/presentation/widgets/points_part_widget.dart';
|
||||
import 'package:syncrow_web/pages/access_management/manage_bookable_spaces/presentation/widgets/week_checkbox_title_widget.dart';
|
||||
|
||||
class StepTwoDetailsWidget extends StatelessWidget {
|
||||
final TextEditingController pointsController;
|
||||
final BookableSpacemodel? editingBookableSpace;
|
||||
const StepTwoDetailsWidget({
|
||||
super.key,
|
||||
required this.pointsController,
|
||||
this.editingBookableSpace,
|
||||
});
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return SizedBox(
|
||||
width: 450,
|
||||
height: 480,
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
WeekDaysCheckboxRow(
|
||||
editingBookableSpace: editingBookableSpace,
|
||||
),
|
||||
const SizedBox(
|
||||
height: 30,
|
||||
),
|
||||
BookingPeriodWidget(
|
||||
editingBookableSpace: editingBookableSpace,
|
||||
),
|
||||
const SizedBox(
|
||||
height: 20,
|
||||
),
|
||||
BlocProvider(
|
||||
create: editingBookableSpace == null
|
||||
? (context) => TogglePointsSwitchCubit()..activateSwitch()
|
||||
: editingBookableSpace!.spaceConfig!.cost == 0
|
||||
? (context) => TogglePointsSwitchCubit()..unActivateSwitch()
|
||||
: (context) => TogglePointsSwitchCubit()..activateSwitch(),
|
||||
child: PointsPartWidget(
|
||||
pointsController: pointsController,
|
||||
editingBookableSpace: editingBookableSpace),
|
||||
)
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
@ -0,0 +1,123 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:syncrow_web/pages/access_management/manage_bookable_spaces/presentation/blocs/steps_cubit/cubit/steps_cubit.dart';
|
||||
import 'package:syncrow_web/utils/color_manager.dart';
|
||||
|
||||
class StepperPartWidget extends StatelessWidget {
|
||||
const StepperPartWidget({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
margin: const EdgeInsets.all(20),
|
||||
padding: const EdgeInsetsGeometry.only(left: 20),
|
||||
child: BlocBuilder<StepsCubit, StepsState>(
|
||||
builder: (context, state) {
|
||||
if (state is StepOneState) {
|
||||
return Column(
|
||||
mainAxisAlignment: MainAxisAlignment.start,
|
||||
children: [
|
||||
const SizedBox(
|
||||
height: 10,
|
||||
),
|
||||
const CircleTitleStepperWidget(
|
||||
title: 'Space',
|
||||
),
|
||||
Container(
|
||||
padding: const EdgeInsets.only(left: 3),
|
||||
alignment: Alignment.centerLeft,
|
||||
height: 40,
|
||||
child: const VerticalDivider(
|
||||
width: 8,
|
||||
)),
|
||||
const CircleTitleStepperWidget(
|
||||
title: 'Settings',
|
||||
titleColor: ColorsManager.softGray,
|
||||
circleColor: ColorsManager.whiteColors,
|
||||
borderColor: ColorsManager.textGray,
|
||||
)
|
||||
],
|
||||
);
|
||||
} else if (state is StepTwoState) {
|
||||
return Column(
|
||||
children: [
|
||||
const SizedBox(
|
||||
height: 10,
|
||||
),
|
||||
const CircleTitleStepperWidget(
|
||||
title: 'Space',
|
||||
titleColor: ColorsManager.softGray,
|
||||
cicleIcon: Icon(
|
||||
Icons.check,
|
||||
color: ColorsManager.whiteColors,
|
||||
size: 12,
|
||||
),
|
||||
circleColor: ColorsManager.trueIconGreen,
|
||||
radius: 15,
|
||||
borderColor: ColorsManager.trueIconGreen,
|
||||
),
|
||||
Container(
|
||||
padding: const EdgeInsets.only(left: 3),
|
||||
alignment: Alignment.centerLeft,
|
||||
height: 40,
|
||||
child: const VerticalDivider(
|
||||
width: 8,
|
||||
)),
|
||||
const CircleTitleStepperWidget(
|
||||
title: 'Settings',
|
||||
)
|
||||
],
|
||||
);
|
||||
} else {
|
||||
return const SizedBox();
|
||||
}
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class CircleTitleStepperWidget extends StatelessWidget {
|
||||
final double? radius;
|
||||
final Widget? cicleIcon;
|
||||
final Color? circleColor;
|
||||
final Color? borderColor;
|
||||
final Color? titleColor;
|
||||
final String title;
|
||||
const CircleTitleStepperWidget({
|
||||
super.key,
|
||||
required this.title,
|
||||
this.circleColor,
|
||||
this.borderColor,
|
||||
this.cicleIcon,
|
||||
this.titleColor,
|
||||
this.radius,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Row(
|
||||
children: [
|
||||
Container(
|
||||
width: radius ?? 15,
|
||||
height: radius ?? 15,
|
||||
decoration: BoxDecoration(
|
||||
shape: BoxShape.circle,
|
||||
color: circleColor ?? ColorsManager.blue1,
|
||||
border: Border.all(color: borderColor ?? ColorsManager.blue1)),
|
||||
child: cicleIcon,
|
||||
),
|
||||
const SizedBox(
|
||||
width: 10,
|
||||
),
|
||||
Text(
|
||||
title,
|
||||
style: TextStyle(
|
||||
fontWeight: FontWeight.w700,
|
||||
color: titleColor ?? ColorsManager.blackColor,
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
@ -0,0 +1,80 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
|
||||
import 'package:syncrow_web/pages/access_management/manage_bookable_spaces/presentation/blocs/setup_bookable_spaces_bloc/setup_bookable_spaces_bloc.dart';
|
||||
|
||||
import 'package:syncrow_web/utils/color_manager.dart';
|
||||
|
||||
class TimePickerWidget extends StatefulWidget {
|
||||
final String title;
|
||||
TimePickerWidget({
|
||||
super.key,
|
||||
required this.onTimePicked,
|
||||
required this.title,
|
||||
});
|
||||
late final SetupBookableSpacesBloc setupBookableSpacesBloc;
|
||||
final void Function(TimeOfDay? timePicked) onTimePicked;
|
||||
@override
|
||||
State<TimePickerWidget> createState() => _TimePickerWidgetState();
|
||||
}
|
||||
|
||||
class _TimePickerWidgetState extends State<TimePickerWidget> {
|
||||
TimeOfDay? timePicked;
|
||||
@override
|
||||
void initState() {
|
||||
widget.setupBookableSpacesBloc = context.read<SetupBookableSpacesBloc>();
|
||||
|
||||
super.initState();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return InkWell(
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
onTap: () async {
|
||||
final tempTime = await showTimePicker(
|
||||
context: context,
|
||||
initialTime: TimeOfDay.now(),
|
||||
builder: (context, child) {
|
||||
return Theme(
|
||||
data: ThemeData.light().copyWith(
|
||||
colorScheme: const ColorScheme.light(
|
||||
primary: ColorsManager.primaryColor,
|
||||
onSurface: Colors.black,
|
||||
),
|
||||
),
|
||||
child: child!,
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
if (tempTime == null) return;
|
||||
|
||||
widget.onTimePicked(tempTime);
|
||||
timePicked = tempTime;
|
||||
|
||||
widget.setupBookableSpacesBloc.add(CheckConfigurValidityEvent());
|
||||
|
||||
setState(() {});
|
||||
},
|
||||
child: Container(
|
||||
height: 32,
|
||||
decoration: const BoxDecoration(
|
||||
borderRadius: BorderRadius.only(
|
||||
topRight: Radius.circular(10),
|
||||
bottomRight: Radius.circular(10),
|
||||
),
|
||||
),
|
||||
alignment: Alignment.centerLeft,
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8),
|
||||
child: Text(
|
||||
timePicked == null ? widget.title : timePicked!.format(context),
|
||||
style: TextStyle(
|
||||
color: ColorsManager.blackColor.withValues(alpha: 0.4),
|
||||
fontSize: 12,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
@ -0,0 +1,51 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:syncrow_web/pages/access_management/manage_bookable_spaces/domain/models/bookable_space_model.dart';
|
||||
import 'package:syncrow_web/pages/access_management/manage_bookable_spaces/presentation/blocs/setup_bookable_spaces_bloc/setup_bookable_spaces_bloc.dart';
|
||||
import 'package:syncrow_web/pages/access_management/manage_bookable_spaces/presentation/widgets/check_box_space_widget.dart';
|
||||
import 'package:syncrow_web/pages/space_management_v2/main_module/shared/models/paginated_data_model.dart';
|
||||
|
||||
class UnbookableListWidget extends StatelessWidget {
|
||||
final PaginatedDataModel<BookableSpacemodel> nonBookableSpaces;
|
||||
const UnbookableListWidget({
|
||||
super.key,
|
||||
required this.scrollController,
|
||||
required this.nonBookableSpaces,
|
||||
});
|
||||
|
||||
final ScrollController scrollController;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
width: 490,
|
||||
decoration: const BoxDecoration(
|
||||
color: Colors.white,
|
||||
borderRadius: BorderRadius.vertical(
|
||||
bottom: Radius.circular(20),
|
||||
),
|
||||
),
|
||||
padding: const EdgeInsets.only(top: 10, left: 20, bottom: 5),
|
||||
child: ListView.separated(
|
||||
separatorBuilder: (context, index) => const SizedBox(
|
||||
height: 5,
|
||||
),
|
||||
controller: scrollController,
|
||||
itemCount: nonBookableSpaces.data.length,
|
||||
itemBuilder: (context, index) {
|
||||
if (index < nonBookableSpaces.data.length) {
|
||||
return CheckBoxSpaceWidget(
|
||||
nonBookableSpace: nonBookableSpaces.data[index],
|
||||
|
||||
);
|
||||
} else {
|
||||
return const Padding(
|
||||
padding: EdgeInsets.symmetric(vertical: 10),
|
||||
child: Center(child: CircularProgressIndicator()),
|
||||
);
|
||||
}
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
@ -0,0 +1,107 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:syncrow_web/pages/access_management/manage_bookable_spaces/domain/models/bookable_space_model.dart';
|
||||
import 'package:syncrow_web/pages/access_management/manage_bookable_spaces/presentation/blocs/setup_bookable_spaces_bloc/setup_bookable_spaces_bloc.dart';
|
||||
import 'package:syncrow_web/pages/access_management/manage_bookable_spaces/presentation/widgets/custom_checkbox_widget.dart';
|
||||
|
||||
class WeekDaysCheckboxRow extends StatefulWidget {
|
||||
final BookableSpacemodel? editingBookableSpace;
|
||||
const WeekDaysCheckboxRow({
|
||||
super.key,
|
||||
this.editingBookableSpace,
|
||||
});
|
||||
|
||||
@override
|
||||
State<WeekDaysCheckboxRow> createState() => _WeekDaysCheckboxRowState();
|
||||
}
|
||||
|
||||
class _WeekDaysCheckboxRowState extends State<WeekDaysCheckboxRow> {
|
||||
final Map<String, bool> _daysChecked = {
|
||||
'Mon': false,
|
||||
'Tue': false,
|
||||
'Wed': false,
|
||||
'Thu': false,
|
||||
'Fri': false,
|
||||
'Sat': false,
|
||||
'Sun': false,
|
||||
};
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
|
||||
final existingDays =
|
||||
widget.editingBookableSpace?.spaceConfig?.bookableDays ?? [];
|
||||
|
||||
for (var day in _daysChecked.keys) {
|
||||
if (existingDays.contains(day)) {
|
||||
_daysChecked[day] = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
Text(
|
||||
'* ',
|
||||
style: Theme.of(context)
|
||||
.textTheme
|
||||
.bodyMedium!
|
||||
.copyWith(color: Colors.red),
|
||||
),
|
||||
const Text('Days'),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: _daysChecked.entries.map((entry) {
|
||||
return Expanded(
|
||||
child: Row(
|
||||
children: [
|
||||
CustomCheckboxWidget(
|
||||
outHeight: 16,
|
||||
outWidth: 16,
|
||||
value: entry.value,
|
||||
onChanged: (newValue) {
|
||||
setState(() {
|
||||
_daysChecked[entry.key] = newValue ?? false;
|
||||
});
|
||||
|
||||
final selectedDays = _daysChecked.entries
|
||||
.where((e) => e.value)
|
||||
.map((e) => e.key)
|
||||
.toList();
|
||||
|
||||
context.read<SetupBookableSpacesBloc>().add(
|
||||
AddBookableDaysEvent(bookableDays: selectedDays),
|
||||
);
|
||||
context.read<SetupBookableSpacesBloc>().add(
|
||||
CheckConfigurValidityEvent(),
|
||||
);
|
||||
},
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
Expanded(
|
||||
child: Text(
|
||||
entry.key,
|
||||
style: const TextStyle(
|
||||
fontSize: 13,
|
||||
fontWeight: FontWeight.w400,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}).toList(),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
@ -2,7 +2,9 @@ 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/manage_bookable_spaces/presentation/screens/manage_bookable_spaces_screen.dart';
|
||||
import 'package:syncrow_web/pages/access_management/booking_system/presentation/view/booking_page.dart' hide BookingPage;
|
||||
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';
|
||||
@ -71,9 +73,14 @@ class _AccessManagementPageState extends State<AccessManagementPage>
|
||||
scaffoldBody: PageView(
|
||||
controller: _pageController,
|
||||
physics: const NeverScrollableScrollPhysics(),
|
||||
children: const [
|
||||
AccessOverviewContent(),
|
||||
BookingPage(),
|
||||
children: [
|
||||
const AccessOverviewContent(),
|
||||
BookingPage(
|
||||
pageController: _pageController,
|
||||
),
|
||||
ManageBookableSpacesPage(
|
||||
pageController: _pageController,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
@ -132,6 +132,8 @@ class _DynamicTableState extends State<DynamicTable> {
|
||||
child: SingleChildScrollView(
|
||||
controller: _horizontalScrollController,
|
||||
scrollDirection: Axis.horizontal,
|
||||
physics:
|
||||
widget.isEmpty ? const NeverScrollableScrollPhysics() : null,
|
||||
child: SizedBox(
|
||||
width: _totalTableWidth,
|
||||
child: Column(
|
||||
@ -164,7 +166,6 @@ class _DynamicTableState extends State<DynamicTable> {
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
Expanded(
|
||||
child: widget.isEmpty
|
||||
? _buildEmptyState()
|
||||
@ -265,7 +266,7 @@ class _DynamicTableState extends State<DynamicTable> {
|
||||
),
|
||||
],
|
||||
),
|
||||
SizedBox(height: widget.size.height * 0.5),
|
||||
SizedBox(height: widget.size.height * 0.2),
|
||||
],
|
||||
),
|
||||
);
|
||||
|
@ -53,8 +53,9 @@ class DeviceManagementBloc
|
||||
for (var community in spaceBloc.state.selectedCommunities) {
|
||||
final spacesList =
|
||||
spaceBloc.state.selectedCommunityAndSpaces[community] ?? [];
|
||||
devices.addAll(await DevicesManagementApi()
|
||||
.fetchDevices(projectUuid, spacesId: spacesList));
|
||||
devices.addAll(await DevicesManagementApi().fetchDevices(projectUuid,
|
||||
spacesId: spacesList,
|
||||
communities: spaceBloc.state.selectedCommunities));
|
||||
}
|
||||
}
|
||||
|
||||
@ -158,7 +159,8 @@ class DeviceManagementBloc
|
||||
add(FilterDevices(_getFilterFromIndex(_selectedIndex)));
|
||||
}
|
||||
|
||||
void _onSelectDevice(SelectDevice event, Emitter<DeviceManagementState> emit) {
|
||||
void _onSelectDevice(
|
||||
SelectDevice event, Emitter<DeviceManagementState> emit) {
|
||||
final selectedUuid = event.selectedDevice.uuid;
|
||||
|
||||
if (_selectedDevices.any((device) => device.uuid == selectedUuid)) {
|
||||
@ -254,7 +256,8 @@ class DeviceManagementBloc
|
||||
_onlineCount = _devices.where((device) => device.online == true).length;
|
||||
_offlineCount = _devices.where((device) => device.online == false).length;
|
||||
_lowBatteryCount = _devices
|
||||
.where((device) => device.batteryLevel != null && device.batteryLevel! < 20)
|
||||
.where((device) =>
|
||||
device.batteryLevel != null && device.batteryLevel! < 20)
|
||||
.length;
|
||||
}
|
||||
|
||||
@ -271,7 +274,8 @@ class DeviceManagementBloc
|
||||
}
|
||||
}
|
||||
|
||||
void _onSearchDevices(SearchDevices event, Emitter<DeviceManagementState> emit) {
|
||||
void _onSearchDevices(
|
||||
SearchDevices event, Emitter<DeviceManagementState> emit) {
|
||||
if ((event.community == null || event.community!.isEmpty) &&
|
||||
(event.unitName == null || event.unitName!.isEmpty) &&
|
||||
(event.deviceNameOrProductName == null ||
|
||||
@ -435,8 +439,8 @@ class DeviceManagementBloc
|
||||
final selectedDevices = loaded.selectedDevice?.map((device) {
|
||||
if (device.uuid == event.deviceId) {
|
||||
return device.copyWith(
|
||||
subspace:
|
||||
device.subspace?.copyWith(subspaceName: event.newSubSpaceName));
|
||||
subspace: device.subspace
|
||||
?.copyWith(subspaceName: event.newSubSpaceName));
|
||||
}
|
||||
return device;
|
||||
}).toList();
|
||||
|
@ -24,12 +24,12 @@ class DeviceManagementPage extends StatefulWidget with HelperResponsiveLayout {
|
||||
}
|
||||
|
||||
class _DeviceManagementPageState extends State<DeviceManagementPage> {
|
||||
|
||||
@override
|
||||
@override
|
||||
void initState() {
|
||||
context.read<SpaceTreeBloc>().add(InitialEvent());
|
||||
super.initState();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return MultiBlocProvider(
|
||||
@ -90,7 +90,7 @@ class _DeviceManagementPageState extends State<DeviceManagementPage> {
|
||||
const TriggerSwitchTabsEvent(isRoutineTab: true));
|
||||
},
|
||||
child: Text(
|
||||
'Routines',
|
||||
'Workflow Automation',
|
||||
style: context.textTheme.titleMedium?.copyWith(
|
||||
color: state.routineTab
|
||||
? ColorsManager.whiteColors
|
||||
|
@ -6,6 +6,7 @@ import 'package:syncrow_web/pages/device_managment/garage_door/bloc/garage_door_
|
||||
import 'package:syncrow_web/pages/device_managment/garage_door/helper/garage_door_helper.dart';
|
||||
import 'package:syncrow_web/pages/device_managment/garage_door/models/garage_door_model.dart';
|
||||
import 'package:syncrow_web/pages/device_managment/garage_door/schedule_view/schedule_garage_view.dart';
|
||||
import 'package:syncrow_web/pages/device_managment/schedule_device/schedule_widgets/schedual_view.dart';
|
||||
import 'package:syncrow_web/pages/device_managment/shared/icon_name_status_container.dart';
|
||||
import 'package:syncrow_web/pages/device_managment/shared/table/report_table.dart';
|
||||
import 'package:syncrow_web/pages/device_managment/shared/toggle_widget.dart';
|
||||
@ -94,11 +95,18 @@ class GarageDoorControlView extends StatelessWidget
|
||||
FetchGarageDoorSchedulesEvent(
|
||||
deviceId: deviceId, category: 'doorcontact_state'),
|
||||
);
|
||||
showDialog(
|
||||
|
||||
showDialog<void>(
|
||||
context: context,
|
||||
builder: (ctx) => BlocProvider.value(
|
||||
value: BlocProvider.of<GarageDoorBloc>(context),
|
||||
child: BuildGarageDoorScheduleView(status: status),
|
||||
child: BuildScheduleView(
|
||||
deviceUuid: deviceId,
|
||||
category: 'Timer',
|
||||
code: 'doorcontact_state',
|
||||
countdownCode: 'Timer',
|
||||
deviceType: 'GD',
|
||||
),
|
||||
));
|
||||
},
|
||||
name: 'Scheduling',
|
||||
|
@ -100,6 +100,7 @@ class _DeviceItem extends StatelessWidget {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
|
||||
return DeviceControlsContainer(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
|
@ -287,7 +287,8 @@ class ScheduleBloc extends Bloc<ScheduleEvent, ScheduleState> {
|
||||
try {
|
||||
if (state is ScheduleLoaded) {
|
||||
Status status = Status(code: '', value: '');
|
||||
if (event.deviceType == 'CUR_2') {
|
||||
if (event.deviceType == 'CUR_2' ||
|
||||
event.deviceType == 'GD' ) {
|
||||
status = status.copyWith(
|
||||
code: 'control',
|
||||
value: event.functionOn == true ? 'open' : 'close');
|
||||
|
@ -69,7 +69,7 @@ class CountdownModeButtons extends StatelessWidget {
|
||||
countDownCode: countDownCode),
|
||||
);
|
||||
},
|
||||
backgroundColor: ColorsManager.primaryColorWithOpacity,
|
||||
backgroundColor: ColorsManager.secondaryColor,
|
||||
child: const Text('Save'),
|
||||
),
|
||||
),
|
||||
|
@ -63,7 +63,7 @@ class InchingModeButtons extends StatelessWidget {
|
||||
),
|
||||
);
|
||||
},
|
||||
backgroundColor: ColorsManager.primaryColor,
|
||||
backgroundColor: ColorsManager.secondaryColor,
|
||||
child: const Text('Save'),
|
||||
),
|
||||
),
|
||||
|
@ -31,11 +31,12 @@ class BuildScheduleView extends StatelessWidget {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return BlocProvider(
|
||||
create: (_) => ScheduleBloc(deviceId: deviceUuid,)
|
||||
create: (_) => ScheduleBloc(
|
||||
deviceId: deviceUuid,
|
||||
)
|
||||
..add(ScheduleGetEvent(category: category))
|
||||
..add(ScheduleFetchStatusEvent(
|
||||
deviceId: deviceUuid,
|
||||
countdownCode: countdownCode ?? '')),
|
||||
deviceId: deviceUuid, countdownCode: countdownCode ?? '')),
|
||||
child: Dialog(
|
||||
backgroundColor: Colors.white,
|
||||
insetPadding: const EdgeInsets.all(20),
|
||||
@ -56,7 +57,7 @@ class BuildScheduleView extends StatelessWidget {
|
||||
children: [
|
||||
const ScheduleHeader(),
|
||||
const SizedBox(height: 20),
|
||||
if (deviceType == 'CUR_2')
|
||||
if (deviceType == 'CUR_2' || deviceType == 'GD')
|
||||
const SizedBox()
|
||||
else
|
||||
ScheduleModeSelector(
|
||||
@ -76,8 +77,7 @@ class BuildScheduleView extends StatelessWidget {
|
||||
category: category,
|
||||
time: '',
|
||||
function: Status(
|
||||
code: code.toString(),
|
||||
value: true),
|
||||
code: code.toString(), value: true),
|
||||
days: [],
|
||||
),
|
||||
isEdit: false,
|
||||
@ -96,7 +96,7 @@ class BuildScheduleView extends StatelessWidget {
|
||||
}
|
||||
},
|
||||
),
|
||||
if (deviceType != 'CUR_2')
|
||||
if (deviceType != 'CUR_2'|| deviceType != 'GD')
|
||||
if (state.scheduleMode == ScheduleModes.countdown ||
|
||||
state.scheduleMode == ScheduleModes.inching)
|
||||
CountdownInchingView(
|
||||
|
@ -24,12 +24,13 @@ class ScheduleManagementUI extends StatelessWidget {
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
SizedBox(
|
||||
width: 170,
|
||||
width: 177,
|
||||
height: 40,
|
||||
child: DefaultButton(
|
||||
borderColor: ColorsManager.grayColor.withOpacity(0.5),
|
||||
padding: 2,
|
||||
backgroundColor: ColorsManager.graysColor,
|
||||
borderWidth: 4,
|
||||
borderColor: ColorsManager.neutralGray,
|
||||
padding: 8,
|
||||
backgroundColor: ColorsManager.textFieldGreyColor,
|
||||
borderRadius: 15,
|
||||
onPressed: onAddSchedule,
|
||||
child: Row(
|
||||
|
@ -39,7 +39,7 @@ class ScheduleModeButtons extends StatelessWidget {
|
||||
borderRadius: 8,
|
||||
height: 40,
|
||||
onPressed: onSave,
|
||||
backgroundColor: ColorsManager.primaryColorWithOpacity,
|
||||
backgroundColor: ColorsManager.secondaryColor,
|
||||
child: const Text('Save'),
|
||||
),
|
||||
),
|
||||
|
@ -194,7 +194,7 @@ class _ScheduleTableView extends StatelessWidget {
|
||||
child: Text(_getSelectedDays(
|
||||
ScheduleModel.parseSelectedDays(schedule.days)))),
|
||||
Center(child: Text(formatIsoStringToTime(schedule.time, context))),
|
||||
if (deviceType == 'CUR_2')
|
||||
if (deviceType == 'CUR_2' || deviceType == 'GD')
|
||||
Center(
|
||||
child: Text(schedule.function.value == true ? 'open' : 'close'))
|
||||
else
|
||||
|
@ -23,7 +23,7 @@ class ScheduleDialogHelper {
|
||||
required String deviceType,
|
||||
}) {
|
||||
bool temp;
|
||||
if (deviceType == 'CUR_2') {
|
||||
if (deviceType == 'CUR_2' || deviceType == 'GD') {
|
||||
temp = schedule!.function.value == 'open' ? true : false;
|
||||
} else {
|
||||
temp = schedule!.function.value;
|
||||
@ -116,7 +116,7 @@ class ScheduleDialogHelper {
|
||||
ScheduleModeButtons(
|
||||
onSave: () {
|
||||
dynamic temp;
|
||||
if (deviceType == 'CUR_2') {
|
||||
if (deviceType == 'CUR_2' || deviceType == 'GD') {
|
||||
temp = functionOn! ? 'open' : 'close';
|
||||
} else {
|
||||
temp = functionOn;
|
||||
@ -202,18 +202,23 @@ class ScheduleDialogHelper {
|
||||
),
|
||||
const SizedBox(width: 10),
|
||||
Radio<bool>(
|
||||
activeColor: ColorsManager.secondaryColor,
|
||||
focusColor: ColorsManager.secondaryColor,
|
||||
value: true,
|
||||
groupValue: isOn,
|
||||
onChanged: (val) => onChanged(true),
|
||||
),
|
||||
Text(categor == 'CUR_2' ? 'open' : 'On'),
|
||||
Text(categor == 'CUR_2' || categor == 'GD' ? 'open' : 'On'),
|
||||
const SizedBox(width: 10),
|
||||
Radio<bool>(
|
||||
activeColor: ColorsManager.secondaryColor,
|
||||
focusColor: ColorsManager.secondaryColor,
|
||||
|
||||
value: false,
|
||||
groupValue: isOn,
|
||||
onChanged: (val) => onChanged(false),
|
||||
),
|
||||
Text(categor == 'CUR_2' ? 'close' : 'Off'),
|
||||
Text(categor == 'CUR_2' || categor == 'GD' ? 'close' : 'Off'),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
@ -937,13 +937,17 @@ class RoutineBloc extends Bloc<RoutineEvent, RoutineState> {
|
||||
List<String> spacesList =
|
||||
spaceBloc.state.selectedCommunityAndSpaces[communityId] ?? [];
|
||||
|
||||
devices.addAll(await DevicesManagementApi()
|
||||
.fetchDevices(projectUuid, spacesId: spacesList));
|
||||
devices.addAll(await DevicesManagementApi().fetchDevices(
|
||||
projectUuid,
|
||||
spacesId: spacesList,
|
||||
communities: spaceBloc.state.selectedCommunities,
|
||||
));
|
||||
}
|
||||
} else {
|
||||
devices.addAll(await DevicesManagementApi().fetchDevices(
|
||||
projectUuid,
|
||||
spacesId: [createRoutineBloc.selectedSpaceId],
|
||||
communities: spaceBloc.state.selectedCommunities,
|
||||
));
|
||||
}
|
||||
|
||||
|
@ -0,0 +1,71 @@
|
||||
import 'package:syncrow_web/pages/space_management_v2/modules/communities/domain/models/space_model.dart';
|
||||
import 'package:syncrow_web/pages/space_management_v2/modules/space_details/domain/models/space_details_model.dart';
|
||||
|
||||
abstract final class SpacesRecursiveHelper {
|
||||
const SpacesRecursiveHelper._();
|
||||
|
||||
static List<SpaceModel> recusrivelyUpdate(
|
||||
List<SpaceModel> spaces,
|
||||
SpaceDetailsModel updatedSpace,
|
||||
) {
|
||||
return spaces.map((space) {
|
||||
final isUpdatedSpace = space.uuid == updatedSpace.uuid;
|
||||
if (isUpdatedSpace) {
|
||||
return space.copyWith(
|
||||
spaceName: updatedSpace.spaceName,
|
||||
icon: updatedSpace.icon,
|
||||
);
|
||||
}
|
||||
final hasChildren = space.children.isNotEmpty;
|
||||
if (hasChildren) {
|
||||
return space.copyWith(
|
||||
children: recusrivelyUpdate(space.children, updatedSpace),
|
||||
);
|
||||
}
|
||||
return space;
|
||||
}).toList();
|
||||
}
|
||||
|
||||
static List<SpaceModel> recusrivelyDelete(
|
||||
List<SpaceModel> spaces,
|
||||
String spaceUuid,
|
||||
) {
|
||||
final updatedSpaces = spaces.map((space) {
|
||||
if (space.uuid == spaceUuid) return null;
|
||||
if (space.children.isNotEmpty) {
|
||||
return space.copyWith(
|
||||
children: recusrivelyDelete(space.children, spaceUuid),
|
||||
);
|
||||
}
|
||||
return space;
|
||||
}).toList();
|
||||
final nonNullSpaces = updatedSpaces.whereType<SpaceModel>().toList();
|
||||
return nonNullSpaces;
|
||||
}
|
||||
|
||||
static List<SpaceModel> recursivelyInsert({
|
||||
required List<SpaceModel> spaces,
|
||||
required String parentUuid,
|
||||
required SpaceModel newSpace,
|
||||
}) {
|
||||
return spaces.map((space) {
|
||||
final isParentSpace = space.uuid == parentUuid;
|
||||
if (isParentSpace) {
|
||||
return space.copyWith(
|
||||
children: [...space.children, newSpace],
|
||||
);
|
||||
}
|
||||
final hasChildren = space.children.isNotEmpty;
|
||||
if (hasChildren) {
|
||||
return space.copyWith(
|
||||
children: recursivelyInsert(
|
||||
spaces: space.children,
|
||||
parentUuid: parentUuid,
|
||||
newSpace: newSpace,
|
||||
),
|
||||
);
|
||||
}
|
||||
return space;
|
||||
}).toList();
|
||||
}
|
||||
}
|
@ -5,13 +5,14 @@ import 'package:syncrow_web/utils/color_manager.dart';
|
||||
class SpacesConnectionsArrowPainter extends CustomPainter {
|
||||
final List<SpaceConnectionModel> connections;
|
||||
final Map<String, Offset> positions;
|
||||
final double cardWidth = 150.0;
|
||||
final Map<String, double> cardWidths;
|
||||
final double cardHeight = 90.0;
|
||||
final Set<String> highlightedUuids;
|
||||
|
||||
SpacesConnectionsArrowPainter({
|
||||
required this.connections,
|
||||
required this.positions,
|
||||
required this.cardWidths,
|
||||
required this.highlightedUuids,
|
||||
});
|
||||
|
||||
@ -29,19 +30,30 @@ class SpacesConnectionsArrowPainter extends CustomPainter {
|
||||
|
||||
final from = positions[connection.from];
|
||||
final to = positions[connection.to];
|
||||
final fromWidth = cardWidths[connection.from] ?? 150.0;
|
||||
final toWidth = cardWidths[connection.to] ?? 150.0;
|
||||
|
||||
if (from != null && to != null) {
|
||||
final startPoint =
|
||||
Offset(from.dx + cardWidth / 2, from.dy + cardHeight - 10);
|
||||
final endPoint = Offset(to.dx + cardWidth / 2, to.dy);
|
||||
Offset(from.dx + fromWidth / 2, from.dy + cardHeight - 10);
|
||||
final endPoint = Offset(to.dx + toWidth / 2, to.dy);
|
||||
|
||||
final path = Path()..moveTo(startPoint.dx, startPoint.dy);
|
||||
|
||||
final controlPoint1 = Offset(startPoint.dx, startPoint.dy + 20);
|
||||
final controlPoint2 = Offset(endPoint.dx, endPoint.dy - 60);
|
||||
|
||||
path.cubicTo(controlPoint1.dx, controlPoint1.dy, controlPoint2.dx,
|
||||
controlPoint2.dy, endPoint.dx, endPoint.dy);
|
||||
if ((startPoint.dx - endPoint.dx).abs() < 1.0) {
|
||||
path.lineTo(endPoint.dx, endPoint.dy);
|
||||
} else {
|
||||
final controlPoint1 = Offset(startPoint.dx, startPoint.dy + 100);
|
||||
final controlPoint2 = Offset(endPoint.dx, endPoint.dy - 100);
|
||||
path.cubicTo(
|
||||
controlPoint1.dx,
|
||||
controlPoint1.dy,
|
||||
controlPoint2.dx,
|
||||
controlPoint2.dy,
|
||||
endPoint.dx,
|
||||
endPoint.dy,
|
||||
);
|
||||
}
|
||||
|
||||
canvas.drawPath(path, paint);
|
||||
|
||||
@ -51,7 +63,7 @@ class SpacesConnectionsArrowPainter extends CustomPainter {
|
||||
: ColorsManager.blackColor.withValues(alpha: 0.5)
|
||||
..style = PaintingStyle.fill
|
||||
..blendMode = BlendMode.srcIn;
|
||||
canvas.drawCircle(endPoint, 4, circlePaint);
|
||||
canvas.drawCircle(endPoint, 6, circlePaint);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -40,4 +40,22 @@ class PaginatedDataModel<T> extends Equatable {
|
||||
totalItems,
|
||||
totalPages,
|
||||
];
|
||||
|
||||
PaginatedDataModel<T> copyWith({
|
||||
List<T>? data,
|
||||
int? page,
|
||||
int? size,
|
||||
bool? hasNext,
|
||||
int? totalItems,
|
||||
int? totalPages,
|
||||
}) {
|
||||
return PaginatedDataModel<T>(
|
||||
data: data ?? this.data,
|
||||
page: page ?? this.page,
|
||||
size: size ?? this.size,
|
||||
hasNext: hasNext ?? this.hasNext,
|
||||
totalItems: totalItems ?? this.totalItems,
|
||||
totalPages: totalPages ?? this.totalPages,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -9,8 +9,10 @@ import 'package:syncrow_web/pages/space_management_v2/modules/communities/presen
|
||||
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/reorder_spaces/data/services/remote_reorder_spaces_service.dart';
|
||||
import 'package:syncrow_web/pages/space_management_v2/modules/reorder_spaces/presentation/bloc/reorder_spaces_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/data/services/unique_space_details_spaces_decorator_service.dart';
|
||||
import 'package:syncrow_web/pages/space_management_v2/modules/space_details/presentation/bloc/space_details_bloc.dart';
|
||||
import 'package:syncrow_web/services/api/http_service.dart';
|
||||
import 'package:syncrow_web/utils/theme/responsive_text_theme.dart';
|
||||
@ -25,15 +27,16 @@ class SpaceManagementPage extends StatefulWidget {
|
||||
|
||||
class _SpaceManagementPageState extends State<SpaceManagementPage> {
|
||||
late final CommunitiesBloc communitiesBloc;
|
||||
late final HTTPService _httpService;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
_httpService = HTTPService();
|
||||
communitiesBloc = CommunitiesBloc(
|
||||
communitiesService: DebouncedCommunitiesService(
|
||||
RemoteCommunitiesService(HTTPService()),
|
||||
RemoteCommunitiesService(_httpService),
|
||||
),
|
||||
)..add(const LoadCommunities(LoadCommunitiesParam()));
|
||||
|
||||
super.initState();
|
||||
}
|
||||
|
||||
@ -49,14 +52,19 @@ class _SpaceManagementPageState extends State<SpaceManagementPage> {
|
||||
),
|
||||
BlocProvider(
|
||||
create: (context) => SpaceDetailsBloc(
|
||||
UniqueSubspacesDecorator(
|
||||
RemoteSpaceDetailsService(httpService: HTTPService()),
|
||||
UniqueSpaceDetailsSpacesDecoratorService(
|
||||
RemoteSpaceDetailsService(httpService: _httpService),
|
||||
),
|
||||
),
|
||||
),
|
||||
BlocProvider(
|
||||
create: (context) => ProductsBloc(
|
||||
RemoteProductsService(HTTPService()),
|
||||
RemoteProductsService(_httpService),
|
||||
),
|
||||
),
|
||||
BlocProvider(
|
||||
create: (context) => ReorderSpacesBloc(
|
||||
RemoteReorderSpacesService(_httpService),
|
||||
),
|
||||
),
|
||||
],
|
||||
|
@ -1,5 +1,6 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:syncrow_web/pages/space_management_v2/main_module/helpers/spaces_recursive_helper.dart';
|
||||
import 'package:syncrow_web/pages/space_management_v2/main_module/models/space_connection_model.dart';
|
||||
import 'package:syncrow_web/pages/space_management_v2/main_module/models/space_reorder_data_model.dart';
|
||||
import 'package:syncrow_web/pages/space_management_v2/main_module/painters/spaces_connections_arrow_painter.dart';
|
||||
@ -10,6 +11,8 @@ import 'package:syncrow_web/pages/space_management_v2/modules/communities/domain
|
||||
import 'package:syncrow_web/pages/space_management_v2/modules/communities/domain/models/space_model.dart';
|
||||
import 'package:syncrow_web/pages/space_management_v2/modules/communities/presentation/bloc/communities_bloc.dart';
|
||||
import 'package:syncrow_web/pages/space_management_v2/modules/communities/presentation/communities_tree_selection_bloc/communities_tree_selection_bloc.dart';
|
||||
import 'package:syncrow_web/pages/space_management_v2/modules/reorder_spaces/domain/params/reorder_spaces_param.dart';
|
||||
import 'package:syncrow_web/pages/space_management_v2/modules/reorder_spaces/presentation/bloc/reorder_spaces_bloc.dart';
|
||||
import 'package:syncrow_web/pages/space_management_v2/modules/space_details/presentation/helpers/space_details_dialog_helper.dart';
|
||||
import 'package:syncrow_web/utils/extension/build_context_x.dart';
|
||||
|
||||
@ -30,10 +33,11 @@ class CommunityStructureCanvas extends StatefulWidget {
|
||||
class _CommunityStructureCanvasState extends State<CommunityStructureCanvas>
|
||||
with SingleTickerProviderStateMixin {
|
||||
final Map<String, Offset> _positions = {};
|
||||
final double _cardWidth = 150.0;
|
||||
final Map<String, double> _cardWidths = {};
|
||||
final double _cardHeight = 90.0;
|
||||
final double _horizontalSpacing = 150.0;
|
||||
final double _verticalSpacing = 120.0;
|
||||
static const double _minCardWidth = 150.0;
|
||||
|
||||
late final TransformationController _transformationController;
|
||||
late final AnimationController _animationController;
|
||||
@ -52,6 +56,7 @@ class _CommunityStructureCanvasState extends State<CommunityStructureCanvas>
|
||||
@override
|
||||
void didUpdateWidget(covariant CommunityStructureCanvas oldWidget) {
|
||||
super.didUpdateWidget(oldWidget);
|
||||
if (widget.selectedSpace == null) return;
|
||||
if (widget.selectedSpace?.uuid != oldWidget.selectedSpace?.uuid) {
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
if (mounted) {
|
||||
@ -68,6 +73,34 @@ class _CommunityStructureCanvasState extends State<CommunityStructureCanvas>
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
double _calculateCardWidth(String text) {
|
||||
final style = context.textTheme.bodyLarge?.copyWith(
|
||||
fontWeight: FontWeight.bold,
|
||||
);
|
||||
final textPainter = TextPainter(
|
||||
text: TextSpan(text: text, style: style),
|
||||
maxLines: 1,
|
||||
textDirection: TextDirection.ltr,
|
||||
)..layout();
|
||||
|
||||
const iconWidth = 40.0;
|
||||
const horizontalPadding = 10.0;
|
||||
const contentPadding = 10.0;
|
||||
final calculatedWidth =
|
||||
iconWidth + horizontalPadding + textPainter.width + contentPadding;
|
||||
|
||||
return calculatedWidth.clamp(_minCardWidth, double.infinity);
|
||||
}
|
||||
|
||||
void _calculateAllCardWidths(List<SpaceModel> spaces) {
|
||||
for (final space in spaces) {
|
||||
_cardWidths[space.uuid] = _calculateCardWidth(space.spaceName);
|
||||
if (space.children.isNotEmpty) {
|
||||
_calculateAllCardWidths(space.children);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Set<String> _getAllDescendantUuids(SpaceModel space) {
|
||||
final uuids = <String>{};
|
||||
for (final child in space.children) {
|
||||
@ -102,11 +135,12 @@ class _CommunityStructureCanvasState extends State<CommunityStructureCanvas>
|
||||
final position = _positions[space.uuid];
|
||||
if (position == null) return;
|
||||
|
||||
final cardWidth = _cardWidths[space.uuid] ?? _minCardWidth;
|
||||
const scale = 1;
|
||||
final viewSize = context.size;
|
||||
if (viewSize == null) return;
|
||||
|
||||
final x = -position.dx * scale + (viewSize.width / 2) - (_cardWidth * scale / 2);
|
||||
final x = -position.dx * scale + (viewSize.width / 2) - (cardWidth * scale / 2);
|
||||
final y =
|
||||
-position.dy * scale + (viewSize.height / 2) - (_cardHeight * scale / 2);
|
||||
|
||||
@ -132,6 +166,16 @@ class _CommunityStructureCanvasState extends State<CommunityStructureCanvas>
|
||||
context.read<CommunitiesBloc>().add(
|
||||
CommunitiesUpdateCommunity(newCommunity),
|
||||
);
|
||||
|
||||
context.read<ReorderSpacesBloc>().add(
|
||||
ReorderSpacesEvent(
|
||||
ReorderSpacesParam(
|
||||
communityUuid: widget.community.uuid,
|
||||
parentSpaceUuid: data.parent?.uuid ?? '',
|
||||
spaces: children,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void _onSpaceTapped(SpaceModel? space) {
|
||||
@ -155,13 +199,16 @@ class _CommunityStructureCanvasState extends State<CommunityStructureCanvas>
|
||||
Map<int, double> levelXOffset,
|
||||
) {
|
||||
for (final space in spaces) {
|
||||
final cardWidth = _cardWidths[space.uuid] ?? _minCardWidth;
|
||||
double childSubtreeWidth = 0;
|
||||
if (space.children.isNotEmpty) {
|
||||
_calculateLayout(space.children, depth + 1, levelXOffset);
|
||||
final firstChildPos = _positions[space.children.first.uuid];
|
||||
final lastChildPos = _positions[space.children.last.uuid];
|
||||
if (firstChildPos != null && lastChildPos != null) {
|
||||
childSubtreeWidth = (lastChildPos.dx + _cardWidth) - firstChildPos.dx;
|
||||
final lastChildWidth =
|
||||
_cardWidths[space.children.last.uuid] ?? _minCardWidth;
|
||||
childSubtreeWidth = (lastChildPos.dx + lastChildWidth) - firstChildPos.dx;
|
||||
}
|
||||
}
|
||||
|
||||
@ -170,7 +217,7 @@ class _CommunityStructureCanvasState extends State<CommunityStructureCanvas>
|
||||
|
||||
if (space.children.isNotEmpty) {
|
||||
final firstChildPos = _positions[space.children.first.uuid]!;
|
||||
x = firstChildPos.dx + (childSubtreeWidth - _cardWidth) / 2;
|
||||
x = firstChildPos.dx + (childSubtreeWidth - cardWidth) / 2;
|
||||
} else {
|
||||
x = currentX;
|
||||
}
|
||||
@ -187,7 +234,7 @@ class _CommunityStructureCanvasState extends State<CommunityStructureCanvas>
|
||||
|
||||
final y = depth * (_verticalSpacing + _cardHeight);
|
||||
_positions[space.uuid] = Offset(x, y);
|
||||
levelXOffset[depth] = x + _cardWidth + _horizontalSpacing;
|
||||
levelXOffset[depth] = x + cardWidth + _horizontalSpacing;
|
||||
}
|
||||
}
|
||||
|
||||
@ -202,11 +249,21 @@ class _CommunityStructureCanvasState extends State<CommunityStructureCanvas>
|
||||
|
||||
List<Widget> _buildTreeWidgets() {
|
||||
_positions.clear();
|
||||
_cardWidths.clear();
|
||||
final community = widget.community;
|
||||
|
||||
_calculateAllCardWidths(community.spaces);
|
||||
|
||||
final levelXOffset = <int, double>{};
|
||||
_calculateLayout(community.spaces, 0, levelXOffset);
|
||||
|
||||
const horizontalCanvasPadding = 100.0;
|
||||
final originalPositions = Map.of(_positions);
|
||||
_positions.clear();
|
||||
for (final entry in originalPositions.entries) {
|
||||
_positions[entry.key] = entry.value.translate(horizontalCanvasPadding, 0);
|
||||
}
|
||||
|
||||
final selectedSpace = widget.selectedSpace;
|
||||
final highlightedUuids = <String>{};
|
||||
if (selectedSpace != null) {
|
||||
@ -224,14 +281,14 @@ class _CommunityStructureCanvasState extends State<CommunityStructureCanvas>
|
||||
community: widget.community,
|
||||
);
|
||||
|
||||
final createButtonX = levelXOffset[0] ?? 0.0;
|
||||
final createButtonX = (levelXOffset[0] ?? 0.0) + horizontalCanvasPadding;
|
||||
const createButtonY = 0.0;
|
||||
|
||||
widgets.add(
|
||||
Positioned(
|
||||
left: createButtonX,
|
||||
top: createButtonY,
|
||||
child: CreateSpaceButton(communityUuid: widget.community.uuid),
|
||||
child: CreateSpaceButton(community: widget.community),
|
||||
),
|
||||
);
|
||||
|
||||
@ -240,6 +297,7 @@ class _CommunityStructureCanvasState extends State<CommunityStructureCanvas>
|
||||
painter: SpacesConnectionsArrowPainter(
|
||||
connections: connections,
|
||||
positions: _positions,
|
||||
cardWidths: _cardWidths,
|
||||
highlightedUuids: highlightedUuids,
|
||||
),
|
||||
child: Stack(alignment: AlignmentDirectional.center, children: widgets),
|
||||
@ -255,10 +313,12 @@ class _CommunityStructureCanvasState extends State<CommunityStructureCanvas>
|
||||
CommunityModel? community,
|
||||
SpaceModel? parent,
|
||||
}) {
|
||||
const targetWidth = 40.0;
|
||||
final padding = (_horizontalSpacing - targetWidth) / 2;
|
||||
if (spaces.isNotEmpty) {
|
||||
final firstChildPos = _positions[spaces.first.uuid]!;
|
||||
final targetPos = Offset(
|
||||
firstChildPos.dx - (_horizontalSpacing / 4),
|
||||
firstChildPos.dx - padding - targetWidth,
|
||||
firstChildPos.dy,
|
||||
);
|
||||
widgets.add(_buildDropTarget(parent, community, 0, targetPos));
|
||||
@ -271,6 +331,7 @@ class _CommunityStructureCanvasState extends State<CommunityStructureCanvas>
|
||||
continue;
|
||||
}
|
||||
|
||||
final cardWidth = _cardWidths[space.uuid] ?? _minCardWidth;
|
||||
final isHighlighted = highlightedUuids.contains(space.uuid);
|
||||
final hasNoSelectedSpace = widget.selectedSpace == null;
|
||||
|
||||
@ -278,20 +339,29 @@ class _CommunityStructureCanvasState extends State<CommunityStructureCanvas>
|
||||
buildSpaceContainer: () {
|
||||
return Opacity(
|
||||
opacity: hasNoSelectedSpace || isHighlighted ? 1.0 : 0.5,
|
||||
child: Tooltip(
|
||||
message: space.spaceName,
|
||||
preferBelow: false,
|
||||
child: SpaceCell(
|
||||
onTap: () => _onSpaceTapped(space),
|
||||
icon: space.icon,
|
||||
name: space.spaceName,
|
||||
),
|
||||
child: SpaceCell(
|
||||
onTap: () => _onSpaceTapped(space),
|
||||
icon: space.icon,
|
||||
name: space.spaceName,
|
||||
),
|
||||
);
|
||||
},
|
||||
onTap: () => SpaceDetailsDialogHelper.showCreate(
|
||||
context,
|
||||
communityUuid: widget.community.uuid,
|
||||
parentUuid: space.uuid,
|
||||
onSuccess: (updatedSpaceModel) {
|
||||
final updatedSpaces = SpacesRecursiveHelper.recursivelyInsert(
|
||||
spaces: widget.community.spaces,
|
||||
parentUuid: space.uuid,
|
||||
newSpace: updatedSpaceModel,
|
||||
);
|
||||
context.read<CommunitiesBloc>().add(
|
||||
CommunitiesUpdateCommunity(
|
||||
widget.community.copyWith(spaces: updatedSpaces),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
|
||||
@ -305,7 +375,7 @@ class _CommunityStructureCanvasState extends State<CommunityStructureCanvas>
|
||||
Positioned(
|
||||
left: position.dx,
|
||||
top: position.dy,
|
||||
width: _cardWidth,
|
||||
width: cardWidth,
|
||||
height: _cardHeight,
|
||||
child: Draggable<SpaceReorderDataModel>(
|
||||
data: reorderData,
|
||||
@ -314,7 +384,7 @@ class _CommunityStructureCanvasState extends State<CommunityStructureCanvas>
|
||||
child: Opacity(
|
||||
opacity: 0.2,
|
||||
child: SizedBox(
|
||||
width: _cardWidth,
|
||||
width: cardWidth,
|
||||
height: _cardHeight,
|
||||
child: spaceCard,
|
||||
),
|
||||
@ -330,7 +400,7 @@ class _CommunityStructureCanvasState extends State<CommunityStructureCanvas>
|
||||
);
|
||||
|
||||
final targetPos = Offset(
|
||||
position.dx + _cardWidth + (_horizontalSpacing / 4) - 20,
|
||||
position.dx + cardWidth + padding,
|
||||
position.dy,
|
||||
);
|
||||
widgets.add(_buildDropTarget(parent, community, i + 1, targetPos));
|
||||
@ -365,24 +435,33 @@ class _CommunityStructureCanvasState extends State<CommunityStructureCanvas>
|
||||
child: DragTarget<SpaceReorderDataModel>(
|
||||
builder: (context, candidateData, rejectedData) {
|
||||
if (_draggedData == null) {
|
||||
return const SizedBox();
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
|
||||
final isTargetForDragged = (_draggedData?.parent?.uuid == parent?.uuid &&
|
||||
_draggedData?.community == null) ||
|
||||
(_draggedData?.community?.uuid == community?.uuid &&
|
||||
_draggedData?.parent == null);
|
||||
final children = parent?.children ?? community?.spaces ?? [];
|
||||
final isSameParent = (_draggedData!.parent?.uuid == parent?.uuid &&
|
||||
_draggedData!.community == null) ||
|
||||
(_draggedData!.community?.uuid == community?.uuid &&
|
||||
_draggedData!.parent == null);
|
||||
|
||||
if (!isTargetForDragged) {
|
||||
return const SizedBox();
|
||||
if (!isSameParent) {
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
|
||||
return Container(
|
||||
final oldIndex =
|
||||
children.indexWhere((s) => s.uuid == _draggedData!.space.uuid);
|
||||
if (oldIndex != -1 && (oldIndex == index || oldIndex == index - 1)) {
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
|
||||
return AnimatedContainer(
|
||||
duration: const Duration(milliseconds: 150),
|
||||
width: 40,
|
||||
alignment: Alignment.center,
|
||||
height: _cardHeight,
|
||||
decoration: BoxDecoration(
|
||||
color: context.theme.colorScheme.primary.withValues(
|
||||
alpha: candidateData.isNotEmpty ? 0.7 : 0.3,
|
||||
alpha: candidateData.isNotEmpty ? 0.9 : 0.3,
|
||||
),
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
@ -405,6 +484,9 @@ class _CommunityStructureCanvasState extends State<CommunityStructureCanvas>
|
||||
|
||||
final oldIndex =
|
||||
children.indexWhere((s) => s.uuid == data.data.space.uuid);
|
||||
if (oldIndex == -1) {
|
||||
return true;
|
||||
}
|
||||
if (oldIndex == index || oldIndex == index - 1) {
|
||||
return false;
|
||||
}
|
||||
@ -418,21 +500,21 @@ class _CommunityStructureCanvasState extends State<CommunityStructureCanvas>
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final treeWidgets = _buildTreeWidgets();
|
||||
return InteractiveViewer(
|
||||
transformationController: _transformationController,
|
||||
boundaryMargin: EdgeInsets.symmetric(
|
||||
horizontal: context.screenWidth * 0.3,
|
||||
vertical: context.screenHeight * 0.3,
|
||||
),
|
||||
minScale: 0.5,
|
||||
maxScale: 3.0,
|
||||
constrained: false,
|
||||
child: GestureDetector(
|
||||
onTap: _resetSelectionAndZoom,
|
||||
return GestureDetector(
|
||||
onTap: _resetSelectionAndZoom,
|
||||
child: InteractiveViewer(
|
||||
transformationController: _transformationController,
|
||||
boundaryMargin: EdgeInsets.symmetric(
|
||||
horizontal: context.screenWidth * 0.3,
|
||||
vertical: context.screenHeight * 0.3,
|
||||
),
|
||||
minScale: 0.5,
|
||||
maxScale: 3.0,
|
||||
constrained: false,
|
||||
child: SizedBox(
|
||||
width: context.screenWidth * 5,
|
||||
height: context.screenHeight * 5,
|
||||
child: Stack(children: treeWidgets),
|
||||
child: Stack(clipBehavior: Clip.none, children: treeWidgets),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
@ -2,41 +2,17 @@ import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:flutter_svg/svg.dart';
|
||||
import 'package:syncrow_web/pages/space_management_v2/main_module/shared/helpers/space_management_community_dialog_helper.dart';
|
||||
import 'package:syncrow_web/pages/space_management_v2/main_module/widgets/community_structure_header_action_buttons.dart';
|
||||
import 'package:syncrow_web/pages/space_management_v2/modules/communities/domain/models/space_model.dart';
|
||||
import 'package:syncrow_web/pages/space_management_v2/modules/communities/presentation/bloc/communities_bloc.dart';
|
||||
import 'package:syncrow_web/pages/space_management_v2/main_module/widgets/community_structure_header_action_buttons_composer.dart';
|
||||
import 'package:syncrow_web/pages/space_management_v2/modules/communities/presentation/communities_tree_selection_bloc/communities_tree_selection_bloc.dart';
|
||||
import 'package:syncrow_web/pages/space_management_v2/modules/space_details/domain/models/space_details_model.dart';
|
||||
import 'package:syncrow_web/pages/space_management_v2/modules/space_details/presentation/helpers/space_details_dialog_helper.dart';
|
||||
import 'package:syncrow_web/utils/color_manager.dart';
|
||||
import 'package:syncrow_web/utils/constants/assets.dart';
|
||||
import 'package:syncrow_web/utils/extension/build_context_x.dart';
|
||||
|
||||
class CommunityStructureHeader extends StatelessWidget {
|
||||
const CommunityStructureHeader({super.key});
|
||||
|
||||
List<SpaceModel> _updateRecursive(
|
||||
List<SpaceModel> spaces,
|
||||
SpaceDetailsModel updatedSpace,
|
||||
) {
|
||||
return spaces.map((space) {
|
||||
if (space.uuid == updatedSpace.uuid) {
|
||||
return space.copyWith(
|
||||
spaceName: updatedSpace.spaceName,
|
||||
icon: updatedSpace.icon,
|
||||
);
|
||||
}
|
||||
if (space.children.isNotEmpty) {
|
||||
return space.copyWith(
|
||||
children: _updateRecursive(space.children, updatedSpace),
|
||||
);
|
||||
}
|
||||
return space;
|
||||
}).toList();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme = Theme.of(context);
|
||||
final screenWidth = MediaQuery.of(context).size.width;
|
||||
return Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 16.0),
|
||||
@ -44,9 +20,9 @@ class CommunityStructureHeader extends StatelessWidget {
|
||||
color: ColorsManager.whiteColors,
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: ColorsManager.shadowBlackColor,
|
||||
blurRadius: 8,
|
||||
offset: const Offset(0, 4),
|
||||
color: ColorsManager.shadowBlackColor.withValues(alpha: 0.1),
|
||||
blurRadius: 20,
|
||||
offset: const Offset(0, 1),
|
||||
),
|
||||
],
|
||||
),
|
||||
@ -57,7 +33,7 @@ class CommunityStructureHeader extends StatelessWidget {
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Expanded(
|
||||
child: _buildCommunityInfo(context, theme, screenWidth),
|
||||
child: _buildCommunityInfo(context, screenWidth),
|
||||
),
|
||||
const SizedBox(width: 16),
|
||||
],
|
||||
@ -67,8 +43,7 @@ class CommunityStructureHeader extends StatelessWidget {
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildCommunityInfo(
|
||||
BuildContext context, ThemeData theme, double screenWidth) {
|
||||
Widget _buildCommunityInfo(BuildContext context, double screenWidth) {
|
||||
final selectedCommunity =
|
||||
context.watch<CommunitiesTreeSelectionBloc>().state.selectedCommunity;
|
||||
final selectedSpace =
|
||||
@ -78,7 +53,7 @@ class CommunityStructureHeader extends StatelessWidget {
|
||||
children: [
|
||||
Text(
|
||||
'Community Structure',
|
||||
style: theme.textTheme.headlineLarge?.copyWith(
|
||||
style: context.textTheme.headlineLarge?.copyWith(
|
||||
color: ColorsManager.blackColor,
|
||||
),
|
||||
),
|
||||
@ -91,7 +66,7 @@ class CommunityStructureHeader extends StatelessWidget {
|
||||
Flexible(
|
||||
child: SelectableText(
|
||||
selectedCommunity.name,
|
||||
style: theme.textTheme.bodyLarge?.copyWith(
|
||||
style: context.textTheme.bodyLarge?.copyWith(
|
||||
color: ColorsManager.blackColor,
|
||||
),
|
||||
maxLines: 1,
|
||||
@ -115,27 +90,8 @@ class CommunityStructureHeader extends StatelessWidget {
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
CommunityStructureHeaderActionButtons(
|
||||
onDelete: (space) {},
|
||||
onDuplicate: (space) {},
|
||||
onEdit: (space) => SpaceDetailsDialogHelper.showEdit(
|
||||
context,
|
||||
spaceModel: selectedSpace!,
|
||||
communityUuid: selectedCommunity.uuid,
|
||||
onSuccess: (updatedSpaceDetails) {
|
||||
final communitiesBloc = context.read<CommunitiesBloc>();
|
||||
final updatedSpaces = _updateRecursive(
|
||||
selectedCommunity.spaces,
|
||||
updatedSpaceDetails,
|
||||
);
|
||||
|
||||
final community = selectedCommunity.copyWith(
|
||||
spaces: updatedSpaces,
|
||||
);
|
||||
|
||||
communitiesBloc.add(CommunitiesUpdateCommunity(community));
|
||||
},
|
||||
),
|
||||
CommunityStructureHeaderActionButtonsComposer(
|
||||
selectedCommunity: selectedCommunity,
|
||||
selectedSpace: selectedSpace,
|
||||
),
|
||||
],
|
||||
|
@ -19,27 +19,27 @@ class CommunityStructureHeaderActionButtons extends StatelessWidget {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
if (selectedSpace == null) return const SizedBox.shrink();
|
||||
|
||||
return Wrap(
|
||||
alignment: WrapAlignment.end,
|
||||
spacing: 10,
|
||||
children: [
|
||||
if (selectedSpace != null) ...[
|
||||
CommunityStructureHeaderButton(
|
||||
label: 'Edit',
|
||||
svgAsset: Assets.editSpace,
|
||||
onPressed: () => onEdit(selectedSpace!),
|
||||
),
|
||||
CommunityStructureHeaderButton(
|
||||
label: 'Duplicate',
|
||||
svgAsset: Assets.duplicate,
|
||||
onPressed: () => onDuplicate(selectedSpace!),
|
||||
),
|
||||
CommunityStructureHeaderButton(
|
||||
label: 'Delete',
|
||||
svgAsset: Assets.spaceDelete,
|
||||
onPressed: () => onDelete(selectedSpace!),
|
||||
),
|
||||
],
|
||||
CommunityStructureHeaderButton(
|
||||
label: 'Edit',
|
||||
svgAsset: Assets.editSpace,
|
||||
onPressed: () => onEdit(selectedSpace!),
|
||||
),
|
||||
CommunityStructureHeaderButton(
|
||||
label: 'Duplicate',
|
||||
svgAsset: Assets.duplicate,
|
||||
onPressed: () => onDuplicate(selectedSpace!),
|
||||
),
|
||||
CommunityStructureHeaderButton(
|
||||
label: 'Delete',
|
||||
svgAsset: Assets.spaceDelete,
|
||||
onPressed: () => onDelete(selectedSpace!),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
@ -0,0 +1,69 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:syncrow_web/pages/space_management_v2/main_module/helpers/spaces_recursive_helper.dart';
|
||||
import 'package:syncrow_web/pages/space_management_v2/main_module/widgets/community_structure_header_action_buttons.dart';
|
||||
import 'package:syncrow_web/pages/space_management_v2/modules/communities/domain/models/community_model.dart';
|
||||
import 'package:syncrow_web/pages/space_management_v2/modules/communities/domain/models/space_model.dart';
|
||||
import 'package:syncrow_web/pages/space_management_v2/modules/communities/presentation/bloc/communities_bloc.dart';
|
||||
import 'package:syncrow_web/pages/space_management_v2/modules/communities/presentation/communities_tree_selection_bloc/communities_tree_selection_bloc.dart';
|
||||
import 'package:syncrow_web/pages/space_management_v2/modules/delete_space/presentation/widgets/delete_space_dialog.dart';
|
||||
import 'package:syncrow_web/pages/space_management_v2/modules/space_details/presentation/helpers/space_details_dialog_helper.dart';
|
||||
|
||||
class CommunityStructureHeaderActionButtonsComposer extends StatelessWidget {
|
||||
const CommunityStructureHeaderActionButtonsComposer({
|
||||
required this.selectedCommunity,
|
||||
required this.selectedSpace,
|
||||
super.key,
|
||||
});
|
||||
final CommunityModel selectedCommunity;
|
||||
final SpaceModel? selectedSpace;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return CommunityStructureHeaderActionButtons(
|
||||
onDelete: (space) => showDialog<void>(
|
||||
context: context,
|
||||
barrierDismissible: false,
|
||||
builder: (_) => DeleteSpaceDialog(
|
||||
space: space,
|
||||
community: selectedCommunity,
|
||||
onSuccess: () {
|
||||
final updatedSpaces = SpacesRecursiveHelper.recusrivelyDelete(
|
||||
selectedCommunity.spaces,
|
||||
space.uuid,
|
||||
);
|
||||
final community = selectedCommunity.copyWith(
|
||||
spaces: updatedSpaces,
|
||||
);
|
||||
context.read<CommunitiesBloc>().add(
|
||||
CommunitiesUpdateCommunity(community),
|
||||
);
|
||||
context.read<CommunitiesTreeSelectionBloc>().add(
|
||||
SelectCommunityEvent(community: selectedCommunity),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
onDuplicate: (space) {},
|
||||
onEdit: (space) => SpaceDetailsDialogHelper.showEdit(
|
||||
context,
|
||||
spaceModel: selectedSpace!,
|
||||
communityUuid: selectedCommunity.uuid,
|
||||
onSuccess: (updatedSpaceDetails) {
|
||||
final communitiesBloc = context.read<CommunitiesBloc>();
|
||||
final updatedSpaces = SpacesRecursiveHelper.recusrivelyUpdate(
|
||||
selectedCommunity.spaces,
|
||||
updatedSpaceDetails,
|
||||
);
|
||||
|
||||
final community = selectedCommunity.copyWith(
|
||||
spaces: updatedSpaces,
|
||||
);
|
||||
|
||||
communitiesBloc.add(CommunitiesUpdateCommunity(community));
|
||||
},
|
||||
),
|
||||
selectedSpace: selectedSpace,
|
||||
);
|
||||
}
|
||||
}
|
@ -1,14 +1,18 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:syncrow_web/pages/space_management_v2/modules/communities/domain/models/community_model.dart';
|
||||
import 'package:syncrow_web/pages/space_management_v2/modules/communities/presentation/bloc/communities_bloc.dart';
|
||||
import 'package:syncrow_web/pages/space_management_v2/modules/communities/presentation/communities_tree_selection_bloc/communities_tree_selection_bloc.dart';
|
||||
import 'package:syncrow_web/pages/space_management_v2/modules/space_details/presentation/helpers/space_details_dialog_helper.dart';
|
||||
import 'package:syncrow_web/utils/color_manager.dart';
|
||||
|
||||
class CreateSpaceButton extends StatefulWidget {
|
||||
const CreateSpaceButton({
|
||||
required this.communityUuid,
|
||||
required this.community,
|
||||
super.key,
|
||||
});
|
||||
|
||||
final String communityUuid;
|
||||
final CommunityModel community;
|
||||
|
||||
@override
|
||||
State<CreateSpaceButton> createState() => _CreateSpaceButtonState();
|
||||
@ -25,7 +29,21 @@ class _CreateSpaceButtonState extends State<CreateSpaceButton> {
|
||||
child: InkWell(
|
||||
onTap: () => SpaceDetailsDialogHelper.showCreate(
|
||||
context,
|
||||
communityUuid: widget.communityUuid,
|
||||
communityUuid: widget.community.uuid,
|
||||
onSuccess: (updatedSpaceModel) {
|
||||
final newCommunity = widget.community.copyWith(
|
||||
spaces: [...widget.community.spaces, updatedSpaceModel],
|
||||
);
|
||||
context.read<CommunitiesBloc>().add(
|
||||
CommunitiesUpdateCommunity(newCommunity),
|
||||
);
|
||||
context.read<CommunitiesTreeSelectionBloc>().add(
|
||||
SelectSpaceEvent(
|
||||
space: updatedSpaceModel,
|
||||
community: newCommunity,
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
child: MouseRegion(
|
||||
onEnter: (_) => setState(() => _isHovered = true),
|
||||
|
@ -2,31 +2,22 @@ import 'package:flutter/material.dart';
|
||||
import 'package:syncrow_web/utils/color_manager.dart';
|
||||
|
||||
class PlusButtonWidget extends StatelessWidget {
|
||||
final Offset offset;
|
||||
final void Function() onButtonTap;
|
||||
final void Function() onTap;
|
||||
|
||||
const PlusButtonWidget({
|
||||
required this.onTap,
|
||||
super.key,
|
||||
required this.offset,
|
||||
required this.onButtonTap,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return GestureDetector(
|
||||
onTap: onButtonTap,
|
||||
child: Container(
|
||||
width: 30,
|
||||
height: 30,
|
||||
decoration: const BoxDecoration(
|
||||
color: ColorsManager.spaceColor,
|
||||
shape: BoxShape.circle,
|
||||
),
|
||||
child: const Icon(
|
||||
Icons.add,
|
||||
color: ColorsManager.whiteColors,
|
||||
size: 20,
|
||||
),
|
||||
return IconButton.filled(
|
||||
onPressed: onTap,
|
||||
style: IconButton.styleFrom(backgroundColor: ColorsManager.spaceColor),
|
||||
icon: const Icon(
|
||||
Icons.add,
|
||||
color: ColorsManager.whiteColors,
|
||||
size: 20,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user