Compare commits

..

38 Commits

Author SHA1 Message Date
0cb2875672 fix UI to fit with Figma 2025-07-17 17:41:22 +03:00
04fcf0a140 break down Massive widgets into smaller ones 2025-07-17 15:03:22 +03:00
9396f37fea refactor code 2025-07-17 14:51:07 +03:00
0e4d37ccac break down nonBokable Bloc into two blocs 2025-07-17 14:50:29 +03:00
9d130139f7 return true refactor code 2025-07-16 12:17:09 +03:00
bee4e05404 delete unused route 2025-07-16 12:10:11 +03:00
6db60a2a97 dont use context in async block 2025-07-16 11:31:54 +03:00
7f9f39811b use switch state instead of if else 2025-07-16 11:26:14 +03:00
739b491bd8 debouncer Note 2025-07-16 11:21:32 +03:00
db157f30c5 first notes requests 2025-07-16 10:35:16 +03:00
6b8827f4d9 fixes after merge 2025-07-15 15:37:08 +03:00
e8c36f5af6 fix after merge 2025-07-15 15:36:20 +03:00
762dade195 Merge branch 'dev' of https://github.com/SyncrowIOT/web into Implement-Spaces-Table-Empty-Filled-Failure-states-bookable-spaces 2025-07-15 15:35:31 +03:00
f5def4b4d7 to open Request 2025-07-15 15:26:37 +03:00
ab326fa820 delete dummy files 2025-07-15 15:18:14 +03:00
e9b4d35f97 add settings and edit feature to bookable spaces 2025-07-15 15:17:43 +03:00
a9895f5462 fix checkbox issue 2025-07-11 10:27:18 +03:00
6e4f0c3c0c edit time picker as in figma 2025-07-10 15:52:52 +03:00
bbf2891804 refactor code 2025-07-10 15:52:38 +03:00
aab2b4a52a insert after update and refactor 2025-07-10 15:52:15 +03:00
d58da9644f add toggling for points 2025-07-10 15:51:44 +03:00
df46a5b905 update spaces remote files 2025-07-10 15:51:17 +03:00
a1fa049a05 no need for dummy data 2025-07-10 15:50:56 +03:00
494a000590 update bookable space logic 2025-07-10 15:50:24 +03:00
b5d72b2a2a refactor code 2025-07-10 15:49:30 +03:00
55a73eee7f we can switch pages in access managment 2025-07-10 15:48:13 +03:00
b128618bfd clean the code for save and next buttons and enhance UI 2025-07-09 15:11:27 +03:00
42c410d982 use TimeOfDay instead of String 2025-07-07 17:07:38 +03:00
368b1be3c0 fix conditions for start and end time for reservation 2025-07-07 17:07:23 +03:00
c13119a4e8 migrate datatable2 package 2025-07-07 15:41:02 +03:00
35e9b606b2 use param to send Update Api for unbookable to be bookable 2025-07-07 15:40:49 +03:00
387586f6f7 unused commnet 2025-07-07 15:40:20 +03:00
7cf4d0b5a9 add to route and related endpoints and color and assets 2025-07-07 15:40:04 +03:00
e4a27b5651 build services for them 2025-07-07 15:39:19 +03:00
f89660a9ff build modeling and params 2025-07-07 15:38:52 +03:00
1a3dc60bd2 build blocs for bookable and nonBookable spaces 2025-07-07 15:38:18 +03:00
201348a9bf build dialog with steps view 2025-07-07 15:37:38 +03:00
e2d4e48875 build main screens with its widgets for bookableScreen 2025-07-07 15:36:35 +03:00
132 changed files with 3771 additions and 1619 deletions

View File

@ -0,0 +1,10 @@
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<g clip-path="url(#clip0_9795_9381)">
<path d="M9.21875 13.5149V10.7805H6.48438C6.05286 10.7805 5.70312 10.4308 5.70312 9.99924C5.70312 9.56787 6.05286 9.21799 6.48438 9.21799H9.21875V6.48361C9.21875 6.05225 9.56848 5.70236 10 5.70236C10.4315 5.70236 10.7812 6.05225 10.7812 6.48361V9.21799H13.5156C13.9471 9.21799 14.2969 9.56787 14.2969 9.99924C14.2969 10.4308 13.9471 10.7805 13.5156 10.7805H10.7812V13.5149C10.7812 13.9464 10.4315 14.2961 10 14.2961C9.56848 14.2961 9.21875 13.9464 9.21875 13.5149ZM17.0711 2.92892C15.1823 1.04019 12.6711 0 10 0C7.32895 0 4.81766 1.04019 2.92892 2.92892C1.04019 4.81766 0 7.32895 0 10C0 12.6711 1.04019 15.1823 2.92892 17.0711C4.81766 18.9598 7.32895 20 10 20C11.8286 20 13.6179 19.5016 15.1743 18.5588C15.5434 18.3353 15.6613 17.8549 15.4378 17.486C15.2142 17.1169 14.7337 16.9989 14.3648 17.2224C13.0525 18.0173 11.5431 18.4375 10 18.4375C5.3476 18.4375 1.5625 14.6524 1.5625 10C1.5625 5.3476 5.3476 1.5625 10 1.5625C14.6524 1.5625 18.4375 5.3476 18.4375 10C18.4375 11.6637 17.9428 13.2829 17.0068 14.6831C16.767 15.0417 16.8634 15.5269 17.2221 15.7668C17.5807 16.0065 18.0659 15.91 18.3058 15.5515C19.4141 13.8936 20 11.9739 20 10C20 7.32895 18.9598 4.81766 17.0711 2.92892Z" fill="#023DFE" fill-opacity="0.7"/>
</g>
<defs>
<clipPath id="clip0_9795_9381">
<rect width="20" height="20" fill="white"/>
</clipPath>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 1.4 KiB

View File

@ -0,0 +1,4 @@
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M10 20C15.514 20 19.9998 15.514 19.9998 9.99995C19.9998 4.48604 15.514 0 10 0C4.48613 0 0.000183105 4.48604 0.000183105 9.99995C0.000183105 15.514 4.48613 20 10 20ZM10 1.36892C14.7591 1.36892 18.6309 5.24077 18.631 9.99995C18.631 14.7591 14.7592 18.631 10 18.6311C5.24095 18.631 1.36919 14.7591 1.36919 9.99986C1.36919 5.24086 5.24095 1.36892 10 1.36892Z" fill="#023DFE" fill-opacity="0.7"/>
<path d="M8.65713 14.2828C8.92444 14.55 9.35784 14.5499 9.62505 14.2828C9.89245 14.0154 9.89245 13.5821 9.62496 13.3147L6.99481 10.6846L14.6112 10.6839C14.9892 10.6838 15.2956 10.3775 15.2956 9.99926C15.2955 9.62126 14.9891 9.31499 14.6111 9.31499L6.99444 9.31572L9.62523 6.68511C9.89254 6.41781 9.89254 5.98432 9.62523 5.7171C9.49154 5.5835 9.3164 5.5166 9.14118 5.5166C8.96605 5.5166 8.79092 5.5835 8.65722 5.71701L4.85811 9.51604C4.7297 9.64435 4.65761 9.81838 4.65761 9.99999C4.6577 10.1816 4.7298 10.3555 4.8582 10.4841L8.65713 14.2828Z" fill="#023DFE" fill-opacity="0.7"/>
</svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

View File

@ -0,0 +1,4 @@
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M9.9999 0C4.48595 0 0 4.48586 0 9.99971C0 15.514 4.48595 20 9.9999 20C15.5138 20 19.9996 15.5139 19.9996 9.99971C19.9996 4.48586 15.5138 0 9.9999 0ZM9.9999 18.5665C5.27638 18.5665 1.43349 14.7234 1.43349 9.99971C1.43349 5.27628 5.27638 1.43349 9.9999 1.43349C14.7233 1.43349 18.5661 5.27628 18.5661 9.99971C18.5661 14.7234 14.7233 18.5665 9.9999 18.5665Z" fill="#D5D5D5"/>
<path d="M15.1416 9.83211H10.4423V4.69526C10.4423 4.29943 10.1215 3.97852 9.72553 3.97852C9.3297 3.97852 9.00879 4.29943 9.00879 4.69526V10.5489C9.00879 10.9447 9.3297 11.2656 9.72553 11.2656H15.1416C15.5376 11.2656 15.8584 10.9447 15.8584 10.5489C15.8584 10.153 15.5375 9.83211 15.1416 9.83211Z" fill="#D5D5D5"/>
</svg>

After

Width:  |  Height:  |  Size: 799 B

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 18 KiB

View File

@ -1,5 +0,0 @@
<svg width="35" height="35" viewBox="0 0 35 35" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M17.5 34.9999C27.1649 34.9999 34.9999 27.1649 34.9999 17.4999C34.9999 7.83499 27.1649 0 17.5 0C7.83499 0 0 7.83499 0 17.5C0 27.1651 7.83499 34.9999 17.5 34.9999Z" fill="#FF6465"/>
<path opacity="0.1" d="M4.70804 17.5C4.70804 8.63343 11.3024 1.30805 19.854 0.158115C19.0839 0.0545507 18.2984 0 17.5 0C7.835 0 0 7.835 0 17.5C0 27.1651 7.83499 35 17.4999 35C18.2983 35 19.0839 34.9455 19.8539 34.8419C11.3024 33.6919 4.70804 26.3665 4.70804 17.5Z" fill="black"/>
<path d="M21.4229 17.5003L26.0301 12.8931C26.365 12.5582 26.365 12.0152 26.0301 11.6804L23.3197 8.96992C22.9848 8.63503 22.4418 8.63503 22.107 8.96992L17.4997 13.5772L12.8924 8.96992C12.5576 8.63503 12.0146 8.63503 11.6798 8.96992L8.96931 11.6804C8.63442 12.0153 8.63442 12.5582 8.96931 12.8931L13.5766 17.5003L8.96931 22.1076C8.63442 22.4425 8.63442 22.9855 8.96931 23.3204L11.6798 26.0308C12.0146 26.3657 12.5576 26.3657 12.8924 26.0308L17.4997 21.4235L22.1071 26.0308C22.442 26.3657 22.9849 26.3657 23.3198 26.0308L26.0302 23.3204C26.3651 22.9855 26.3651 22.4425 26.0302 22.1076L21.4229 17.5003Z" fill="white"/>
</svg>

Before

