diff --git a/lib/core/external/date_service.dart b/lib/core/external/date_service.dart index 76d7e0c7c..f7b8dcc1d 100644 --- a/lib/core/external/date_service.dart +++ b/lib/core/external/date_service.dart @@ -1,4 +1,3 @@ class DateService { - int currentWeekday() => DateTime.now().weekday; - int currentHour() => DateTime.now().hour; + DateTime get currentDateTime => DateTime.now(); } diff --git a/lib/features/opening_hours/data/datasources/opening_hours_local_data_source.dart b/lib/features/opening_hours/data/datasources/opening_hours_local_data_source.dart index 3599cd9d6..c9c2e7211 100644 --- a/lib/features/opening_hours/data/datasources/opening_hours_local_data_source.dart +++ b/lib/features/opening_hours/data/datasources/opening_hours_local_data_source.dart @@ -1,19 +1,21 @@ import 'package:coffeecard/features/opening_hours/domain/entities/timeslot.dart'; +import 'package:flutter/material.dart'; class OpeningHoursLocalDataSource { Map getOpeningHours() { - const normalOperation = Timeslot(start: 8, end: 16); - const shortDayOperation = Timeslot(start: 8, end: 14); - const closed = Timeslot(); + const openTime = TimeOfDay(hour: 8, minute: 0); + const normalDayCloseTime = TimeOfDay(hour: 15, minute: 30); + const shortDayCloseTime = TimeOfDay(hour: 13, minute: 30); + + const normalDayOpeningHours = Timeslot(openTime, normalDayCloseTime); + const shortDayOpeningHours = Timeslot(openTime, shortDayCloseTime); return { - DateTime.monday: normalOperation, - DateTime.tuesday: normalOperation, - DateTime.wednesday: normalOperation, - DateTime.thursday: normalOperation, - DateTime.friday: shortDayOperation, - DateTime.saturday: closed, - DateTime.sunday: closed, + DateTime.monday: normalDayOpeningHours, + DateTime.tuesday: normalDayOpeningHours, + DateTime.wednesday: normalDayOpeningHours, + DateTime.thursday: normalDayOpeningHours, + DateTime.friday: shortDayOpeningHours, }; } } diff --git a/lib/features/opening_hours/data/repositories/opening_hours_repository_impl.dart b/lib/features/opening_hours/data/repositories/opening_hours_repository_impl.dart index 0901dfc78..639d7d5e2 100644 --- a/lib/features/opening_hours/data/repositories/opening_hours_repository_impl.dart +++ b/lib/features/opening_hours/data/repositories/opening_hours_repository_impl.dart @@ -1,7 +1,10 @@ import 'package:coffeecard/core/external/date_service.dart'; import 'package:coffeecard/features/opening_hours/data/datasources/opening_hours_local_data_source.dart'; import 'package:coffeecard/features/opening_hours/domain/entities/opening_hours.dart'; +import 'package:coffeecard/features/opening_hours/domain/entities/timeslot.dart'; import 'package:coffeecard/features/opening_hours/domain/repositories/opening_hours_repository.dart'; +import 'package:flutter/material.dart'; +import 'package:fpdart/fpdart.dart'; class OpeningHoursRepositoryImpl implements OpeningHoursRepository { final OpeningHoursLocalDataSource dataSource; @@ -15,9 +18,11 @@ class OpeningHoursRepositoryImpl implements OpeningHoursRepository { @override OpeningHours getOpeningHours() { final allOpeningHours = dataSource.getOpeningHours(); - final currentWeekDay = dateService.currentWeekday(); + final currentWeekday = dateService.currentDateTime.weekday; - final todaysOpeningHours = allOpeningHours[currentWeekDay]!; + final todaysOpeningHours = Option.fromNullable( + allOpeningHours[currentWeekday], + ); return OpeningHours( allOpeningHours: allOpeningHours, @@ -28,16 +33,12 @@ class OpeningHoursRepositoryImpl implements OpeningHoursRepository { @override bool isOpen() { final todaysOpeningHours = getOpeningHours().todaysOpeningHours; + final currentTimeOfDay = + TimeOfDay.fromDateTime(dateService.currentDateTime); - if (todaysOpeningHours.isClosed) { - return false; - } - - final currentHour = dateService.currentHour(); - - if (currentHour < todaysOpeningHours.start! || - currentHour > todaysOpeningHours.end!) return false; - - return true; + return todaysOpeningHours.match( + () => false, + currentTimeOfDay.isInTimeslot, + ); } } diff --git a/lib/features/opening_hours/domain/entities/opening_hours.dart b/lib/features/opening_hours/domain/entities/opening_hours.dart index 091b158a6..9e5a81a95 100644 --- a/lib/features/opening_hours/domain/entities/opening_hours.dart +++ b/lib/features/opening_hours/domain/entities/opening_hours.dart @@ -1,9 +1,10 @@ import 'package:coffeecard/features/opening_hours/domain/entities/timeslot.dart'; import 'package:equatable/equatable.dart'; +import 'package:fpdart/fpdart.dart'; class OpeningHours extends Equatable { final Map allOpeningHours; - final Timeslot todaysOpeningHours; + final Option todaysOpeningHours; const OpeningHours({ required this.allOpeningHours, diff --git a/lib/features/opening_hours/domain/entities/timeslot.dart b/lib/features/opening_hours/domain/entities/timeslot.dart index 071ba3782..f5638bed2 100644 --- a/lib/features/opening_hours/domain/entities/timeslot.dart +++ b/lib/features/opening_hours/domain/entities/timeslot.dart @@ -1,19 +1,37 @@ -import 'package:coffeecard/core/strings.dart'; import 'package:equatable/equatable.dart'; +import 'package:flutter/material.dart'; +/// A timeslot with a start and end time. class Timeslot extends Equatable { - final int? start; - final int? end; + final TimeOfDay startTime; + final TimeOfDay endTime; - const Timeslot({this.start, this.end}); + const Timeslot(this.startTime, this.endTime); - bool get isClosed => start == null || end == null; + String format(BuildContext context) { + final start = startTime.format(context); + final end = endTime.format(context); + return '$start-$end'; + } @override - String toString() => isClosed - ? Strings.closed - : '${start.toString().padLeft(2, '0')}:00 - $end:00'; + List get props => [startTime, endTime]; +} - @override - List get props => [start, end]; +/// Operators for comparing [TimeOfDay]s. +extension TimeOfDayOperators on TimeOfDay { + /// Returns true if [other] is before [this]. + bool operator <=(TimeOfDay other) { + if (hour < other.hour) { + return true; + } else if (hour == other.hour) { + return minute <= other.minute; + } else { + return false; + } + } + + bool isInTimeslot(Timeslot timeslot) { + return timeslot.startTime <= this && this <= timeslot.endTime; + } } diff --git a/lib/features/opening_hours/presentation/cubit/opening_hours_cubit.dart b/lib/features/opening_hours/presentation/cubit/opening_hours_cubit.dart index 07ef4fa2b..1994911da 100644 --- a/lib/features/opening_hours/presentation/cubit/opening_hours_cubit.dart +++ b/lib/features/opening_hours/presentation/cubit/opening_hours_cubit.dart @@ -3,6 +3,7 @@ import 'package:coffeecard/features/opening_hours/domain/entities/timeslot.dart' import 'package:coffeecard/features/opening_hours/domain/usecases/check_open_status.dart'; import 'package:coffeecard/features/opening_hours/domain/usecases/get_opening_hours.dart'; import 'package:equatable/equatable.dart'; +import 'package:fpdart/fpdart.dart'; part 'opening_hours_state.dart'; diff --git a/lib/features/opening_hours/presentation/cubit/opening_hours_state.dart b/lib/features/opening_hours/presentation/cubit/opening_hours_state.dart index 5eb964a16..4abb2444a 100644 --- a/lib/features/opening_hours/presentation/cubit/opening_hours_state.dart +++ b/lib/features/opening_hours/presentation/cubit/opening_hours_state.dart @@ -13,7 +13,7 @@ class OpeningHoursInitial extends OpeningHoursState { class OpeningHoursLoaded extends OpeningHoursState { final Map openingHours; - final Timeslot todaysOpeningHours; + final Option todaysOpeningHours; final bool isOpen; const OpeningHoursLoaded({ diff --git a/lib/features/opening_hours/presentation/pages/opening_hours_page.dart b/lib/features/opening_hours/presentation/pages/opening_hours_page.dart index ade7ebb2f..dfe0016d7 100644 --- a/lib/features/opening_hours/presentation/pages/opening_hours_page.dart +++ b/lib/features/opening_hours/presentation/pages/opening_hours_page.dart @@ -16,11 +16,6 @@ class OpeningHoursPage extends StatelessWidget { return MaterialPageRoute(builder: (_) => OpeningHoursPage(state: state)); } - List> get openingHours { - return state.openingHours.entries.toList() - ..sort((a, b) => a.key.compareTo(b.key)); - } - @override Widget build(BuildContext context) { return AppScaffold.withTitle( @@ -37,7 +32,7 @@ class OpeningHoursPage extends StatelessWidget { child: Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ - _OpeningHoursView(openingHours: openingHours), + _OpeningHoursView(openingHours: state.openingHours), const Gap(36), Row( children: [ @@ -65,18 +60,21 @@ class OpeningHoursPage extends StatelessWidget { class _OpeningHoursView extends StatelessWidget { const _OpeningHoursView({required this.openingHours}); - final List> openingHours; + final Map openingHours; @override Widget build(BuildContext context) { return ListView.separated( shrinkWrap: true, physics: const NeverScrollableScrollPhysics(), - itemCount: openingHours.length, + itemCount: Strings.weekdaysPlural.length, separatorBuilder: (_, __) => const Gap(12), - itemBuilder: (context, index) { - final weekday = openingHours[index].key; - final hours = openingHours[index].value; + itemBuilder: (_, index) { + final weekday = index + 1; + final hours = switch (openingHours[weekday]) { + final Timeslot timeslot => timeslot.format(context), + _ => Strings.closed, + }; return Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, @@ -85,7 +83,10 @@ class _OpeningHoursView extends StatelessWidget { Strings.weekdaysPlural[weekday]!, style: AppTextStyle.settingKey, ), - Text(hours.toString(), style: AppTextStyle.receiptItemKey), + Text( + hours, + style: AppTextStyle.receiptItemKey, + ), ], ); }, diff --git a/test/features/opening_hours/data/repositories/opening_hours_repository_impl_test.dart b/test/features/opening_hours/data/repositories/opening_hours_repository_impl_test.dart index cd9a0d626..cf21235ee 100644 --- a/test/features/opening_hours/data/repositories/opening_hours_repository_impl_test.dart +++ b/test/features/opening_hours/data/repositories/opening_hours_repository_impl_test.dart @@ -4,18 +4,34 @@ import 'package:coffeecard/features/opening_hours/data/repositories/opening_hour import 'package:coffeecard/features/opening_hours/domain/entities/opening_hours.dart'; import 'package:coffeecard/features/opening_hours/domain/entities/timeslot.dart'; import 'package:coffeecard/features/opening_hours/domain/repositories/opening_hours_repository.dart'; +import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; +import 'package:fpdart/fpdart.dart'; import 'package:mockito/annotations.dart'; import 'package:mockito/mockito.dart'; import 'opening_hours_repository_impl_test.mocks.dart'; -@GenerateMocks([OpeningHoursLocalDataSource, DateService]) +@GenerateNiceMocks([ + MockSpec(), + MockSpec(), +]) void main() { late MockOpeningHoursLocalDataSource dataSource; late MockDateService dateService; late OpeningHoursRepository repository; + // test values + + // monday 13th of november 2023 at 12:00 + final mondayNoon = DateTime(2023, DateTime.november, 13, 12); + + const normalTimeslot = Timeslot( + TimeOfDay(hour: 8, minute: 0), + TimeOfDay(hour: 15, minute: 30), + ); + final openMonday = {DateTime.monday: normalTimeslot}; + setUp(() { dataSource = MockOpeningHoursLocalDataSource(); dateService = MockDateService(); @@ -28,32 +44,47 @@ void main() { group('getOpeningHours', () { test('should return [OpeningHours] with data from data source', () { // arrange - const testOpeningHours = OpeningHours( - allOpeningHours: { - 0: Timeslot(), - }, - todaysOpeningHours: Timeslot(), + when(dataSource.getOpeningHours()).thenReturn(openMonday); + when(dateService.currentDateTime).thenReturn(mondayNoon); + + // act + final actual = repository.getOpeningHours(); + + // assert + expect( + actual, + OpeningHours( + allOpeningHours: openMonday, + todaysOpeningHours: const Option.of(normalTimeslot), + ), ); + }); - when(dataSource.getOpeningHours()) - .thenReturn(testOpeningHours.allOpeningHours); - when(dateService.currentWeekday()).thenReturn(0); + test('should return [OpeningHours] with empty [todaysOpeningHours]', () { + // arrange + when(dataSource.getOpeningHours()).thenReturn(openMonday); + when(dateService.currentDateTime) + .thenReturn(mondayNoon.copyWith(day: 14)); // act final actual = repository.getOpeningHours(); // assert - expect(actual, testOpeningHours); + expect( + actual, + OpeningHours( + allOpeningHours: openMonday, + todaysOpeningHours: const Option.none(), + ), + ); }); }); group('isOpen', () { test('should return [false] if today is closed', () { // arrange - when(dataSource.getOpeningHours()).thenReturn({ - 0: const Timeslot(), - }); - when(dateService.currentWeekday()).thenReturn(0); + when(dataSource.getOpeningHours()).thenReturn({}); + when(dateService.currentDateTime).thenReturn(mondayNoon); // act final actual = repository.isOpen(); @@ -63,12 +94,9 @@ void main() { }); test('should return [false] if current hour is before opening time', () { - // arrange - when(dataSource.getOpeningHours()).thenReturn({ - 0: const Timeslot(start: 1, end: 2), - }); - when(dateService.currentWeekday()).thenReturn(0); - when(dateService.currentHour()).thenReturn(0); + when(dataSource.getOpeningHours()).thenReturn(openMonday); + when(dateService.currentDateTime) + .thenReturn(mondayNoon.copyWith(hour: 7)); // act final actual = repository.isOpen(); @@ -78,11 +106,9 @@ void main() { }); test('should return [false] if current hour is after closing time', () { - when(dataSource.getOpeningHours()).thenReturn({ - 0: const Timeslot(start: 1, end: 2), - }); - when(dateService.currentWeekday()).thenReturn(0); - when(dateService.currentHour()).thenReturn(3); + when(dataSource.getOpeningHours()).thenReturn(openMonday); + when(dateService.currentDateTime) + .thenReturn(mondayNoon.copyWith(hour: 20)); // act final actual = repository.isOpen(); @@ -93,18 +119,65 @@ void main() { test( 'should return [true] if current hour is between opening and closing hour', + () { + when(dataSource.getOpeningHours()).thenReturn(openMonday); + when(dateService.currentDateTime).thenReturn(mondayNoon); + + // act + final actual = repository.isOpen(); + + // assert + expect(actual, true); + }, + ); + + test( + 'should return [true] if current minute is between opening and closing minute', + () { + when(dataSource.getOpeningHours()).thenReturn(openMonday); + when(dateService.currentDateTime) + .thenReturn(mondayNoon.copyWith(hour: 15, minute: 15)); + + // act + final actual = repository.isOpen(); + + // assert + expect(actual, true); + }, + ); + + test( + 'should return [false] if current minute is before opening minute', () { when(dataSource.getOpeningHours()).thenReturn({ - 0: const Timeslot(start: 0, end: 4), + DateTime.monday: const Timeslot( + TimeOfDay(hour: 8, minute: 30), + TimeOfDay(hour: 15, minute: 30), + ), }); - when(dateService.currentWeekday()).thenReturn(0); - when(dateService.currentHour()).thenReturn(2); + when(dateService.currentDateTime) + .thenReturn(mondayNoon.copyWith(hour: 8, minute: 15)); // act final actual = repository.isOpen(); // assert - expect(actual, true); + expect(actual, false); + }, + ); + + test( + 'should return [false] if current minute is after closing minute', + () { + when(dataSource.getOpeningHours()).thenReturn(openMonday); + when(dateService.currentDateTime) + .thenReturn(mondayNoon.copyWith(hour: 15, minute: 45)); + + // act + final actual = repository.isOpen(); + + // assert + expect(actual, false); }, ); }); diff --git a/test/features/opening_hours/domain/usecases/get_opening_hours_test.dart b/test/features/opening_hours/domain/usecases/get_opening_hours_test.dart index fac6c8487..ebd80a513 100644 --- a/test/features/opening_hours/domain/usecases/get_opening_hours_test.dart +++ b/test/features/opening_hours/domain/usecases/get_opening_hours_test.dart @@ -26,8 +26,10 @@ void main() { test('should return opening hours', () async { // arrange - const theOpeningHours = - OpeningHours(allOpeningHours: {}, todaysOpeningHours: Timeslot()); + const theOpeningHours = OpeningHours( + allOpeningHours: {}, + todaysOpeningHours: Option.none(), + ); when(repository.getOpeningHours()).thenReturn( theOpeningHours, diff --git a/test/features/opening_hours/presentation/cubit/opening_hours_cubit_test.dart b/test/features/opening_hours/presentation/cubit/opening_hours_cubit_test.dart index a01e45de3..3c9596d4e 100644 --- a/test/features/opening_hours/presentation/cubit/opening_hours_cubit_test.dart +++ b/test/features/opening_hours/presentation/cubit/opening_hours_cubit_test.dart @@ -35,8 +35,10 @@ void main() { }); group('getOpeninghours', () { - const theOpeningHours = - OpeningHours(allOpeningHours: {}, todaysOpeningHours: Timeslot()); + const theOpeningHours = OpeningHours( + allOpeningHours: {}, + todaysOpeningHours: Option.none(), + ); const isOpen = true; blocTest(