diff --git a/packages/flutter_availability/lib/src/service/availability_service.dart b/packages/flutter_availability/lib/src/service/availability_service.dart index cea7b60..557a424 100644 --- a/packages/flutter_availability/lib/src/service/availability_service.dart +++ b/packages/flutter_availability/lib/src/service/availability_service.dart @@ -1,4 +1,6 @@ +import "package:flutter/material.dart"; import "package:flutter_availability_data_interface/flutter_availability_data_interface.dart"; +import "package:rxdart/rxdart.dart"; /// class AvailabilityService { @@ -13,4 +15,108 @@ class AvailabilityService { /// final AvailabilityDataInterface dataInterface; + + /// Creates a set of availabilities for the given [range], where every + /// availability is a copy of [availability] with only date information + /// changed + Future createAvailability( + AvailabilityModel availability, + DateTimeRange range, + ) async { + await dataInterface.createAvailabilitiesForUser( + userId: userId, + availability: availability, + start: range.start, + end: range.end, + ); + } + + /// Returns a stream where data from availabilities and templates are merged + Stream> getOverviewDataForMonth( + DateTime dayInMonth, + ) { + var start = DateTime(dayInMonth.year, dayInMonth.month); + var end = DateTime(start.year, start.month + 1, 0); + + var availabilityStream = dataInterface.getAvailabilityForUser( + userId: userId, + start: start, + end: end, + ); + + return availabilityStream.switchMap((availabilities) { + var templateIds = availabilities + .map((availability) => availability.templateId) + .whereType() + .toSet() + .toList(); + + var templatesStream = dataInterface.getTemplatesForUser( + userId: userId, + templateIds: templateIds, + ); + + List combineTemplateWithAvailability( + List availabilities, + List templates, + ) { + // create a map to reduce lookup speed to O1 + var templateMap = { + for (var template in templates) ...{ + template.id: template, + }, + }; + + return [ + for (var availability in availabilities) ...[ + AvailabilityWithTemplate( + availabilityModel: availability, + template: templateMap[availability.templateId], + ), + ], + ]; + } + + return Rx.combineLatest2( + Stream.value(availabilities), + templatesStream, + combineTemplateWithAvailability, + ); + }); + } +} + +/// A combination of availability and template for a single day +class AvailabilityWithTemplate { + /// Creates + const AvailabilityWithTemplate({ + required this.availabilityModel, + required this.template, + }); + + /// the availability + final AvailabilityModel availabilityModel; + + /// the related template, if any + final AvailabilityTemplateModel? template; +} + +/// Extension to retrieve all unique templates from a combined list +extension RetrieveUniqueTemplates on List { + /// Retrieve all unique templates from a combined list + List getUniqueTemplates() => + map((entry) => entry.template) + .whereType() + .fold( + [], + (current, template) { + if (!current.any((other) => other.id == template.id)) { + return [ + ...current, + template, + ]; + } + return current; + }, + ); } diff --git a/packages/flutter_availability/lib/src/service/local_data_interface.dart b/packages/flutter_availability/lib/src/service/local_data_interface.dart index 2cdd4f7..7b93472 100644 --- a/packages/flutter_availability/lib/src/service/local_data_interface.dart +++ b/packages/flutter_availability/lib/src/service/local_data_interface.dart @@ -3,17 +3,24 @@ import "dart:async"; import "package:flutter_availability_data_interface/flutter_availability_data_interface.dart"; /// A local implementation of the [AvailabilityDataInterface] that stores data -/// in memory. +/// in memory. class LocalAvailabilityDataInterface implements AvailabilityDataInterface { final Map> _userAvailabilities = {}; + final Map> _userTemplates = {}; final StreamController>> _availabilityController = StreamController.broadcast(); + final StreamController>> + _templateController = StreamController.broadcast(); - void _notifyChanges() { + void _notifyAvailabilityChanges() { _availabilityController.add(_userAvailabilities); } + void _notifyTemplateChanges() { + _templateController.add(_userTemplates); + } + @override Future> applyTemplateForUser( String userId, @@ -21,29 +28,54 @@ class LocalAvailabilityDataInterface implements AvailabilityDataInterface { DateTime start, DateTime end, ) async { - // Implementation for applying a template - throw UnimplementedError(); + var availabilities = template.apply(start, end); + _userAvailabilities.putIfAbsent(userId, () => []).addAll(availabilities); + _notifyAvailabilityChanges(); + return availabilities; } @override - Future createAvailabilityForUser( - String userId, - AvailabilityModel availability, - ) async { + Future createAvailabilitiesForUser({ + required String userId, + required AvailabilityModel availability, + required DateTime start, + required DateTime end, + }) async { var availabilities = _userAvailabilities.putIfAbsent(userId, () => []); - var newAvailability = availability.copyWith(id: _generateId()); - availabilities.add(newAvailability); - _notifyChanges(); - return newAvailability; + + var templateData = DayTemplateData( + startTime: availability.startDate, + endTime: availability.endDate, + breaks: availability.breaks, + ); + + var newAvailabilities = templateData.apply( + start: start, + end: end, + userId: userId, + ); + + for (var newAvailability in newAvailabilities) { + availabilities.add( + newAvailability.copyWith( + id: _generateId(), + ), + ); + } + + _notifyAvailabilityChanges(); } @override Future createTemplateForUser( String userId, AvailabilityTemplateModel template, - ) { - // Implementation for creating a template - throw UnimplementedError(); + ) async { + var templates = _userTemplates.putIfAbsent(userId, () => []); + var newTemplate = template.copyWith(id: _generateId()); + templates.add(newTemplate); + _notifyTemplateChanges(); + return newTemplate; } @override @@ -55,14 +87,17 @@ class LocalAvailabilityDataInterface implements AvailabilityDataInterface { if (availabilities != null) { availabilities .removeWhere((availability) => availability.id == availabilityId); - _notifyChanges(); + _notifyAvailabilityChanges(); } } @override - Future deleteTemplateForUser(String userId, String templateId) { - // Implementation for deleting a template - throw UnimplementedError(); + Future deleteTemplateForUser(String userId, String templateId) async { + var templates = _userTemplates[userId]; + if (templates != null) { + templates.removeWhere((template) => template.id == templateId); + _notifyTemplateChanges(); + } } @override @@ -109,16 +144,32 @@ class LocalAvailabilityDataInterface implements AvailabilityDataInterface { Stream getTemplateForUserById( String userId, String templateId, - ) { - // Implementation for getting a template by ID - throw UnimplementedError(); - } + ) => + _templateController.stream.map((templatesMap) { + var templates = templatesMap[userId]; + if (templates != null) { + return templates.firstWhere((template) => template.id == templateId); + } else { + throw Exception("Template not found"); + } + }); @override - Stream> getTemplatesForUser(String userId) { - // Implementation for getting all templates for a user - throw UnimplementedError(); - } + Stream> getTemplatesForUser({ + required String userId, + List? templateIds, + }) => + _templateController.stream.map((templatesMap) { + var templates = templatesMap[userId]; + if (templateIds != null) { + return templates + ?.where((template) => templateIds.contains(template.id)) + .toList() ?? + []; + } else { + return templates ?? []; + } + }); @override Future updateAvailabilityForUser( @@ -132,7 +183,7 @@ class LocalAvailabilityDataInterface implements AvailabilityDataInterface { .indexWhere((availability) => availability.id == availabilityId); if (index != -1) { availabilities[index] = updatedModel.copyWith(id: availabilityId); - _notifyChanges(); + _notifyAvailabilityChanges(); return availabilities[index]; } } @@ -144,10 +195,20 @@ class LocalAvailabilityDataInterface implements AvailabilityDataInterface { String userId, String templateId, AvailabilityTemplateModel updatedModel, - ) { - // Implementation for updating a template - throw UnimplementedError(); + ) async { + var templates = _userTemplates[userId]; + if (templates != null) { + var index = templates.indexWhere((template) => template.id == templateId); + if (index != -1) { + templates[index] = updatedModel.copyWith(id: templateId); + _notifyTemplateChanges(); + return templates[index]; + } + } + throw Exception("Template not found"); } - String _generateId() => DateTime.now().millisecondsSinceEpoch.toString(); + int _id = 1; + + String _generateId() => (_id++).toString(); } diff --git a/packages/flutter_availability/lib/src/ui/screens/template_availability_day_overview.dart b/packages/flutter_availability/lib/src/ui/screens/template_availability_day_overview.dart index 7bc03f3..be15326 100644 --- a/packages/flutter_availability/lib/src/ui/screens/template_availability_day_overview.dart +++ b/packages/flutter_availability/lib/src/ui/screens/template_availability_day_overview.dart @@ -124,9 +124,11 @@ class _AvailabilityDayOverviewState extends State { } } else { // add an availability for the user - await service.dataInterface.createAvailabilityForUser( - userId, - _availability, + await service.dataInterface.createAvailabilitiesForUser( + userId: userId, + availability: _availability, + start: widget.date, + end: widget.date, ); } if (context.mounted) { diff --git a/packages/flutter_availability/lib/src/ui/widgets/default_base_screen.dart b/packages/flutter_availability/lib/src/ui/widgets/default_base_screen.dart index 08e745e..9bbb11f 100644 --- a/packages/flutter_availability/lib/src/ui/widgets/default_base_screen.dart +++ b/packages/flutter_availability/lib/src/ui/widgets/default_base_screen.dart @@ -3,12 +3,12 @@ import "package:flutter_availability/src/util/scope.dart"; /// Default base screen for any availability screen class DefaultBaseScreen extends StatelessWidget { - /// Create a base screen const DefaultBaseScreen({ required this.child, super.key, }); + /// Builder as default option static Widget builder( BuildContext context, diff --git a/packages/flutter_availability/lib/src/util/scope.dart b/packages/flutter_availability/lib/src/util/scope.dart index c575d8f..7fd92b4 100644 --- a/packages/flutter_availability/lib/src/util/scope.dart +++ b/packages/flutter_availability/lib/src/util/scope.dart @@ -2,9 +2,9 @@ import "package:flutter/widgets.dart"; import "package:flutter_availability/src/config/availability_options.dart"; import "package:flutter_availability/src/service/availability_service.dart"; -/// +/// class AvailabilityScope extends InheritedWidget { - /// + /// const AvailabilityScope({ required this.userId, required this.options, @@ -12,10 +12,13 @@ class AvailabilityScope extends InheritedWidget { required super.child, super.key, }); + /// final String userId; + /// final AvailabilityOptions options; + /// final AvailabilityService service; diff --git a/packages/flutter_availability/pubspec.yaml b/packages/flutter_availability/pubspec.yaml index d725a03..4baee3e 100644 --- a/packages/flutter_availability/pubspec.yaml +++ b/packages/flutter_availability/pubspec.yaml @@ -11,6 +11,7 @@ dependencies: flutter: sdk: flutter intl: any + rxdart: ^0.28.0 flutter_availability_data_interface: git: url: https://github.com/Iconica-Development/flutter_availability @@ -19,6 +20,7 @@ dependencies: dev_dependencies: flutter_test: sdk: flutter + mocktail: ^1.0.4 flutter_iconica_analysis: git: url: https://github.com/Iconica-Development/flutter_iconica_analysis diff --git a/packages/flutter_availability/test/mocks.dart b/packages/flutter_availability/test/mocks.dart new file mode 100644 index 0000000..f73eaad --- /dev/null +++ b/packages/flutter_availability/test/mocks.dart @@ -0,0 +1,8 @@ +import "package:flutter_availability_data_interface/flutter_availability_data_interface.dart"; +import "package:mocktail/mocktail.dart"; + +class DataInterfaceMock extends Mock implements AvailabilityDataInterface {} + +class MockTemplate extends Mock implements AvailabilityTemplateModel {} + +class MockAvailability extends Mock implements AvailabilityModel {} diff --git a/packages/flutter_availability/test/service/availability_service_test.dart b/packages/flutter_availability/test/service/availability_service_test.dart new file mode 100644 index 0000000..43b9266 --- /dev/null +++ b/packages/flutter_availability/test/service/availability_service_test.dart @@ -0,0 +1,102 @@ +import "package:flutter_availability/src/service/availability_service.dart"; +import "package:flutter_test/flutter_test.dart"; +import "package:mocktail/mocktail.dart"; + +import "../mocks.dart"; + +const String testUserId = "test_user"; + +void main() { + group("AvailabilityService", () { + late AvailabilityService sut; + late DataInterfaceMock dataInterfaceMock; + + setUp(() { + dataInterfaceMock = DataInterfaceMock(); + sut = AvailabilityService( + userId: testUserId, + dataInterface: dataInterfaceMock, + ); + }); + + group("getOverviewDataForMonth", () { + test("should combine availabilities and relating templates", () { + var dateTime = DateTime(2012, 12); + var templateId = "1"; + + var availabilityModel1 = MockAvailability(); + when(() => availabilityModel1.templateId).thenReturn(templateId); + var availabilityModel2 = MockAvailability(); + when(() => availabilityModel2.templateId).thenReturn(templateId); + + when( + () => dataInterfaceMock.getAvailabilityForUser( + userId: any(named: "userId"), + start: any(named: "start"), + end: any(named: "end"), + ), + ).thenAnswer( + (_) => Stream.value([ + availabilityModel1, + availabilityModel2, + ]), + ); + + var template1 = MockTemplate(); + when(() => template1.id).thenReturn(templateId); + var template2 = MockTemplate(); + when(() => template2.id).thenReturn("other"); + when( + () => dataInterfaceMock.getTemplatesForUser( + userId: any(named: "userId"), + templateIds: any(named: "templateIds"), + ), + ).thenAnswer( + (_) => Stream.value([ + template1, + template2, + ]), + ); + + var resultingStream = sut.getOverviewDataForMonth(dateTime); + + expect( + resultingStream, + emits( + isA>() + .having((e) => e.length, "has two values", equals(2)) + .having( + (e) => e, + "Contains availability 1", + contains( + predicate( + (e) => e.availabilityModel == availabilityModel1, + ), + ), + ) + .having( + (e) => e, + "Contains template 1", + contains( + predicate( + (e) => e.template == template1, + ), + ), + ) + .having( + (e) => e, + "Does not contain template 2", + isNot( + contains( + predicate( + (e) => e.template == template2, + ), + ), + ), + ), + ), + ); + }); + }); + }); +} diff --git a/packages/flutter_availability_data_interface/lib/src/data_interface.dart b/packages/flutter_availability_data_interface/lib/src/data_interface.dart index 56bab9e..52359fe 100644 --- a/packages/flutter_availability_data_interface/lib/src/data_interface.dart +++ b/packages/flutter_availability_data_interface/lib/src/data_interface.dart @@ -38,16 +38,21 @@ abstract interface class AvailabilityDataInterface { ); /// Creates a new persistant representation of an availability model. - Future createAvailabilityForUser( - String userId, - AvailabilityModel availability, - ); + Future createAvailabilitiesForUser({ + required String userId, + required AvailabilityModel availability, + required DateTime start, + required DateTime end, + }); /// Retrieves a list of templates for the given [userId]. /// /// Whether this is a one time value or a continuous stream of values is up to /// the implementation. - Stream> getTemplatesForUser(String userId); + Stream> getTemplatesForUser({ + required String userId, + List? templateIds, + }); /// Retrieves a specific template for the given /// [userId] and [templateId] diff --git a/packages/flutter_availability_data_interface/lib/src/models/templates.dart b/packages/flutter_availability_data_interface/lib/src/models/templates.dart index 55fc5f9..0bd7a5e 100644 --- a/packages/flutter_availability_data_interface/lib/src/models/templates.dart +++ b/packages/flutter_availability_data_interface/lib/src/models/templates.dart @@ -76,6 +76,25 @@ class AvailabilityTemplateModel { end: end, templateId: id, ); + + /// Copies the current properties into a new + /// instance of [AvailabilityTemplateModel], + AvailabilityTemplateModel copyWith({ + String? id, + String? userId, + String? name, + int? color, + AvailabilityTemplateType? templateType, + TemplateData? templateData, + }) => + AvailabilityTemplateModel( + id: id ?? this.id, + userId: userId ?? this.userId, + name: name ?? this.name, + color: color ?? this.color, + templateType: templateType ?? this.templateType, + templateData: templateData ?? this.templateData, + ); } /// Used as the key for defining week-based templates