Width:  |  Height:  |  Size: 1.2 KiB

View File

@ -1,9 +0,0 @@
<svg width="40" height="40" viewBox="0 0 40 40" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M38.0142 39.2553L35.3682 40H20.9308H19.9999H19.0691H1.24111C0.555643 40 0 39.4444 0 38.7589V1.24111C0 0.555643 0.555643 0 1.24111 0H19.0682H20.1226H20.9543H35.4255L38.2625 1.24111C38.9479 1.24111 39.5036 1.79675 39.5036 2.48221L39.2553 38.0142C39.2553 38.6997 38.6997 39.2553 38.0142 39.2553Z" fill="#E9E9E9"/>
<path d="M38.7585 0H35.0352C35.7206 0 36.2763 0.555643 36.2763 1.24111V38.7589C36.2763 39.4444 35.7206 40 35.0352 40H38.7585C39.4439 40 39.9996 39.4444 39.9996 38.7589V1.24111C39.9996 0.555643 39.4439 0 38.7585 0Z" fill="#D1D1D1"/>
<path opacity="0.6" d="M12.0283 31.8319V33.3212C12.0283 34.0067 11.6086 34.5623 11.0908 34.5623H6.96582C6.44804 34.5623 6.02832 34.0067 6.02832 33.3212V31.8319C6.02832 31.1465 6.44804 30.5908 6.96582 30.5908H11.0908C11.6086 30.5908 12.0283 31.1465 12.0283 31.8319Z" fill="#023DFE" fill-opacity="0.5"/>
<path opacity="0.6" d="M12.0283 7.24109V8.73042C12.0283 9.41588 11.6086 9.97153 11.0908 9.97153H6.96582C6.44804 9.97153 6.02832 9.41588 6.02832 8.73042V7.24109C6.02832 6.55563 6.44804 5.99998 6.96582 5.99998H11.0908C11.6086 5.99998 12.0283 6.55563 12.0283 7.24109Z" fill="#023DFE" fill-opacity="0.5"/>
<path opacity="0.6" d="M26.0283 31.8319V33.3212C26.0283 34.0067 26.448 34.5623 26.9658 34.5623H31.0908C31.6086 34.5623 32.0283 34.0067 32.0283 33.3212V31.8319C32.0283 31.1465 31.6086 30.5908 31.0908 30.5908H26.9658C26.448 30.5908 26.0283 31.1465 26.0283 31.8319Z" fill="#023DFE" fill-opacity="0.5"/>
<path opacity="0.6" d="M26.0283 7.24109V8.73042C26.0283 9.41588 26.448 9.97153 26.9658 9.97153H31.0908C31.6086 9.97153 32.0283 9.41588 32.0283 8.73042V7.24109C32.0283 6.55563 31.6086 5.99998 31.0908 5.99998H26.9658C26.448 5.99998 26.0283 6.55563 26.0283 7.24109Z" fill="#023DFE" fill-opacity="0.5"/>
<path d="M19.0693 0H20.931V40H19.0693V0Z" fill="#D1D1D1"/>
</svg>

Before

Width:  |  Height:  |  Size: 1.9 KiB

View File

@ -1,12 +0,0 @@
<svg width="40" height="40" viewBox="0 0 40 40" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M38.0142 39.2553L35.3682 40H20.9308H19.9999H19.0691H1.24111C0.555643 40 0 39.4444 0 38.7589V1.24111C0 0.555651 0.555643 7.62939e-06 1.24111 7.62939e-06H19.0682H20.1226H20.9543H35.4255L38.2625 1.24111C38.9479 1.24111 39.5036 1.79676 39.5036 2.48222L39.2553 38.0142C39.2553 38.6997 38.6997 39.2553 38.0142 39.2553Z" fill="#E9E9E9"/>
<path d="M38.7585 0H35.0352C35.7206 0 36.2763 0.555643 36.2763 1.24111V38.7589C36.2763 39.4444 35.7206 40 35.0352 40H38.7585C39.4439 40 39.9996 39.4444 39.9996 38.7589V1.24111C39.9996 0.555643 39.4439 0 38.7585 0Z" fill="#D1D1D1"/>
<path opacity="0.6" d="M8.64062 31.8319V33.3212C8.64062 34.0067 8.22091 34.5623 7.70312 34.5623H3.57813C3.06034 34.5623 2.64062 34.0067 2.64062 33.3212V31.8319C2.64062 31.1464 3.06034 30.5908 3.57813 30.5908H7.70312C8.22091 30.5908 8.64062 31.1464 8.64062 31.8319Z" fill="#023DFE" fill-opacity="0.5"/>
<path opacity="0.6" d="M8.64062 7.24109V8.73042C8.64062 9.41588 8.22091 9.97152 7.70312 9.97152H3.57813C3.06034 9.97152 2.64062 9.41588 2.64062 8.73042V7.24109C2.64062 6.55563 3.06034 5.99998 3.57813 5.99998H7.70312C8.22091 5.99998 8.64062 6.55563 8.64062 7.24109Z" fill="#023DFE" fill-opacity="0.5"/>
<path opacity="0.6" d="M27.6406 31.8319V33.3212C27.6406 34.0067 28.0603 34.5623 28.5781 34.5623H32.7031C33.2209 34.5623 33.6406 34.0067 33.6406 33.3212V31.8319C33.6406 31.1464 33.2209 30.5908 32.7031 30.5908H28.5781C28.0603 30.5908 27.6406 31.1464 27.6406 31.8319Z" fill="#023DFE" fill-opacity="0.5"/>
<path opacity="0.6" d="M27.6406 7.24109V8.73042C27.6406 9.41588 28.0603 9.97152 28.5781 9.97152H32.7031C33.2209 9.97152 33.6406 9.41588 33.6406 8.73042V7.24109C33.6406 6.55563 33.2209 5.99998 32.7031 5.99998H28.5781C28.0603 5.99998 27.6406 6.55563 27.6406 7.24109Z" fill="#023DFE" fill-opacity="0.5"/>
<path opacity="0.6" d="M15.0625 31.8319V33.3212C15.0625 34.0067 15.4822 34.5623 16 34.5623H20.125C20.6428 34.5623 21.0625 34.0067 21.0625 33.3212V31.8319C21.0625 31.1464 20.6428 30.5908 20.125 30.5908H16C15.4822 30.5908 15.0625 31.1464 15.0625 31.8319Z" fill="#023DFE" fill-opacity="0.5"/>
<path opacity="0.6" d="M15.0625 7.24109V8.73042C15.0625 9.41588 15.4822 9.97152 16 9.97152H20.125C20.6428 9.97152 21.0625 9.41588 21.0625 8.73042V7.24109C21.0625 6.55563 20.6428 5.99998 20.125 5.99998H16C15.4822 5.99998 15.0625 6.55563 15.0625 7.24109Z" fill="#023DFE" fill-opacity="0.5"/>
<path d="M23.125 0H24.9867V40H23.125V0Z" fill="#D1D1D1"/>
<path d="M11.1719 0H13.0335V40H11.1719V0Z" fill="#D1D1D1"/>
</svg>

Before

Width:  |  Height:  |  Size: 2.5 KiB

View File

@ -1,63 +0,0 @@
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;
}
}
}

View File

@ -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,

View File

@ -1,5 +1,4 @@
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';
@ -14,21 +13,147 @@ class RemoteCalendarService implements CalendarSystemService {
@override
Future<CalendarEventsResponse> getCalendarEvents({
required LoadEventsParam params,
required String spaceId,
}) async {
final month = params.startDate.month.toString().padLeft(2, '0');
final year = params.startDate.year.toString();
try {
return await _httpService.get<CalendarEventsResponse>(
path: ApiEndpoints.getBookings
.replaceAll('{mm}', month)
.replaceAll('{yyyy}', year)
.replaceAll('{space}', params.id),
final response = await _httpService.get(
path: ApiEndpoints.getCalendarEvents,
queryParameters: {
'spaceId': spaceId,
},
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>) {

View File

@ -1,34 +0,0 @@
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')}';
}
}

View File

@ -1,8 +1,7 @@
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 LoadEventsParam params,
required String spaceId,
});
}

View File

@ -2,10 +2,9 @@ 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';
@ -13,35 +12,27 @@ class CalendarEventsBloc extends Bloc<CalendarEventsEvent, CalendarEventState> {
final 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);
}
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(params: param);
final events = response.data.map(_toCalendarEventData).toList();
final response = await calendarService.getCalendarEvents(
spaceId: event.spaceId,
);
final events =
response.data.map<CalendarEventData>(_toCalendarEventData).toList();
eventController.addAll(events);
emit(EventsLoaded(
events: events,
spaceId: spaceId,
month: month,
year: year,
));
emit(EventsLoaded(events: events));
} catch (e) {
emit(EventsError('Failed to load events'));
}
@ -49,19 +40,16 @@ 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();
@ -73,9 +61,6 @@ 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,
));
}
}
@ -105,13 +90,14 @@ class CalendarEventsBloc extends Bloc<CalendarEventsEvent, CalendarEventState> {
);
return CalendarEventData(
date: startTime,
date: startTime,
startTime: startTime,
endTime: endTime,
title: '${booking.user.firstName} ${booking.user.lastName}',
title:
'${booking.space.spaceName} - ${booking.user.firstName} ${booking.user.lastName}',
description: 'Cost: ${booking.cost}',
color: Colors.blue,
event: booking,
event: booking,
);
}

View File

@ -6,10 +6,16 @@ abstract class CalendarEventsEvent {
}
class LoadEvents extends CalendarEventsEvent {
final LoadEventsParam param;
const LoadEvents(this.param);
}
final String spaceId;
final DateTime weekStart;
final DateTime weekEnd;
const LoadEvents({
required this.spaceId,
required this.weekStart,
required this.weekEnd,
});
}
class AddEvent extends CalendarEventsEvent {
final CalendarEventData event;

View File

@ -7,17 +7,11 @@ class EventsInitial extends CalendarEventState {}
class EventsLoading extends CalendarEventState {}
final class EventsLoaded extends CalendarEventState {
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,
});
}

View File

