feat(availability_service): load availabilities with templates and change interface

This commit is contained in:
Joey Boerwinkel 2024-07-05 13:37:18 +02:00 committed by Bart Ribbers
parent a2c6f83914
commit c15965c982
10 changed files with 351 additions and 43 deletions

View file

@ -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<void> 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<List<AvailabilityWithTemplate>> 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<String>()
.toSet()
.toList();
var templatesStream = dataInterface.getTemplatesForUser(
userId: userId,
templateIds: templateIds,
);
List<AvailabilityWithTemplate> combineTemplateWithAvailability(
List<AvailabilityModel> availabilities,
List<AvailabilityTemplateModel> 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<AvailabilityWithTemplate> {
/// Retrieve all unique templates from a combined list
List<AvailabilityTemplateModel> getUniqueTemplates() =>
map((entry) => entry.template)
.whereType<AvailabilityTemplateModel>()
.fold(
<AvailabilityTemplateModel>[],
(current, template) {
if (!current.any((other) => other.id == template.id)) {
return [
...current,
template,
];
}
return current;
},
);
}

View file

@ -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<String, List<AvailabilityModel>> _userAvailabilities = {};
final Map<String, List<AvailabilityTemplateModel>> _userTemplates = {};
final StreamController<Map<String, List<AvailabilityModel>>>
_availabilityController = StreamController.broadcast();
final StreamController<Map<String, List<AvailabilityTemplateModel>>>
_templateController = StreamController.broadcast();
void _notifyChanges() {
void _notifyAvailabilityChanges() {
_availabilityController.add(_userAvailabilities);
}
void _notifyTemplateChanges() {
_templateController.add(_userTemplates);
}
@override
Future<List<AvailabilityModel>> 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<AvailabilityModel> createAvailabilityForUser(
String userId,
AvailabilityModel availability,
) async {
Future<void> 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<AvailabilityTemplateModel> 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<void> deleteTemplateForUser(String userId, String templateId) {
// Implementation for deleting a template
throw UnimplementedError();
Future<void> 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<AvailabilityTemplateModel> 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<List<AvailabilityTemplateModel>> getTemplatesForUser(String userId) {
// Implementation for getting all templates for a user
throw UnimplementedError();
}
Stream<List<AvailabilityTemplateModel>> getTemplatesForUser({
required String userId,
List<String>? 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<AvailabilityModel> 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();
}

View file

@ -124,9 +124,11 @@ class _AvailabilityDayOverviewState extends State<AvailabilityDayOverview> {
}
} 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) {

View file

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

View file

@ -12,10 +12,13 @@ class AvailabilityScope extends InheritedWidget {
required super.child,
super.key,
});
///
final String userId;
///
final AvailabilityOptions options;
///
final AvailabilityService service;

View file

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

View file

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

View file

@ -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<List<AvailabilityWithTemplate>>()
.having((e) => e.length, "has two values", equals(2))
.having(
(e) => e,
"Contains availability 1",
contains(
predicate<AvailabilityWithTemplate>(
(e) => e.availabilityModel == availabilityModel1,
),
),
)
.having(
(e) => e,
"Contains template 1",
contains(
predicate<AvailabilityWithTemplate>(
(e) => e.template == template1,
),
),
)
.having(
(e) => e,
"Does not contain template 2",
isNot(
contains(
predicate<AvailabilityWithTemplate>(
(e) => e.template == template2,
),
),
),
),
),
);
});
});
});
}

View file

@ -38,16 +38,21 @@ abstract interface class AvailabilityDataInterface {
);
/// Creates a new persistant representation of an availability model.
Future<AvailabilityModel> createAvailabilityForUser(
String userId,
AvailabilityModel availability,
);
Future<void> 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<List<AvailabilityTemplateModel>> getTemplatesForUser(String userId);
Stream<List<AvailabilityTemplateModel>> getTemplatesForUser({
required String userId,
List<String>? templateIds,
});
/// Retrieves a specific template for the given
/// [userId] and [templateId]

View file

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