mirror of
https://github.com/Iconica-Development/flutter_availability.git
synced 2025-05-19 05:03:44 +02:00
feat(availability_service): load availabilities with templates and change interface
This commit is contained in:
parent
a2c6f83914
commit
c15965c982
10 changed files with 351 additions and 43 deletions
|
@ -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;
|
||||
},
|
||||
);
|
||||
}
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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;
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
8
packages/flutter_availability/test/mocks.dart
Normal file
8
packages/flutter_availability/test/mocks.dart
Normal 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 {}
|
|
@ -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,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
|
@ -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]
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Reference in a new issue