@ -2,13 +2,11 @@ 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';
@ -48,11 +46,9 @@ class _BookingPageState extends State<BookingPage> {
if (selectedRoom != null) {
context.read<CalendarEventsBloc>().add(
LoadEvents(
LoadEventsParam(
startDate: dateState.weekStart,
endDate: dateState.weekStart.add(const Duration(days: 6)),
id: selectedRoom.uuid,
),
spaceId: selectedRoom.uuid,
weekStart: dateState.weekStart,
weekEnd: dateState.weekStart.add(const Duration(days: 6)),
),
);
}
@ -65,14 +61,11 @@ class _BookingPageState extends State<BookingPage> {
BlocProvider(create: (_) => SelectedBookableSpaceBloc()),
BlocProvider(create: (_) => DateSelectionBloc()),
BlocProvider(
create: (_) => CalendarEventsBloc(
calendarService: MemoryCalendarServiceWithRemoteFallback(
remoteService: RemoteCalendarService(
HTTPService(),
),
memoryService: MemoryCalendarService(),
),
)),
create: (_) => CalendarEventsBloc(
calendarService:
FakeRemoteCalendarService(HTTPService(), useDummy: true),
),
),
],
child: Builder(
builder: (context) =>
@ -145,7 +138,7 @@ class _BookingPageState extends State<BookingPage> {
),
),
Expanded(
flex: 5,
flex: 4,
child: Padding(
padding: const EdgeInsets.all(20.0),
child: Column(
@ -194,7 +187,6 @@ class _BookingPageState extends State<BookingPage> {
],
),
Expanded(
flex: 5,
child: BlocBuilder<SelectedBookableSpaceBloc,
SelectedBookableSpaceState>(
builder: (context, roomState) {

View File

@ -72,7 +72,11 @@ class __SidebarContentState extends State<_SidebarContent> {
@override
Widget build(BuildContext context) {
return BlocConsumer<SidebarBloc, SidebarState>(
listener: (context, state) {},
listener: (context, state) {
if (state.currentPage == 1 && searchController.text.isNotEmpty) {
searchController.clear();
}
},
builder: (context, state) {
return Column(
children: [
@ -143,7 +147,6 @@ class __SidebarContentState extends State<_SidebarContent> {
IconButton(
icon: const Icon(Icons.close),
onPressed: () {
searchController.clear();
context.read<SidebarBloc>().add(ResetSearch());
},
),

View File

@ -1,15 +1,16 @@
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(
@ -17,86 +18,39 @@ class EventTileWidget extends StatelessWidget {
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: events.map((event) {
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 =
final bool 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: EdgeInsets.all(5),
padding: const EdgeInsets.all(6),
decoration: BoxDecoration(
color: isEventEnded
? ColorsManager.grayColor.withOpacity(0.1)
: ColorsManager.blue1.withOpacity(0.1),
? ColorsManager.lightGrayBorderColor
: ColorsManager.blue1.withOpacity(0.25),
borderRadius: BorderRadius.circular(6),
border: const Border(
left: BorderSide(
color: ColorsManager.grayColor,
width: 4,
),
),
),
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,
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
DateFormat('h:mm a').format(event.startTime!),
style: const TextStyle(
fontWeight: FontWeight.bold,
fontSize: 12,
color: Colors.black87,
),
),
const SizedBox(height: 2),
Text(
event.title,
style: const TextStyle(
fontSize: 12,
color: ColorsManager.blackColor,
),
),
],
),
),
);
}).toList(),

View File

@ -4,6 +4,8 @@ import 'package:syncrow_web/utils/color_manager.dart';
class SvgTextButton extends StatelessWidget {
final String svgAsset;
final double? horizontalPadding;
final double? verticalPadding;
final String label;
final VoidCallback onPressed;
final Color backgroundColor;
@ -12,16 +14,21 @@ 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.horizontalPadding,
this.verticalPadding,
this.boxShadow = const [
BoxShadow(
color: ColorsManager.lightGrayColor,
@ -40,7 +47,9 @@ class SvgTextButton extends StatelessWidget {
borderRadius: BorderRadius.circular(borderRadius),
onTap: onPressed,
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 12),
padding: EdgeInsets.symmetric(
horizontal: horizontalPadding ?? 24,
vertical: verticalPadding ?? 12),
decoration: BoxDecoration(
color: backgroundColor,
borderRadius: BorderRadius.circular(borderRadius),

View File

@ -24,21 +24,17 @@ class RoomListItem extends StatelessWidget {
activeColor: ColorsManager.primaryColor,
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,
),
),
);

View File

@ -1,6 +1,7 @@
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';
@ -22,12 +23,6 @@ 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) {
@ -57,159 +52,154 @@ class WeeklyCalendarPage extends StatelessWidget {
);
}
final weekDays = _getWeekDays(weekStart);
const double timeLineWidth = 65;
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) {
!date.isBefore(start) && !date.isAfter(end);
// remove this line and Check if the date is within the range
return false;
return !date.isBefore(start) && !date.isAfter(end);
}
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),
borderRadius: BorderRadius.circular(8),
),
);
} else if (isSelected) {
return Container(
height: height,
width: width,
decoration: BoxDecoration(
color:
ColorsManager.spaceColor.withOpacity(0.07),
borderRadius: BorderRadius.circular(8),
),
);
}
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: -80,
startDay: WeekDays.monday,
liveTimeIndicatorSettings:
const LiveTimeIndicatorSettings(
showBullet: false,
height: 0,
),
weekDayBuilder: (date) {
return WeekDayHeader(
date: date,
isSelectedDay: isSameDay(date, selectedDate),
);
},
timeLineBuilder: (date) {
return TimeLineWidget(date: date);
},
timeLineWidth: timeLineWidth,
weekPageHeaderBuilder: (start, end) => Container(),
weekTitleHeight: 60,
weekNumberBuilder: (firstDayOfWeek) => Padding(
padding: const EdgeInsets.only(right: 15, bottom: 10),
child: Column(
crossAxisAlignment: CrossAxisAlignment.end,
mainAxisAlignment: MainAxisAlignment.end,
children: [
Text(
firstDayOfWeek.timeZoneName
.replaceAll(':00', ''),
style: Theme.of(context)
.textTheme
.bodyMedium
?.copyWith(
fontSize: 12,
color: ColorsManager.blackColor,
fontWeight: FontWeight.w400,
),
),
],
),
),
eventTileBuilder: (date, events, boundary, start, end) {
return EventTileWidget(
events: events,
);
},
return Padding(
padding: const EdgeInsets.only(left: 25.0, right: 25.0, top: 25),
child: Stack(
children: [
WeekView(
weekDetectorBuilder: ({
required date,
required height,
required heightPerMinute,
required minuteSlotSize,
required width,
}) {
return isInRange(date, highlightStart, highlightEnd)
? HatchedColumnBackground(
backgroundColor: ColorsManager.grey800,
lineColor: ColorsManager.textGray,
opacity: 0.3,
stripeSpacing: 12,
borderRadius: BorderRadius.circular(8),
)
: const SizedBox();
},
pageViewPhysics: const NeverScrollableScrollPhysics(),
key: ValueKey(weekStart),
controller: eventController,
initialDay: weekStart,
startHour: startHour - 1,
endHour: endHour,
heightPerMinute: 1.1,
showLiveTimeLineInAllDays: false,
showVerticalLines: true,
emulateVerticalOffsetBy: -80,
startDay: WeekDays.monday,
liveTimeIndicatorSettings: const LiveTimeIndicatorSettings(
showBullet: false,
height: 0,
),
weekDayBuilder: (date) {
return WeekDayHeader(
date: date,
isSelectedDay: isSameDay(date, selectedDate),
);
},
timeLineBuilder: (date) {
return TimeLineWidget(date: date);
},
timeLineWidth: timeLineWidth,
weekPageHeaderBuilder: (start, end) => Container(),
weekTitleHeight: 60,
weekNumberBuilder: (firstDayOfWeek) => Padding(
padding: const EdgeInsets.only(right: 15, bottom: 10),
child: Column(
crossAxisAlignment: CrossAxisAlignment.end,
mainAxisAlignment: MainAxisAlignment.end,
children: [
Text(
firstDayOfWeek.timeZoneName.replaceAll(':00', ''),
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
fontSize: 12,
color: ColorsManager.blackColor,
fontWeight: FontWeight.w400,
),
),
],
),
),
eventTileBuilder: (date, events, boundary, start, end) {
return EventTileWidget(
events: events,
);
},
),
if (selectedDayIndex >= 0)
Positioned(
left: (timeLineWidth + 3) +
(dayColumnWidth - 8) * (selectedDayIndex - 0.01),
top: 0,
bottom: 0,
width: dayColumnWidth,
child: IgnorePointer(
child: Container(
margin: const EdgeInsets.symmetric(
vertical: 0, horizontal: 4),
color: ColorsManager.spaceColor.withOpacity(0.07),
),
Positioned(
right: 0,
top: 50,
bottom: 0,
child: IgnorePointer(
child: Container(
width: 1,
color: Theme.of(context).scaffoldBackgroundColor,
),
),
),
),
if (selectedSidebarIndex >= 0 &&
selectedSidebarIndex != selectedDayIndex)
Positioned(
left: (timeLineWidth + 3) +
(dayColumnWidth - 8) * (selectedSidebarIndex - 0.01),
top: 0,
bottom: 0,
width: dayColumnWidth,
child: IgnorePointer(
child: Container(
margin: const EdgeInsets.symmetric(
vertical: 0, horizontal: 4),
color: Colors.orange.withOpacity(0.14),
),
],
),
),
Positioned(
right: 0,
top: 50,
bottom: 0,
child: IgnorePointer(
child: Container(
width: 1,
color: Theme.of(context).scaffoldBackgroundColor,
),
),
),
));
],
),
);
},
);
}
List<DateTime> _getWeekDays(DateTime date) {
final int weekday = date.weekday;
final DateTime monday = date.subtract(Duration(days: weekday - 1));
return List.generate(7, (i) => monday.add(Duration(days: i)));
}
}
bool isSameDay(DateTime d1, DateTime d2) {

View File

@ -0,0 +1,59 @@
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 Container(
child: Row(
children: [
Expanded(
child: Container(
color: Colors.blueGrey[100],
child: const Center(
child: Text(
'Side bar',
style: TextStyle(fontSize: 24, fontWeight: FontWeight.bold),
),
),
)),
Expanded(
flex: 4,
child: Padding(
padding: const EdgeInsets.all(20.0),
child: SizedBox(
child: Column(
children: [
Row(
mainAxisAlignment: MainAxisAlignment.start,
children: [
SvgTextButton(
svgAsset: Assets.homeIcon,
label: 'Manage Bookable Spaces',
onPressed: () {
pageController.jumpToPage(2);
}),
const SizedBox(width: 20),
SvgTextButton(
svgAsset: Assets.groupIcon,
label: 'Manage Users',
onPressed: () {})
],
)
],
),
),
))
],
),
);
}
}

