mirror of
https://github.com/SyncrowIOT/web.git
synced 2025-08-25 18:49:41 +00:00
Compare commits
6 Commits
6b8827f4d9
...
9d130139f7
Author | SHA1 | Date | |
---|---|---|---|
9d130139f7 | |||
bee4e05404 | |||
6db60a2a97 | |||
7f9f39811b | |||
739b491bd8 | |||
db157f30c5 |
@ -1,51 +1,62 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:dio/dio.dart';
|
||||
import 'package:syncrow_web/pages/access_management/manage_bookable_spaces/domain/models/bookable_space_model.dart';
|
||||
import 'package:syncrow_web/pages/access_management/manage_bookable_spaces/domain/params/non_bookable_spaces_params.dart';
|
||||
import 'package:syncrow_web/pages/access_management/manage_bookable_spaces/domain/service/non_bookable_spaces_service.dart';
|
||||
import 'package:syncrow_web/pages/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) async {
|
||||
try {
|
||||
final response = await _httpService.get(
|
||||
path: ApiEndpoints.bookableSpaces,
|
||||
queryParameters: {
|
||||
'configured': false,
|
||||
'page': params.currentPage,
|
||||
'search': params.searchedWords,
|
||||
},
|
||||
expectedResponseModel: (json) {
|
||||
final result = json as Map<String, dynamic>;
|
||||
return PaginatedDataModel.fromJson(
|
||||
result,
|
||||
BookableSpacemodel.fromJsonList,
|
||||
);
|
||||
},
|
||||
);
|
||||
return response;
|
||||
} on DioException catch (e) {
|
||||
final message = e.response?.data as Map<String, dynamic>?;
|
||||
final error = message?['error'] as Map<String, dynamic>?;
|
||||
final errorMessage = error?['error'] as String? ?? '';
|
||||
final formattedErrorMessage = [
|
||||
_defaultErrorMessage,
|
||||
errorMessage,
|
||||
].join(': ');
|
||||
throw APIException(formattedErrorMessage);
|
||||
} catch (e) {
|
||||
final formattedErrorMessage = [_defaultErrorMessage, '$e'].join(': ');
|
||||
throw APIException(formattedErrorMessage);
|
||||
}
|
||||
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
|
||||
|
@ -23,7 +23,7 @@ class BookableSpacesBloc
|
||||
LoadBookableSpacesEvent event, Emitter<BookableSpacesState> emit) async {
|
||||
emit(BookableSpacesLoading());
|
||||
try {
|
||||
final bookableSpaces = await bookableSpacesService.load(event.params);
|
||||
final bookableSpaces = await bookableSpacesService.load(event.param);
|
||||
emit(BookableSpacesLoaded(bookableSpacesList: bookableSpaces));
|
||||
} on APIException catch (e) {
|
||||
emit(BookableSpacesError(error: e.message));
|
||||
|
@ -8,8 +8,8 @@ sealed class BookableSpacesEvent extends Equatable {
|
||||
}
|
||||
|
||||
class LoadBookableSpacesEvent extends BookableSpacesEvent {
|
||||
final BookableSpacesParams params;
|
||||
const LoadBookableSpacesEvent(this.params);
|
||||
final BookableSpacesParams param;
|
||||
const LoadBookableSpacesEvent(this.param);
|
||||
}
|
||||
|
||||
class InsertUpdatedSpaceEvent extends BookableSpacesEvent {
|
||||
|
@ -65,20 +65,21 @@ class ManageBookableSpacesWidget extends StatelessWidget {
|
||||
child: Column(
|
||||
children: [
|
||||
Expanded(
|
||||
flex: 10, child: TopPartWidget(pageController: pageController)),
|
||||
flex: 10,
|
||||
child: RowOfButtonsTitleWidget(pageController: pageController)),
|
||||
const SizedBox(
|
||||
height: 10,
|
||||
),
|
||||
const Expanded(
|
||||
flex: 85,
|
||||
child: TablePartWidget(),
|
||||
child: TableOfBookableSpacesWidget(),
|
||||
),
|
||||
const SizedBox(
|
||||
height: 5,
|
||||
),
|
||||
const Expanded(
|
||||
flex: 5,
|
||||
child: BottomPaginationPartWidget(),
|
||||
child: PaginationButtonsWidget(),
|
||||
),
|
||||
],
|
||||
),
|
||||
|
@ -12,24 +12,37 @@ import 'package:syncrow_web/pages/access_management/manage_bookable_spaces/prese
|
||||
import 'package:syncrow_web/services/api/http_service.dart';
|
||||
import 'package:syncrow_web/utils/color_manager.dart';
|
||||
|
||||
class SetupBookableSpacesDialog extends StatelessWidget {
|
||||
final TextEditingController pointsController = TextEditingController();
|
||||
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: editingBookableSpace == null
|
||||
create: widget.editingBookableSpace == null
|
||||
? (context) => StepsCubit()..initDialogValue()
|
||||
: (context) => StepsCubit()..editValueInit(),
|
||||
),
|
||||
BlocProvider<NonBookableSpacesBloc>(
|
||||
create: editingBookableSpace == null
|
||||
create: widget.editingBookableSpace == null
|
||||
? (context) => NonBookableSpacesBloc(
|
||||
RemoteNonBookableSpaces(HTTPService()),
|
||||
)..add(
|
||||
@ -42,7 +55,7 @@ class SetupBookableSpacesDialog extends StatelessWidget {
|
||||
RemoteNonBookableSpaces(HTTPService()),
|
||||
)..add(
|
||||
EditModeSelected(
|
||||
editingBookableSpace: editingBookableSpace!),
|
||||
editingBookableSpace: widget.editingBookableSpace!),
|
||||
),
|
||||
),
|
||||
],
|
||||
@ -81,7 +94,7 @@ class SetupBookableSpacesDialog extends StatelessWidget {
|
||||
flex: 7,
|
||||
child: DetailsStepsWidget(
|
||||
pointsController: pointsController,
|
||||
editingBookableSpace: editingBookableSpace,
|
||||
editingBookableSpace: widget.editingBookableSpace,
|
||||
),
|
||||
)
|
||||
],
|
||||
@ -95,7 +108,7 @@ class SetupBookableSpacesDialog extends StatelessWidget {
|
||||
: SaveSecondStepButton(
|
||||
selectedSpaces: selectedSpaces,
|
||||
pointsController: pointsController,
|
||||
isEditingMode: editingBookableSpace != null,
|
||||
isEditingMode: widget.editingBookableSpace != null,
|
||||
);
|
||||
}),
|
||||
],
|
||||
|
@ -80,7 +80,7 @@ class ButtonsDividerBottomDialogWidget extends StatelessWidget {
|
||||
content: Text(
|
||||
nonBookableState.error,
|
||||
style:
|
||||
const TextStyle(color: ColorsManager.activeGreen),
|
||||
const TextStyle(color: ColorsManager.red),
|
||||
),
|
||||
duration: const Duration(seconds: 2),
|
||||
behavior: SnackBarBehavior.floating,
|
||||
|
@ -18,20 +18,16 @@ class DetailsStepsWidget extends StatelessWidget {
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 30, vertical: 20),
|
||||
child: BlocBuilder<StepsCubit, StepsState>(
|
||||
builder: (context, state) {
|
||||
if (state is StepOneState) {
|
||||
return const SpacesStepDetailsWidget();
|
||||
} else if (state is StepTwoState) {
|
||||
return StepTwoDetailsWidget(
|
||||
child: BlocBuilder<StepsCubit, StepsState>(builder: (context, state) {
|
||||
return switch (state) {
|
||||
StepOneState() => const SpacesStepDetailsWidget(),
|
||||
StepTwoState() => StepTwoDetailsWidget(
|
||||
pointsController: pointsController,
|
||||
editingBookableSpace:editingBookableSpace
|
||||
);
|
||||
} else {
|
||||
return const SizedBox();
|
||||
}
|
||||
},
|
||||
),
|
||||
editingBookableSpace: editingBookableSpace,
|
||||
),
|
||||
StepsInitial() => const SizedBox(),
|
||||
};
|
||||
}),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -4,8 +4,8 @@ import 'package:syncrow_web/pages/access_management/manage_bookable_spaces/domai
|
||||
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 BottomPaginationPartWidget extends StatelessWidget {
|
||||
const BottomPaginationPartWidget({super.key});
|
||||
class PaginationButtonsWidget extends StatelessWidget {
|
||||
const PaginationButtonsWidget({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
|
@ -13,8 +13,8 @@ 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 TablePartWidget extends StatelessWidget {
|
||||
const TablePartWidget({
|
||||
class TableOfBookableSpacesWidget extends StatelessWidget {
|
||||
const TableOfBookableSpacesWidget({
|
||||
super.key,
|
||||
});
|
||||
|
||||
@ -36,7 +36,7 @@ class TablePartWidget extends StatelessWidget {
|
||||
.add(LoadBookableSpacesEvent(
|
||||
BookableSpacesParams(currentPage: 1),
|
||||
)),
|
||||
child: const Text('try Again'))
|
||||
child: const Text('Try Again'))
|
||||
]);
|
||||
} else if (state is BookableSpacesLoaded) {
|
||||
return CustomDataTable<BookableSpacemodel>(
|
||||
|
@ -7,8 +7,8 @@ import 'package:syncrow_web/pages/access_management/manage_bookable_spaces/prese
|
||||
import 'package:syncrow_web/utils/color_manager.dart';
|
||||
import 'package:syncrow_web/utils/constants/assets.dart';
|
||||
|
||||
class TopPartWidget extends StatelessWidget {
|
||||
const TopPartWidget({
|
||||
class RowOfButtonsTitleWidget extends StatelessWidget {
|
||||
const RowOfButtonsTitleWidget({
|
||||
super.key,
|
||||
required this.pageController,
|
||||
});
|
||||
|
@ -1,5 +1,6 @@
|
||||
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;
|
||||
@ -27,11 +28,11 @@ class SearchUnbookableSpacesWidget extends StatelessWidget {
|
||||
height: height ?? 30,
|
||||
padding: const EdgeInsets.only(top: 4),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
color: ColorsManager.whiteColors,
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
boxShadow: const [
|
||||
BoxShadow(
|
||||
color: Color(0x26000000),
|
||||
color: ColorsManager.shadowOfSearchTextfield,
|
||||
offset: Offset(0, 4),
|
||||
blurRadius: 5,
|
||||
),
|
||||
@ -45,14 +46,15 @@ class SearchUnbookableSpacesWidget extends StatelessWidget {
|
||||
contentPadding:
|
||||
const EdgeInsets.symmetric(vertical: 5, horizontal: 15),
|
||||
hintText: title,
|
||||
hintStyle: const TextStyle(color: Colors.grey),
|
||||
hintStyle: const TextStyle(color: ColorsManager.hintTextGrey),
|
||||
border: InputBorder.none,
|
||||
suffixIcon:
|
||||
suffix ?? const Icon(Icons.search, size: 20, color: Colors.grey),
|
||||
suffixIcon: suffix ??
|
||||
const Icon(Icons.search,
|
||||
size: 20, color: ColorsManager.hintTextGrey),
|
||||
),
|
||||
style: const TextStyle(
|
||||
fontSize: 14,
|
||||
color: Colors.grey,
|
||||
color: ColorsManager.hintTextGrey,
|
||||
),
|
||||
),
|
||||
);
|
||||
|
@ -1,5 +1,3 @@
|
||||
import 'dart:async';
|
||||
|
||||
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';
|
||||
@ -19,7 +17,6 @@ class SpacesStepDetailsWidget extends StatefulWidget {
|
||||
}
|
||||
|
||||
class _SpacesStepDetailsWidgetState extends State<SpacesStepDetailsWidget> {
|
||||
Timer? _debounce;
|
||||
ScrollController scrollController = ScrollController();
|
||||
int currentPage = 1;
|
||||
String? currentSearchTerm;
|
||||
@ -80,7 +77,7 @@ class _SpacesStepDetailsWidgetState extends State<SpacesStepDetailsWidget> {
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
boxShadow: const [
|
||||
BoxShadow(
|
||||
color: Color(0x40000000),
|
||||
color: ColorsManager.shadowOfDetailsContainer,
|
||||
offset: Offset.zero,
|
||||
blurRadius: 5,
|
||||
),
|
||||
@ -94,7 +91,7 @@ class _SpacesStepDetailsWidgetState extends State<SpacesStepDetailsWidget> {
|
||||
padding:
|
||||
const EdgeInsets.symmetric(vertical: 15, horizontal: 20),
|
||||
decoration: const BoxDecoration(
|
||||
color: Color(0xFFF8F8F8),
|
||||
color: ColorsManager.circleRolesBackground,
|
||||
borderRadius: BorderRadius.vertical(
|
||||
top: Radius.circular(20),
|
||||
),
|
||||
@ -102,19 +99,16 @@ class _SpacesStepDetailsWidgetState extends State<SpacesStepDetailsWidget> {
|
||||
child: SearchUnbookableSpacesWidget(
|
||||
title: 'Search',
|
||||
onChanged: (p0) {
|
||||
if (_debounce?.isActive ?? false) _debounce!.cancel();
|
||||
_debounce = Timer(const Duration(milliseconds: 500), () {
|
||||
currentSearchTerm = p0;
|
||||
currentPage = 1;
|
||||
context.read<NonBookableSpacesBloc>().add(
|
||||
LoadUnBookableSpacesEvent(
|
||||
nonBookableSpacesParams: NonBookableSpacesParams(
|
||||
currentPage: currentPage,
|
||||
searchedWords: currentSearchTerm,
|
||||
),
|
||||
currentSearchTerm = p0;
|
||||
currentPage = 1;
|
||||
context.read<NonBookableSpacesBloc>().add(
|
||||
LoadUnBookableSpacesEvent(
|
||||
nonBookableSpacesParams: NonBookableSpacesParams(
|
||||
currentPage: currentPage,
|
||||
searchedWords: currentSearchTerm,
|
||||
),
|
||||
);
|
||||
});
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
@ -127,15 +121,13 @@ class _SpacesStepDetailsWidgetState extends State<SpacesStepDetailsWidget> {
|
||||
}
|
||||
},
|
||||
builder: (context, state) {
|
||||
if (state is NonBookableSpacesError) {
|
||||
return Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Text(state.error),
|
||||
const SizedBox(
|
||||
height: 5,
|
||||
),
|
||||
ElevatedButton(
|
||||
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(
|
||||
@ -147,28 +139,28 @@ class _SpacesStepDetailsWidgetState extends State<SpacesStepDetailsWidget> {
|
||||
),
|
||||
);
|
||||
},
|
||||
child: const Text('Try Again'))
|
||||
],
|
||||
);
|
||||
} else if (state is NonBookableSpacesLoading) {
|
||||
if (state.lastNonBookableSpaces == null) {
|
||||
return const Center(
|
||||
child: CircularProgressIndicator(),
|
||||
);
|
||||
} else {
|
||||
return UnbookableListWidget(
|
||||
child: const Text('Try Again'),
|
||||
),
|
||||
],
|
||||
),
|
||||
NonBookableSpacesLoading(lastNonBookableSpaces: null) =>
|
||||
const Center(child: CircularProgressIndicator()),
|
||||
NonBookableSpacesLoading(
|
||||
lastNonBookableSpaces: final spaces
|
||||
) =>
|
||||
UnbookableListWidget(
|
||||
scrollController: scrollController,
|
||||
nonBookableSpaces: state.lastNonBookableSpaces!,
|
||||
);
|
||||
}
|
||||
} else if (state is NonBookableSpacesLoaded) {
|
||||
return UnbookableListWidget(
|
||||
scrollController: scrollController,
|
||||
nonBookableSpaces: state.nonBookableSpaces,
|
||||
);
|
||||
} else {
|
||||
return const SizedBox();
|
||||
}
|
||||
nonBookableSpaces: spaces!,
|
||||
),
|
||||
NonBookableSpacesLoaded(
|
||||
nonBookableSpaces: final spaces
|
||||
) =>
|
||||
UnbookableListWidget(
|
||||
scrollController: scrollController,
|
||||
nonBookableSpaces: spaces,
|
||||
),
|
||||
_ => const SizedBox(),
|
||||
};
|
||||
},
|
||||
),
|
||||
)
|
||||
|
@ -7,12 +7,12 @@ import 'package:syncrow_web/utils/color_manager.dart';
|
||||
|
||||
class TimePickerWidget extends StatefulWidget {
|
||||
final String title;
|
||||
const TimePickerWidget({
|
||||
TimePickerWidget({
|
||||
super.key,
|
||||
required this.onTimePicked,
|
||||
required this.title,
|
||||
});
|
||||
|
||||
late NonBookableSpacesBloc nonBookableSpacesBloc;
|
||||
final void Function(TimeOfDay? timePicked) onTimePicked;
|
||||
@override
|
||||
State<TimePickerWidget> createState() => _TimePickerWidgetState();
|
||||
@ -20,6 +20,13 @@ class TimePickerWidget extends StatefulWidget {
|
||||
|
||||
class _TimePickerWidgetState extends State<TimePickerWidget> {
|
||||
TimeOfDay? timePicked;
|
||||
@override
|
||||
void initState() {
|
||||
widget.nonBookableSpacesBloc = context.read<NonBookableSpacesBloc>();
|
||||
|
||||
super.initState();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return InkWell(
|
||||
@ -42,7 +49,7 @@ class _TimePickerWidgetState extends State<TimePickerWidget> {
|
||||
);
|
||||
widget.onTimePicked(tempTime);
|
||||
timePicked = tempTime;
|
||||
context.read<NonBookableSpacesBloc>().add(CheckConfigurValidityEvent());
|
||||
widget.nonBookableSpacesBloc.add(CheckConfigurValidityEvent());
|
||||
setState(() {});
|
||||
},
|
||||
child: Container(
|
||||
|
@ -87,4 +87,7 @@ abstract class ColorsManager {
|
||||
static const Color grey50 = Color(0xFF718096);
|
||||
static const Color red100 = Color(0xFFFE0202);
|
||||
static const Color grey800 = Color(0xffF8F8F8);
|
||||
static const Color shadowOfSearchTextfield = Color(0x26000000);
|
||||
static const Color hintTextGrey = Colors.grey;
|
||||
static const Color shadowOfDetailsContainer = Color(0x40000000);
|
||||
}
|
||||
|
@ -7,5 +7,4 @@ class RoutesConst {
|
||||
static const String spacesManagementPage = '/spaces_management-page';
|
||||
static const String rolesAndPermissions = '/roles_and_Permissions-page';
|
||||
static const String analytics = '/syncrow_analytics';
|
||||
static const String manageBookableSapcesPage = '/manage_bookable_spaces';
|
||||
}
|
||||
|
@ -11,11 +11,7 @@ bool isEndTimeAfterStartTime(TimeOfDay start, TimeOfDay end) {
|
||||
final startMinutes = start.hour * 60 + start.minute;
|
||||
final endMinutes = end.hour * 60 + end.minute;
|
||||
|
||||
if (endMinutes <= startMinutes) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
return endMinutes <= startMinutes;
|
||||
}
|
||||
|
||||
String formatTimeOfDayTo24HourString(TimeOfDay time) {
|
||||
|
Reference in New Issue
Block a user