View File

@ -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(
BookableSpacesParams 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);
}
}
}

View File

@ -0,0 +1,85 @@
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/params/send_bookable_spaces_to_api_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 {
Timer? _debounce;
final HTTPService _httpService;
RemoteNonBookableSpaces(this._httpService);
static const _defaultErrorMessage = 'Failed to load Spaces';
@override
Future<PaginatedDataModel<BookableSpacemodel>> load(
NonBookableSpacesParams params) {
final completer = Completer<PaginatedDataModel<BookableSpacemodel>>();
_debounce?.cancel();
_debounce = Timer(const Duration(milliseconds: 500), () 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,
);
},
);
completer.complete(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(': ');
completer.completeError(APIException(formattedErrorMessage));
} catch (e) {
final formattedErrorMessage = [_defaultErrorMessage, '$e'].join(': ');
completer.completeError(APIException(formattedErrorMessage));
}
});
return completer.future;
}
@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);
}
}
}

View File

@ -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);
}
}
}

View File

@ -0,0 +1,63 @@
import 'package:flutter/material.dart';
class BookableSpaceConfig {
String configUuid;
List<String> bookableDays;
TimeOfDay? bookingStartTime;
TimeOfDay? bookingEndTime;
int cost;
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,
);
}
}

View File

@ -0,0 +1,58 @@
import 'package:syncrow_web/pages/access_management/manage_bookable_spaces/domain/models/bookable_space_config.dart';
class BookableSpacemodel {
String spaceUuid;
String spaceName;
BookableSpaceConfig? spaceConfig;
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,
);
}
}

View File

@ -0,0 +1,10 @@
class BookableSpacesParams {
int currentPage;
BookableSpacesParams({
required this.currentPage,
});
Map<String, dynamic> toJson() => {
'page': currentPage,
};
}

View File

@ -0,0 +1,12 @@
class NonBookableSpacesParams {
int currentPage;
String? searchedWords;
NonBookableSpacesParams({
required this.currentPage,
this.searchedWords,
});
Map<String, dynamic> toJson() => {
'page': currentPage,
};
}

View File

@ -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 {
List<String> spaceUuids;
List<String> daysAvailable;
String startTime;
String endTime;
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
};
}

View File

@ -0,0 +1,42 @@
import 'package:flutter/foundation.dart';
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 {
String spaceUuid;
List<String>? bookableDays;
String? bookingStartTime;
String? bookingEndTime;
int? cost;
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,
};
}

View File

@ -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(
BookableSpacesParams param);
}

View File

@ -0,0 +1,10 @@
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/params/send_bookable_spaces_to_api_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);
Future<void> sendBookableSpacesToApi(SendBookableSpacesToApiParams params);
}

View File

@ -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);
}

View File

@ -0,0 +1,63 @@
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 editedBookableSpace = event.bookableSpaces.data.firstWhere(
(element) => element.spaceUuid == event.bookableSpace.spaceUuid,
);
final config = editedBookableSpace.spaceConfig!.copyWith(
availability: event.updatedBookableSpaceConfig.availability,
bookableDays: event.updatedBookableSpaceConfig.bookableDays,
bookingEndTime: event.updatedBookableSpaceConfig.bookingEndTime,
bookingStartTime: event.updatedBookableSpaceConfig.bookingStartTime,
cost: event.updatedBookableSpaceConfig.cost,
);
editedBookableSpace.spaceConfig = config;
final index = event.bookableSpaces.data.indexWhere(
(element) => element.spaceUuid == event.bookableSpace.spaceUuid,
);
event.bookableSpaces.data.removeAt(index);
event.bookableSpaces.data.insert(index, editedBookableSpace);
}
emit(BookableSpacesLoaded(bookableSpacesList: event.bookableSpaces));
}
}

View File

@ -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 BookableSpacesParams 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,
});
}

View File

@ -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 {}

View File

@ -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()),
);
}
}
}
}

View File

@ -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,
});
}

View File

@ -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);
}

View File

@ -0,0 +1,78 @@
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/params/send_bookable_spaces_to_api_params.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;
List<BookableSpacemodel> selectedBookableSpaces = [];
SetupBookableSpacesBloc(this.nonBookableSpacesService)
: super(SetupBookableSpacesInitial()) {
on<AddToBookableSpaceEvent>(_onAddToBookableSpaceEvent);
on<RemoveFromBookableSpaceEvent>(_onRemoveFromBookableSpaceEvent);
on<SendBookableSpacesToApi>(_onSendBookableSpacesToApi);
on<CheckConfigurValidityEvent>(_onCheckConfigurValidityEvent);
on<EditModeSelected>(_onEditModeSelected);
}
TimeOfDay? get endTime =>
selectedBookableSpaces.first.spaceConfig!.bookingEndTime;
TimeOfDay? get startTime =>
selectedBookableSpaces.first.spaceConfig!.bookingStartTime;
void _onAddToBookableSpaceEvent(
AddToBookableSpaceEvent event,
Emitter<SetupBookableSpacesState> emit,
) {
emit(InProgressState());
selectedBookableSpaces.add(event.nonBookableSpace);
emit(AddNonBookableSpaceIntoBookableState(
bookableSpaces: selectedBookableSpaces));
}
void _onRemoveFromBookableSpaceEvent(RemoveFromBookableSpaceEvent event,
Emitter<SetupBookableSpacesState> emit) {
emit(InProgressState());
selectedBookableSpaces.remove(event.bookableSpace);
emit(RemoveBookableSpaceIntoNonBookableState(
bookableSpaces: selectedBookableSpaces));
}
Future<void> _onSendBookableSpacesToApi(SendBookableSpacesToApi event,
Emitter<SetupBookableSpacesState> emit) async {
emit(SendBookableSpacesLoading());
try {
await nonBookableSpacesService.sendBookableSpacesToApi(
SendBookableSpacesToApiParams.fromBookableSpacesModel(
selectedBookableSpaces,
),
);
emit(SendBookableSpacesSuccess());
} catch (e) {
emit(
SendBookableSpacesError(e.toString()),
);
}
}
void _onCheckConfigurValidityEvent(CheckConfigurValidityEvent event,
Emitter<SetupBookableSpacesState> emit) {
if (selectedBookableSpaces.first.spaceConfig!.isValid) {
emit(ValidSaveButtonState());
} else {
emit(UnValidSaveButtonState());
}
}
void _onEditModeSelected(
EditModeSelected event, Emitter<SetupBookableSpacesState> emit) {
selectedBookableSpaces.clear();
selectedBookableSpaces.add(event.editingBookableSpace);
}
}

View File

@ -0,0 +1,32 @@
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 SendBookableSpacesToApi extends SetupBookableSpacesEvent {}
class CheckConfigurValidityEvent extends SetupBookableSpacesEvent {}
class EditModeSelected extends SetupBookableSpacesEvent {
final BookableSpacemodel editingBookableSpace;
const EditModeSelected({
required this.editingBookableSpace,
});
}

View File

@ -0,0 +1,39 @@
part of 'setup_bookable_spaces_bloc.dart';
sealed class SetupBookableSpacesState extends Equatable {
const SetupBookableSpacesState();
@override
List<Object> get props => [];
}
final class SetupBookableSpacesInitial extends SetupBookableSpacesState {}
class AddNonBookableSpaceIntoBookableState extends SetupBookableSpacesState {
final List<BookableSpacemodel> bookableSpaces;
const AddNonBookableSpaceIntoBookableState({
required this.bookableSpaces,
});
}
class InProgressState extends SetupBookableSpacesState {}
class RemoveBookableSpaceIntoNonBookableState extends SetupBookableSpacesState {
final List<BookableSpacemodel> bookableSpaces;
const RemoveBookableSpaceIntoNonBookableState({
required this.bookableSpaces,
});
}
class ValidSaveButtonState extends SetupBookableSpacesState {}
class UnValidSaveButtonState extends SetupBookableSpacesState {}
class SendBookableSpacesLoading extends SetupBookableSpacesState {}
class SendBookableSpacesSuccess extends SetupBookableSpacesState {}
class SendBookableSpacesError extends SetupBookableSpacesState {
final String error;
const SendBookableSpacesError(this.error);
}

View File

@ -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(StepsInitial());
void initDialogValue() {
emit(StepOneState());
}
void editValueInit() {
emit(StepTwoState());
}
void goToNextStep() {
if (state is StepOneState) {
emit(StepTwoState());
}
}
}

View File

@ -0,0 +1,15 @@
part of 'steps_cubit.dart';
sealed class StepsState extends Equatable {
const StepsState();
@override
List<Object> get props => [];
}
final class StepsInitial extends StepsState {}
final class StepOneState extends StepsState {}
final class StepTwoState extends StepsState {}

View File

@ -0,0 +1,18 @@
import 'package:bloc/bloc.dart';
import 'package:equatable/equatable.dart';
part 'toggle_points_switch_state.dart';
class TogglePointsSwitchCubit extends Cubit<TogglePointsSwitchState> {
TogglePointsSwitchCubit() : super(TogglePointsSwitchInitial());
bool switchValue = true;
void activateSwitch() {
switchValue = true;
emit(ActivatePointsSwitch());
}
void unActivateSwitch() {
switchValue = false;
emit(UnActivatePointsSwitch());
}
}

View File

@ -0,0 +1,14 @@
part of 'toggle_points_switch_cubit.dart';
sealed class TogglePointsSwitchState extends Equatable {
const TogglePointsSwitchState();
@override
List<Object> get props => [];
}
final class TogglePointsSwitchInitial extends TogglePointsSwitchState {}
class ActivatePointsSwitch extends TogglePointsSwitchState {}
class UnActivatePointsSwitch extends TogglePointsSwitchState {}

View File

@ -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.updatedParams.spaceUuid));
try {
final updatedSpace =
await updateBookableSpaceService.update(event.updatedParams);
emit(UpdateBookableSpaceSuccess(bookableSpaceConfig: updatedSpace));
event.onSuccess?.call();
} on APIException catch (e) {
emit(UpdateBookableSpaceFailure(error: e.message));
} catch (e) {
emit(
UpdateBookableSpaceFailure(error: e.toString()),
);
}
}
}

View File

@ -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 updatedParams;
const UpdateBookableSpace({
required this.updatedParams,
this.onSuccess,
});
}

View File

@ -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,
});
}

View File

@ -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(
BookableSpacesParams(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(),
),
],
),
);
}
}

View File

@ -0,0 +1,125 @@
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.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/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/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(
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!,
)),
)
],
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 setupBookableSpacesBloc =
context.watch<SetupBookableSpacesBloc>();
final selectedSpaces =
setupBookableSpacesBloc.selectedBookableSpaces;
return stepsState is StepOneState
? NextFirstStepButton(selectedSpaces: selectedSpaces)
: SaveSecondStepButton(
selectedSpaces: selectedSpaces,
pointsController: pointsController,
isEditingMode: widget.editingBookableSpace != null,
);
}),
],
),
),
);
}
}

View File

@ -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.blueColor,
inactiveTrackColor: ColorsManager.grayBorder,
thumbColor: WidgetStateProperty.resolveWith<Color>(
(Set<WidgetState> states) {
return ColorsManager.whiteColors;
}),
onChanged: (value) {
context.read<UpdateBookableSpacesBloc>().add(
UpdateBookableSpace(
updatedParams: UpdateBookableSpaceParam(
spaceUuid: space.spaceUuid,
availability: value,
)),
);
},
);
},
),
),
);
}
}

View File

@ -0,0 +1,129 @@
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) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Text(
'* ',
style: Theme.of(context)
.textTheme
.bodyMedium!
.copyWith(color: Colors.red),
),
const Text('Booking Period'),
],
),
Container(
width: 300,
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(12),
color: ColorsManager.graysColor,
),
child: Row(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
TimePickerWidget(
title: editingBookableSpace == null
? 'Start Time'
: editingBookableSpace!.spaceConfig!.bookingStartTime!
.format(context),
onTimePicked: (timePicked) {
if (timePicked == null) {
return;
}
final setupBookableSpacesBloc =
context.read<SetupBookableSpacesBloc>();
if (setupBookableSpacesBloc.endTime != null &&
isEndTimeAfterStartTime(
timePicked, setupBookableSpacesBloc.endTime!)) {
ScaffoldMessenger.of(context).clearSnackBars();
ScaffoldMessenger.of(context).showSnackBar(const SnackBar(
content:
Text("You can't choose start Time Before End time"),
duration: Duration(seconds: 2),
backgroundColor: ColorsManager.red,
));
throw Exception();
} else {
setupBookableSpacesBloc.selectedBookableSpaces.forEach(
(e) => e.spaceConfig!.bookingStartTime = timePicked,
);
}
},
),
const Icon(
Icons.arrow_right_alt,
color: ColorsManager.grayColor,
),
TimePickerWidget(
title: editingBookableSpace == null
? 'End Time'
: editingBookableSpace!.spaceConfig!.bookingEndTime!
.format(context),
onTimePicked: (timePicked) {
if (timePicked == null) {
return;
}
final setupBookableSpacesBloc =
context.read<SetupBookableSpacesBloc>();
if (setupBookableSpacesBloc.startTime != null &&
isEndTimeAfterStartTime(
setupBookableSpacesBloc.startTime!, timePicked)) {
ScaffoldMessenger.of(context).clearSnackBars();
ScaffoldMessenger.of(context).showSnackBar(const SnackBar(
content:
Text("You can't choose End Time After Start time"),
duration: Duration(seconds: 2),
backgroundColor: ColorsManager.red,
));
throw Exception();
} else {
setupBookableSpacesBloc.selectedBookableSpaces.forEach(
(e) => e.spaceConfig!.bookingEndTime = timePicked,
);
}
},
),
Container(
width: 50,
height: 32,
decoration: const BoxDecoration(
borderRadius: BorderRadius.only(
topLeft: Radius.circular(10),
bottomLeft: Radius.circular(10),
),
),
alignment: Alignment.center,
child: SvgPicture.asset(
Assets.clockIcon,
height: 15,
color: ColorsManager.blackColor.withValues(alpha: 0.4),
),
)
],
),
),
],
);
}
}

View File

@ -0,0 +1,106 @@
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(
BookableSpacesParams(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,
),
);
},
),
)
],
)
],
);
}
}

View File

@ -0,0 +1,74 @@
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/utils/color_manager.dart';
class CheckBoxSpaceWidget extends StatelessWidget {
final BookableSpacemodel nonBookableSpace;
final List<BookableSpacemodel> selectedSpaces;
const CheckBoxSpaceWidget({
super.key,
required this.nonBookableSpace,
required this.selectedSpaces,
});
@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 Checkbox(
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: 5),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
nonBookableSpace.spaceName,
style: const TextStyle(
fontWeight: FontWeight.bold,
color: ColorsManager.textGray,
),
),
Text(
nonBookableSpace.spaceVirtualAddress,
style: const TextStyle(
fontSize: 12,
color: ColorsManager.textGray,
),
),
],
)),
],
);
}
}

View File

@ -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,
),
));
}
}

View File

@ -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(),
);
}
}

View File

@ -0,0 +1,33 @@
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,
),
StepsInitial() => const SizedBox(),
};
}),
);
}
}

View File

@ -0,0 +1,59 @@
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: 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,
fixedSize: const Size(50, 30),
elevation: 1,
),
child: SvgPicture.asset(
Assets.settings,
height: 15,
color: ColorsManager.blue1,
),
),
);
}
}

View File

@ -0,0 +1,158 @@
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 = [];
// « Two pages back
if (currentPage > 2) {
paginationItems.add(
_buildArrowButton(
label: '«',
onTap: () {
context.read<BookableSpacesBloc>().add(
LoadBookableSpacesEvent(
BookableSpacesParams(currentPage: currentPage - 2),
),
);
},
),
);
}
// < One page back
if (currentPage > 1) {
paginationItems.add(
_buildArrowButton(
label: '<',
onTap: () {
context.read<BookableSpacesBloc>().add(
LoadBookableSpacesEvent(
BookableSpacesParams(currentPage: currentPage - 1),
),
);
},
),
);
}
// Page numbers
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(
BookableSpacesParams(currentPage: i),
),
);
}
},
child: Container(
width: 30,
height: 30,
alignment: Alignment.center,
decoration: BoxDecoration(
color: i == currentPage
? ColorsManager.dialogBlueTitle
: Colors.grey[300],
borderRadius: BorderRadius.circular(8),
),
child: Text(
'$i',
style: TextStyle(
color: i == currentPage ? Colors.white : Colors.black,
fontWeight: i == currentPage
? FontWeight.bold
: FontWeight.normal,
),
),
),
),
),
);
}
// > One page forward
if (currentPage < totalPages) {
paginationItems.add(
_buildArrowButton(
label: '>',
onTap: () {
context.read<BookableSpacesBloc>().add(
LoadBookableSpacesEvent(
BookableSpacesParams(currentPage: currentPage + 1),
),
);
},
),
);
}
// » Two pages forward
if (currentPage + 1 < totalPages) {
paginationItems.add(
_buildArrowButton(
label: '»',
onTap: () {
context.read<BookableSpacesBloc>().add(
LoadBookableSpacesEvent(
BookableSpacesParams(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(
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
decoration: BoxDecoration(
color: Colors.grey[300],
borderRadius: BorderRadius.circular(8),
),
child: Text(
label,
style: const TextStyle(
color: Colors.black,
fontWeight: FontWeight.bold,
),
),
),
),
);
}
}

View File

@ -0,0 +1,121 @@
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(
BookableSpacesParams(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(Padding(
padding: const EdgeInsetsGeometry.only(left: 10),
child: Text(
space.spaceVirtualAddress,
style: const TextStyle(fontSize: 11),
))),
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),
),
);
}
}

View File

@ -0,0 +1,78 @@
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: [
ElevatedButton(
style: ElevatedButton.styleFrom(
padding: EdgeInsets.zero,
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(
verticalPadding: 10,
horizontalPadding: 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(),
),
);
},
)
],
),
);
}
}

View File

@ -0,0 +1,32 @@
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/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 {
final List<BookableSpacemodel> selectedSpaces;
const NextFirstStepButton({
super.key,
required this.selectedSpaces,
});
@override
Widget build(BuildContext context) {
return ButtonsDividerBottomDialogWidget(
title: 'Next',
onNextPressed: selectedSpaces.isEmpty
? null
: () {
context.read<StepsCubit>().goToNextStep();
context
.read<SetupBookableSpacesBloc>()
.add(CheckConfigurValidityEvent());
},
onCancelPressed: () => context.pop(),
);
}
}

View File

@ -0,0 +1,142 @@
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;
const PointsPartWidget({
super.key,
required this.pointsController,
this.editingBookableSpace,
});
final TextEditingController pointsController;
@override
State<PointsPartWidget> createState() => _PointsPartWidgetState();
}
class _PointsPartWidgetState extends State<PointsPartWidget> {
@override
void initState() {
if (widget.editingBookableSpace != null) {
widget.pointsController.text =
widget.editingBookableSpace!.spaceConfig!.cost.toString();
}
super.initState();
}
@override
Widget build(BuildContext context) {
return BlocBuilder<TogglePointsSwitchCubit, TogglePointsSwitchState>(
builder: (context, state) {
return Column(
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.start,
children: [
if (state is ActivatePointsSwitch)
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(
trackOutlineColor: WidgetStateProperty.resolveWith<Color>(
(Set<WidgetState> states) {
return ColorsManager.whiteColors;
}),
activeTrackColor: ColorsManager.blueColor,
inactiveTrackColor: ColorsManager.grayBorder,
thumbColor: WidgetStateProperty.resolveWith<Color>(
(Set<WidgetState> states) {
return ColorsManager.whiteColors;
}),
value: context.watch<TogglePointsSwitchCubit>().switchValue,
onChanged: (value) {
if (value) {
context
.read<TogglePointsSwitchCubit>()
.activateSwitch();
context
.read<SetupBookableSpacesBloc>()
.selectedBookableSpaces
.forEach(
(e) => e.spaceConfig!.cost = -1,
);
context
.read<SetupBookableSpacesBloc>()
.add(CheckConfigurValidityEvent());
} else {
context
.read<TogglePointsSwitchCubit>()
.unActivateSwitch();
widget.pointsController.clear();
context
.read<SetupBookableSpacesBloc>()
.selectedBookableSpaces
.forEach(
(e) => e.spaceConfig!.cost = 0,
);
context
.read<SetupBookableSpacesBloc>()
.add(CheckConfigurValidityEvent());
}
},
),
)
],
),
const SizedBox(
height: 5,
),
if (state is ActivatePointsSwitch)
SearchUnbookableSpacesWidget(
title: 'Ex: 0',
height: 40,
onChanged: (p0) {
context
.read<SetupBookableSpacesBloc>()
.selectedBookableSpaces
.forEach(
(e) => e.spaceConfig!.cost = int.parse(
widget.pointsController.text.isEmpty
? '0'
: widget.pointsController.text,
),
);
context
.read<SetupBookableSpacesBloc>()
.add(CheckConfigurValidityEvent());
},
controller: widget.pointsController,
inputFormatters: [FilteringTextInputFormatter.digitsOnly],
suffix: const SizedBox(),
)
else
const SizedBox(),
],
);
},
);
}
}

View File

@ -0,0 +1,73 @@
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/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 List<BookableSpacemodel> selectedSpaces;
final TextEditingController pointsController;
final bool isEditingMode;
const SaveSecondStepButton({
super.key,
required this.selectedSpaces,
required this.pointsController,
required this.isEditingMode,
});
@override
Widget build(BuildContext context) {
return BlocConsumer<SetupBookableSpacesBloc, SetupBookableSpacesState>(
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),
),
);
}
},
builder: (context, state) {
return ButtonsDividerBottomDialogWidget(
title: 'Save',
onNextPressed: state is UnValidSaveButtonState
? null
: () {
if (selectedSpaces.any(
(element) => element.isValid,
)) {
isEditingMode
? callEditLogic(context)
: context.read<SetupBookableSpacesBloc>().add(
SendBookableSpacesToApi(),
);
}
},
onCancelPressed: () => context.pop(),
);
},
);
}
void callEditLogic(BuildContext context) {
context.read<UpdateBookableSpacesBloc>().add(
UpdateBookableSpace(
onSuccess: () =>
context.read<NonBookableSpacesBloc>().add(CallInitStateEvent()),
updatedParams: UpdateBookableSpaceParam.fromBookableModel(
context
.read<SetupBookableSpacesBloc>()
.selectedBookableSpaces
.first,
),
),
);
}
}

View File

@ -0,0 +1,62 @@
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:syncrow_web/utils/color_manager.dart';
class SearchUnbookableSpacesWidget extends StatelessWidget {
final String title;
final Widget? suffix;
final double? height;
final double? width;
final TextEditingController? controller;
final List<TextInputFormatter>? inputFormatters;
final void Function(String)? onChanged;
const SearchUnbookableSpacesWidget({
required this.title,
this.controller,
this.onChanged,
this.suffix,
this.height,
this.width,
this.inputFormatters,
super.key,
});
@override
Widget build(BuildContext context) {
return Container(
width: width ?? 480,
height: height ?? 30,
padding: const EdgeInsets.only(top: 4),
decoration: BoxDecoration(
color: ColorsManager.whiteColors,
borderRadius: BorderRadius.circular(12),
boxShadow: const [
BoxShadow(
color: ColorsManager.shadowOfSearchTextfield,
offset: Offset(0, 4),
blurRadius: 5,
),
],
),
child: TextField(
controller: controller,
inputFormatters: inputFormatters,
onChanged: onChanged,
decoration: InputDecoration(
contentPadding:
const EdgeInsets.symmetric(vertical: 5, horizontal: 15),
hintText: title,
hintStyle: const TextStyle(color: ColorsManager.hintTextGrey),
border: InputBorder.none,
suffixIcon: suffix ??
const Icon(Icons.search,
size: 20, color: ColorsManager.hintTextGrey),
),
style: const TextStyle(
fontSize: 14,
color: ColorsManager.hintTextGrey,
),
),
);
}
}

View File

@ -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(),
};
},
),
)
],
),
)
],
);
}
}

View File

@ -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: 20,
),
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),
)
],
),
);
}
}

View File

@ -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: 50,
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: 50,
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,
),
),
],
);
}
}

View File

@ -0,0 +1,76 @@
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 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!,
);
},
);
widget.onTimePicked(tempTime);
timePicked = tempTime;
widget.setupBookableSpacesBloc.add(CheckConfigurValidityEvent());
setState(() {});
},
child: Container(
width: 100,
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,
),
),
),
);
}
}

View File

@ -0,0 +1,53 @@
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],
selectedSpaces: context
.read<SetupBookableSpacesBloc>()
.selectedBookableSpaces,
);
} else {
return const Padding(
padding: EdgeInsets.symmetric(vertical: 10),
child: Center(child: CircularProgressIndicator()),
);
}
},
),
);
}
}

View File

@ -0,0 +1,84 @@
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';
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 Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: _daysChecked.entries.map((entry) {
return Expanded(
child: Row(
children: [
Expanded(
child: Checkbox(
value: entry.value,
onChanged: (newValue) {
setState(() {
_daysChecked[entry.key] = newValue ?? false;
final selectedDays = _daysChecked.entries
.where((e) => e.value)
.map((e) => e.key)
.toList();
for (var space in context
.read<SetupBookableSpacesBloc>()
.selectedBookableSpaces) {
space.spaceConfig!.bookableDays = selectedDays;
}
});
context
.read<SetupBookableSpacesBloc>()
.add(CheckConfigurValidityEvent());
},
),
),
Expanded(
child: Text(
entry.key,
style: const TextStyle(fontSize: 10),
)),
],
),
);
}).toList(),
);
}
}

View File

@ -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,
),
],
),
),

View File

@ -53,9 +53,8 @@ class DeviceManagementBloc
for (var community in spaceBloc.state.selectedCommunities) {
final spacesList =
spaceBloc.state.selectedCommunityAndSpaces[community] ?? [];
devices.addAll(await DevicesManagementApi().fetchDevices(projectUuid,
spacesId: spacesList,
communities: spaceBloc.state.selectedCommunities));
devices.addAll(await DevicesManagementApi()
.fetchDevices(projectUuid, spacesId: spacesList));
}
}
@ -159,8 +158,7 @@ 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)) {
@ -256,8 +254,7 @@ 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;
}
@ -274,8 +271,7 @@ 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 ||
@ -439,8 +435,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();

View File

@ -100,7 +100,6 @@ class _DeviceItem extends StatelessWidget {
@override
Widget build(BuildContext context) {
return DeviceControlsContainer(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,

View File

@ -937,17 +937,13 @@ class RoutineBloc extends Bloc<RoutineEvent, RoutineState> {
List<String> spacesList =
spaceBloc.state.selectedCommunityAndSpaces[communityId] ?? [];
devices.addAll(await DevicesManagementApi().fetchDevices(
projectUuid,
spacesId: spacesList,
communities: spaceBloc.state.selectedCommunities,
));
devices.addAll(await DevicesManagementApi()
.fetchDevices(projectUuid, spacesId: spacesList));
}
} else {
devices.addAll(await DevicesManagementApi().fetchDevices(
projectUuid,
spacesId: [createRoutineBloc.selectedSpaceId],
communities: spaceBloc.state.selectedCommunities,
));
}

View File

@ -1,71 +0,0 @@
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();
}
}

View File

@ -5,14 +5,13 @@ import 'package:syncrow_web/utils/color_manager.dart';
class SpacesConnectionsArrowPainter extends CustomPainter {
final List<SpaceConnectionModel> connections;
final Map<String, Offset> positions;
final Map<String, double> cardWidths;
final double cardWidth = 150.0;
final double cardHeight = 90.0;
final Set<String> highlightedUuids;
SpacesConnectionsArrowPainter({
required this.connections,
required this.positions,
required this.cardWidths,
required this.highlightedUuids,
});
@ -30,30 +29,19 @@ 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 + fromWidth / 2, from.dy + cardHeight - 10);
final endPoint = Offset(to.dx + toWidth / 2, to.dy);
Offset(from.dx + cardWidth / 2, from.dy + cardHeight - 10);
final endPoint = Offset(to.dx + cardWidth / 2, to.dy);
final path = Path()..moveTo(startPoint.dx, startPoint.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,
);
}
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);
canvas.drawPath(path, paint);
@ -63,7 +51,7 @@ class SpacesConnectionsArrowPainter extends CustomPainter {
: ColorsManager.blackColor.withValues(alpha: 0.5)
..style = PaintingStyle.fill
..blendMode = BlendMode.srcIn;
canvas.drawCircle(endPoint, 6, circlePaint);
canvas.drawCircle(endPoint, 4, circlePaint);
}
}
}

View File

@ -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,
);
}
}

View File

@ -10,7 +10,7 @@ import 'package:syncrow_web/pages/space_management_v2/modules/communities/presen
import 'package:syncrow_web/pages/space_management_v2/modules/products/data/services/remote_products_service.dart';
import 'package:syncrow_web/pages/space_management_v2/modules/products/presentation/bloc/products_bloc.dart';
import 'package:syncrow_web/pages/space_management_v2/modules/space_details/data/services/remote_space_details_service.dart';
import 'package:syncrow_web/pages/space_management_v2/modules/space_details/data/services/unique_space_details_spaces_decorator_service.dart';
import 'package:syncrow_web/pages/space_management_v2/modules/space_details/data/services/unique_subspaces_decorator.dart';
import 'package:syncrow_web/pages/space_management_v2/modules/space_details/presentation/bloc/space_details_bloc.dart';
import 'package:syncrow_web/services/api/http_service.dart';
import 'package:syncrow_web/utils/theme/responsive_text_theme.dart';
@ -49,7 +49,7 @@ class _SpaceManagementPageState extends State<SpaceManagementPage> {
),
BlocProvider(
create: (context) => SpaceDetailsBloc(
UniqueSpaceDetailsSpacesDecoratorService(
UniqueSubspacesDecorator(
RemoteSpaceDetailsService(httpService: HTTPService()),
),
),

View File

@ -1,6 +1,5 @@
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';
@ -31,11 +30,10 @@ class CommunityStructureCanvas extends StatefulWidget {
class _CommunityStructureCanvasState extends State<CommunityStructureCanvas>
with SingleTickerProviderStateMixin {
final Map<String, Offset> _positions = {};
final Map<String, double> _cardWidths = {};
final double _cardWidth = 150.0;
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;
@ -54,7 +52,6 @@ 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) {
@ -71,34 +68,6 @@ 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) {
@ -133,12 +102,11 @@ 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);
@ -187,16 +155,13 @@ 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) {
final lastChildWidth =
_cardWidths[space.children.last.uuid] ?? _minCardWidth;
childSubtreeWidth = (lastChildPos.dx + lastChildWidth) - firstChildPos.dx;
childSubtreeWidth = (lastChildPos.dx + _cardWidth) - firstChildPos.dx;
}
}
@ -205,7 +170,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;
}
@ -222,7 +187,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;
}
}
@ -237,11 +202,8 @@ 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);
@ -269,7 +231,7 @@ class _CommunityStructureCanvasState extends State<CommunityStructureCanvas>
Positioned(
left: createButtonX,
top: createButtonY,
child: CreateSpaceButton(community: widget.community),
child: CreateSpaceButton(communityUuid: widget.community.uuid),
),
);
@ -278,7 +240,6 @@ class _CommunityStructureCanvasState extends State<CommunityStructureCanvas>
painter: SpacesConnectionsArrowPainter(
connections: connections,
positions: _positions,
cardWidths: _cardWidths,
highlightedUuids: highlightedUuids,
),
child: Stack(alignment: AlignmentDirectional.center, children: widgets),
@ -310,7 +271,6 @@ class _CommunityStructureCanvasState extends State<CommunityStructureCanvas>
continue;
}
final cardWidth = _cardWidths[space.uuid] ?? _minCardWidth;
final isHighlighted = highlightedUuids.contains(space.uuid);
final hasNoSelectedSpace = widget.selectedSpace == null;
@ -318,29 +278,20 @@ class _CommunityStructureCanvasState extends State<CommunityStructureCanvas>
buildSpaceContainer: () {
return Opacity(
opacity: hasNoSelectedSpace || isHighlighted ? 1.0 : 0.5,
child: SpaceCell(
onTap: () => _onSpaceTapped(space),
icon: space.icon,
name: space.spaceName,
child: Tooltip(
message: space.spaceName,
preferBelow: false,
child: SpaceCell(
onTap: () => _onSpaceTapped(space),
icon: space.icon,
name: space.spaceName,
),
),
);
},
onTap: () => SpaceDetailsDialogHelper.showCreate(
context,
communityUuid: widget.community.uuid,
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),
),
);
},
),
);
@ -354,7 +305,7 @@ class _CommunityStructureCanvasState extends State<CommunityStructureCanvas>
Positioned(
left: position.dx,
top: position.dy,
width: cardWidth,
width: _cardWidth,
height: _cardHeight,
child: Draggable<SpaceReorderDataModel>(
data: reorderData,
@ -363,7 +314,7 @@ class _CommunityStructureCanvasState extends State<CommunityStructureCanvas>
child: Opacity(
opacity: 0.2,
child: SizedBox(
width: cardWidth,
width: _cardWidth,
height: _cardHeight,
child: spaceCard,
),
@ -379,7 +330,7 @@ class _CommunityStructureCanvasState extends State<CommunityStructureCanvas>
);
final targetPos = Offset(
position.dx + cardWidth + (_horizontalSpacing / 4) - 20,
position.dx + _cardWidth + (_horizontalSpacing / 4) - 20,
position.dy,
);
widgets.add(_buildDropTarget(parent, community, i + 1, targetPos));
@ -467,17 +418,17 @@ class _CommunityStructureCanvasState extends State<CommunityStructureCanvas>
@override
Widget build(BuildContext context) {
final treeWidgets = _buildTreeWidgets();
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,
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,
child: SizedBox(
width: context.screenWidth * 5,
height: context.screenHeight * 5,

View File

@ -2,17 +2,41 @@ 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_composer.dart';
import 'package:syncrow_web/pages/space_management_v2/main_module/widgets/community_structure_header_action_buttons.dart';
import 'package:syncrow_web/pages/space_management_v2/modules/communities/domain/models/space_model.dart';
import 'package:syncrow_web/pages/space_management_v2/modules/communities/presentation/bloc/communities_bloc.dart';
import 'package:syncrow_web/pages/space_management_v2/modules/communities/presentation/communities_tree_selection_bloc/communities_tree_selection_bloc.dart';
import 'package:syncrow_web/pages/space_management_v2/modules/space_details/domain/models/space_details_model.dart';
import 'package:syncrow_web/pages/space_management_v2/modules/space_details/presentation/helpers/space_details_dialog_helper.dart';
import 'package:syncrow_web/utils/color_manager.dart';
import 'package:syncrow_web/utils/constants/assets.dart';
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),
@ -20,9 +44,9 @@ class CommunityStructureHeader extends StatelessWidget {
color: ColorsManager.whiteColors,
boxShadow: [
BoxShadow(
color: ColorsManager.shadowBlackColor.withValues(alpha: 0.1),
blurRadius: 20,
offset: const Offset(0, 1),
color: ColorsManager.shadowBlackColor,
blurRadius: 8,
offset: const Offset(0, 4),
),
],
),
@ -33,7 +57,7 @@ class CommunityStructureHeader extends StatelessWidget {
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Expanded(
child: _buildCommunityInfo(context, screenWidth),
child: _buildCommunityInfo(context, theme, screenWidth),
),
const SizedBox(width: 16),
],
@ -43,7 +67,8 @@ class CommunityStructureHeader extends StatelessWidget {
);
}
Widget _buildCommunityInfo(BuildContext context, double screenWidth) {
Widget _buildCommunityInfo(
BuildContext context, ThemeData theme, double screenWidth) {
final selectedCommunity =
context.watch<CommunitiesTreeSelectionBloc>().state.selectedCommunity;
final selectedSpace =
@ -53,7 +78,7 @@ class CommunityStructureHeader extends StatelessWidget {
children: [
Text(
'Community Structure',
style: context.textTheme.headlineLarge?.copyWith(
style: theme.textTheme.headlineLarge?.copyWith(
color: ColorsManager.blackColor,
),
),
@ -66,7 +91,7 @@ class CommunityStructureHeader extends StatelessWidget {
Flexible(
child: SelectableText(
selectedCommunity.name,
style: context.textTheme.bodyLarge?.copyWith(
style: theme.textTheme.bodyLarge?.copyWith(
color: ColorsManager.blackColor,
),
maxLines: 1,
@ -90,8 +115,27 @@ class CommunityStructureHeader extends StatelessWidget {
),
),
const SizedBox(width: 8),
CommunityStructureHeaderActionButtonsComposer(
selectedCommunity: selectedCommunity,
CommunityStructureHeaderActionButtons(
onDelete: (space) {},
onDuplicate: (space) {},
onEdit: (space) => SpaceDetailsDialogHelper.showEdit(
context,
spaceModel: selectedSpace!,
communityUuid: selectedCommunity.uuid,
onSuccess: (updatedSpaceDetails) {
final communitiesBloc = context.read<CommunitiesBloc>();
final updatedSpaces = _updateRecursive(
selectedCommunity.spaces,
updatedSpaceDetails,
);
final community = selectedCommunity.copyWith(
spaces: updatedSpaces,
);
communitiesBloc.add(CommunitiesUpdateCommunity(community));
},
),
selectedSpace: selectedSpace,
),
],

View File

@ -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: [
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!),
),
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!),
),
],
],
);
}

View File

@ -1,69 +0,0 @@
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,
);
}
}

View File

@ -1,18 +1,14 @@
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.community,
required this.communityUuid,
super.key,
});
final CommunityModel community;
final String communityUuid;
@override
State<CreateSpaceButton> createState() => _CreateSpaceButtonState();
@ -29,21 +25,7 @@ class _CreateSpaceButtonState extends State<CreateSpaceButton> {
child: InkWell(
onTap: () => SpaceDetailsDialogHelper.showCreate(
context,
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,
),
);
},
communityUuid: widget.communityUuid,
),
child: MouseRegion(
onEnter: (_) => setState(() => _isHovered = true),

View File

@ -2,22 +2,31 @@ import 'package:flutter/material.dart';
import 'package:syncrow_web/utils/color_manager.dart';
class PlusButtonWidget extends StatelessWidget {
final void Function() onTap;
final Offset offset;
final void Function() onButtonTap;
const PlusButtonWidget({
required this.onTap,
super.key,
required this.offset,
required this.onButtonTap,
});
@override
Widget build(BuildContext context) {
return IconButton.filled(
onPressed: onTap,
style: IconButton.styleFrom(backgroundColor: ColorsManager.spaceColor),
icon: const Icon(
Icons.add,
color: ColorsManager.whiteColors,
size: 20,
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,
),
),
);
}

View File

@ -29,9 +29,10 @@ class _SpaceCardWidgetState extends State<SpaceCardWidget> {
widget.buildSpaceContainer(),
if (isHovered)
Positioned(
bottom: -5,
bottom: 0,
child: PlusButtonWidget(
onTap: widget.onTap,
offset: Offset.zero,
onButtonTap: widget.onTap,
),
),
],

View File

@ -20,19 +20,21 @@ class SpaceCell extends StatelessWidget {
return InkWell(
onTap: onTap,
child: Container(
padding: const EdgeInsetsDirectional.only(end: 10),
width: 150,
height: 70,
decoration: _containerDecoration(),
child: Row(
spacing: 10,
mainAxisSize: MainAxisSize.min,
children: [
_buildIconContainer(),
Text(
name,
style: context.textTheme.bodyLarge?.copyWith(
fontWeight: FontWeight.bold,
color: ColorsManager.blackColor,
const SizedBox(width: 10),
Expanded(
child: Text(
name,
style: context.textTheme.bodyLarge?.copyWith(
fontWeight: FontWeight.bold,
color: ColorsManager.blackColor,
),
overflow: TextOverflow.ellipsis,
),
),
],

View File

@ -3,9 +3,6 @@ import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:syncrow_web/pages/space_management_v2/main_module/widgets/community_structure_canvas.dart';
import 'package:syncrow_web/pages/space_management_v2/main_module/widgets/community_structure_header.dart';
import 'package:syncrow_web/pages/space_management_v2/main_module/widgets/create_space_button.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';
class SpaceManagementCommunityStructure extends StatelessWidget {
@ -13,59 +10,31 @@ class SpaceManagementCommunityStructure extends StatelessWidget {
@override
Widget build(BuildContext context) {
return BlocBuilder<CommunitiesTreeSelectionBloc, CommunitiesTreeSelectionState>(
builder: (context, state) {
final selectedCommunity = state.selectedCommunity;
final selectedSpace = state.selectedSpace;
if (selectedCommunity == null) {
return const SizedBox.shrink();
}
return Column(
mainAxisSize: MainAxisSize.min,
children: [
const CommunityStructureHeader(),
BlocBuilder<CommunitiesBloc, CommunitiesState>(
builder: (context, state) {
final community = state.communities.firstWhere(
(element) => element.uuid == selectedCommunity.uuid,
orElse: () => selectedCommunity,
);
return Visibility(
visible: community.spaces.isNotEmpty,
replacement: _buildEmptyWidget(community),
child: _buildCanvas(community, selectedSpace),
);
},
),
],
);
},
);
}
Widget _buildCanvas(
CommunityModel selectedCommunity,
SpaceModel? selectedSpace,
) {
return Expanded(
child: CommunityStructureCanvas(
community: selectedCommunity,
selectedSpace: selectedSpace,
),
);
}
Widget _buildEmptyWidget(CommunityModel selectedCommunity) {
final selectionBloc = context.watch<CommunitiesTreeSelectionBloc>().state;
final selectedCommunity = selectionBloc.selectedCommunity;
final selectedSpace = selectionBloc.selectedSpace;
const spacer = Spacer(flex: 6);
return Expanded(
child: Row(
return Visibility(
visible: selectedCommunity!.spaces.isNotEmpty,
replacement: Row(
children: [
spacer,
Expanded(child: CreateSpaceButton(community: selectedCommunity)),
spacer,
Expanded(
child: CreateSpaceButton(communityUuid: selectedCommunity.uuid),
),
spacer
],
),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
const CommunityStructureHeader(),
Expanded(
child: CommunityStructureCanvas(
community: selectedCommunity,
selectedSpace: selectedSpace,
),
),
],
),
);

View File

@ -1,63 +0,0 @@
import 'package:dio/dio.dart';
import 'package:syncrow_web/pages/common/bloc/project_manager.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/create_space/domain/params/create_space_param.dart';
import 'package:syncrow_web/pages/space_management_v2/modules/create_space/domain/services/create_space_service.dart';
import 'package:syncrow_web/services/api/api_exception.dart';
import 'package:syncrow_web/services/api/http_service.dart';
final class RemoteCreateSpaceService implements CreateSpaceService {
const RemoteCreateSpaceService(this._httpService);
final HTTPService _httpService;
static const _defaultErrorMessage = 'Failed to create space';
@override
Future<SpaceModel> createSpace(CreateSpaceParam param) async {
try {
final path = await _makeUrl(param);
final response = await _httpService.post(
path: path,
body: param.toJson(),
expectedResponseModel: (data) {
final response = data as Map<String, dynamic>;
final isSuccess = response['success'] as bool;
if (!isSuccess) {
throw APIException(response['error'] as String);
}
return SpaceModel.fromJson(response['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);
}
}
Future<String> _makeUrl(CreateSpaceParam param) async {
final projectUuid = await ProjectManager.getProjectUUID();
if (projectUuid == null || projectUuid.isEmpty) {
throw APIException('Project UUID is not set');
}
final communityUuid = param.communityUuid;
if (communityUuid.isEmpty) {
throw APIException('Community UUID is not set');
}
return '/projects/$projectUuid/communities/$communityUuid/spaces';
}
}

View File

@ -1,22 +0,0 @@
import 'package:syncrow_web/pages/space_management_v2/modules/space_details/domain/models/space_details_model.dart';
class CreateSpaceParam {
final String communityUuid;
final SpaceDetailsModel space;
final String? parentUuid;
const CreateSpaceParam({
required this.communityUuid,
required this.space,
required this.parentUuid,
});
Map<String, dynamic> toJson() {
return {
'parentUuid': parentUuid,
...space.toJson(),
'x': 0,
'y': 0,
};
}
}

View File

@ -1,6 +0,0 @@
import 'package:syncrow_web/pages/space_management_v2/modules/communities/domain/models/space_model.dart';
import 'package:syncrow_web/pages/space_management_v2/modules/create_space/domain/params/create_space_param.dart';
abstract interface class CreateSpaceService {
Future<SpaceModel> createSpace(CreateSpaceParam param);
}

View File

@ -1,34 +0,0 @@
import 'package:bloc/bloc.dart';
import 'package:equatable/equatable.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/create_space/domain/params/create_space_param.dart';
import 'package:syncrow_web/pages/space_management_v2/modules/create_space/domain/services/create_space_service.dart';
import 'package:syncrow_web/services/api/api_exception.dart';
part 'create_space_event.dart';
part 'create_space_state.dart';
class CreateSpaceBloc extends Bloc<CreateSpaceEvent, CreateSpaceState> {
CreateSpaceBloc(
this._createSpaceService,
) : super(const CreateSpaceInitial()) {
on<CreateSpace>(_onCreateSpace);
}
final CreateSpaceService _createSpaceService;
Future<void> _onCreateSpace(
CreateSpace event,
Emitter<CreateSpaceState> emit,
) async {
emit(const CreateSpaceLoading());
try {
final result = await _createSpaceService.createSpace(event.param);
emit(CreateSpaceSuccess(result));
} on APIException catch (e) {
emit(CreateSpaceFailure(e.message));
} catch (e) {
emit(CreateSpaceFailure(e.toString()));
}
}
}

View File

@ -1,17 +0,0 @@
part of 'create_space_bloc.dart';
sealed class CreateSpaceEvent extends Equatable {
const CreateSpaceEvent();
@override
List<Object> get props => [];
}
final class CreateSpace extends CreateSpaceEvent {
const CreateSpace(this.param);
final CreateSpaceParam param;
@override
List<Object> get props => [param];
}

View File

@ -1,31 +0,0 @@
part of 'create_space_bloc.dart';
sealed class CreateSpaceState extends Equatable {
const CreateSpaceState();
@override
List<Object> get props => [];
}
final class CreateSpaceInitial extends CreateSpaceState {
const CreateSpaceInitial();
}
final class CreateSpaceLoading extends CreateSpaceState {
const CreateSpaceLoading();
}
final class CreateSpaceSuccess extends CreateSpaceState {
const CreateSpaceSuccess(this.space);
final SpaceModel space;
@override
List<Object> get props => [space];
}
final class CreateSpaceFailure extends CreateSpaceState {
const CreateSpaceFailure(this.errorMessage);
final String errorMessage;
}

View File

@ -1,64 +0,0 @@
import 'package:dio/dio.dart';
import 'package:syncrow_web/pages/common/bloc/project_manager.dart';
import 'package:syncrow_web/pages/space_management_v2/modules/delete_space/domain/params/delete_space_param.dart';
import 'package:syncrow_web/pages/space_management_v2/modules/delete_space/domain/services/delete_space_service.dart';
import 'package:syncrow_web/services/api/api_exception.dart';
import 'package:syncrow_web/services/api/http_service.dart';
import 'package:syncrow_web/utils/constants/api_const.dart';
final class RemoteDeleteSpaceService implements DeleteSpaceService {
const RemoteDeleteSpaceService(this._httpService);
final HTTPService _httpService;
@override
Future<void> delete(DeleteSpaceParam param) async {
try {
await _httpService.delete(
path: await _makeUrl(param),
expectedResponseModel: (json) {
final response = json as Map<String, dynamic>;
final hasSuccessfullyDeletedSpace = response['success'] as bool? ?? false;
if (!hasSuccessfullyDeletedSpace) {
throw APIException('Failed to delete space');
}
return hasSuccessfullyDeletedSpace;
},
);
} on DioException catch (e) {
final message = e.response?.data as Map<String, dynamic>?;
throw APIException(_getErrorMessageFromBody(message));
} catch (e) {
throw APIException(e.toString());
}
}
String _getErrorMessageFromBody(Map<String, dynamic>? body) {
if (body == null) return 'Failed to delete space';
final error = body['error'] as Map<String, dynamic>?;
final errorMessage = error?['message'] as String? ?? '';
return errorMessage;
}
Future<String> _makeUrl(DeleteSpaceParam param) async {
final projectUuid = await ProjectManager.getProjectUUID();
if (projectUuid == null) {
throw APIException('Project UUID is not set');
}
if (param.communityUuid.isEmpty) {
throw APIException('Community UUID is not set');
}
if (param.spaceUuid.isEmpty) {
throw APIException('Space UUID is not set');
}
return ApiEndpoints.deleteSpace
.replaceAll('{projectId}', projectUuid)
.replaceAll('{communityId}', param.communityUuid)
.replaceAll('{spaceId}', param.spaceUuid);
}
}

View File

@ -1,9 +0,0 @@
class DeleteSpaceParam {
const DeleteSpaceParam({
required this.spaceUuid,
required this.communityUuid,
});
final String spaceUuid;
final String communityUuid;
}

View File

@ -1,5 +0,0 @@
import 'package:syncrow_web/pages/space_management_v2/modules/delete_space/domain/params/delete_space_param.dart';
abstract interface class DeleteSpaceService {
Future<void> delete(DeleteSpaceParam param);
}

View File

@ -1,31 +0,0 @@
import 'package:bloc/bloc.dart';
import 'package:equatable/equatable.dart';
import 'package:syncrow_web/pages/space_management_v2/modules/delete_space/domain/params/delete_space_param.dart';
import 'package:syncrow_web/pages/space_management_v2/modules/delete_space/domain/services/delete_space_service.dart';
import 'package:syncrow_web/services/api/api_exception.dart';
part 'delete_space_event.dart';
part 'delete_space_state.dart';
class DeleteSpaceBloc extends Bloc<DeleteSpaceEvent, DeleteSpaceState> {
DeleteSpaceBloc(this._deleteSpaceService) : super(DeleteSpaceInitial()) {
on<DeleteSpace>(_onDeleteSpace);
}
final DeleteSpaceService _deleteSpaceService;
Future<void> _onDeleteSpace(
DeleteSpace event,
Emitter<DeleteSpaceState> emit,
) async {
emit(DeleteSpaceLoading());
try {
await _deleteSpaceService.delete(event.param);
emit(const DeleteSpaceSuccess('Space deleted successfully'));
} on APIException catch (e) {
emit(DeleteSpaceFailure(e.message));
} catch (e) {
emit(DeleteSpaceFailure(e.toString()));
}
}
}

Some files were not shown because too many files have changed in this diff Show More