mirror of https://github.com/flutter/samples.git
[Compass App] Home screen with booking list (#2428)
This PR introduces the storage and retrieval of bookings.
### Server implementation
- New booking routes:
- GET `/booking`: Obtain the list ofer user bookings
- GET `/booking/{id}`: Obtain the specific booking object
- POST `/booking`: Creates a new booking
- The `BookingApiModel` objects are incomplete, the `Destination` is a
reference, not the full object, and the `Activity` list are also listed
as references only.
- Server booking always has an existing booking created (Alaska, North
America) for demo purposes.
- Storage is "in memory", stopping the server deletes all stored
bookings.
### New `BookingRepository`
- New repository class.
- Both local and remote implementations.
- Converts the `BookingApiModel` into a complete `Booking` that the app
can use, or a `BookingSummary` that only contains the necessary
information for the home screen.
### New `LocalDataService`
- Service that loads hard-coded data or from assets.
### New `HomeScreen`
- Route path: `/`
- Loads and displays the list of created bookings from the
`BookingRepository`.
- Tap on a booking title opens the `BookingScreen`.
- Floating Action Button to create a new booking.
### Changes in `BookingScreen`
- Can be accessed at the end of creating a booking or from the home
screen when tapping a booking title.
- Two commands:
- `createBooking`: Takes the stored `ItineraryConfig` and creates a
booking, the booking is stored to the `BookingRepository` (locally or on
the server).
- `loadBooking`: Takes a booking ID and loads that existing booking from
the `BookingRepository`.
- Simplified navigation: Once at `BookingScreen`, user can only navigate
back to `HomeScreen`.
- Share button converted to `FloatingActionButton`
### Integration Tests
- Updated to use new home screen.
- Updated to cover opening an existing booking in tests.
### TODO Next
- Refactor the `compass_model` project and move data model classes to
`server` and `app`, then delete project.
- Implement some user information for the home screen (e.g. retrieve
user name and profile picture url)
### Screencast
[Screencast from 2024-09-02
16-25-25.webm](https://github.com/user-attachments/assets/8aba4a61-def6-4752-a4e5-70cbed362524)
## Pre-launch Checklist
- [x] I read the [Flutter Style Guide] _recently_, and have followed its
advice.
- [x] I signed the [CLA].
- [x] I read the [Contributors Guide].
- [x] I updated/added relevant documentation (doc comments with `///`).
- [x] All existing and new tests are passing.
If you need help, consider asking for advice on the #hackers-devrel
channel on [Discord].
<!-- Links -->
[Flutter Style Guide]:
https://github.com/flutter/flutter/blob/master/docs/contributing/Style-guide-for-Flutter-repo.md
[CLA]: https://cla.developers.google.com/
[Discord]:
https://github.com/flutter/flutter/blob/master/docs/contributing/Chat.md
[Contributors Guide]:
https://github.com/flutter/samples/blob/main/CONTRIBUTING.md
pull/2444/head
parent
3be6873210
commit
fb869e729e
@ -1,37 +1,28 @@
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:compass_model/model.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
|
||||
import '../../../config/assets.dart';
|
||||
import '../../../utils/result.dart';
|
||||
import '../../services/local/local_data_service.dart';
|
||||
import 'activity_repository.dart';
|
||||
|
||||
/// Local implementation of ActivityRepository
|
||||
/// Uses data from assets folder
|
||||
class ActivityRepositoryLocal implements ActivityRepository {
|
||||
ActivityRepositoryLocal({
|
||||
required LocalDataService localDataService,
|
||||
}) : _localDataService = localDataService;
|
||||
|
||||
final LocalDataService _localDataService;
|
||||
|
||||
@override
|
||||
Future<Result<List<Activity>>> getByDestination(String ref) async {
|
||||
try {
|
||||
final localData = await _loadAsset();
|
||||
final list = _parse(localData);
|
||||
|
||||
final activities =
|
||||
list.where((activity) => activity.destinationRef == ref).toList();
|
||||
final activities = (await _localDataService.getActivities())
|
||||
.where((activity) => activity.destinationRef == ref)
|
||||
.toList();
|
||||
|
||||
return Result.ok(activities);
|
||||
} on Exception catch (error) {
|
||||
return Result.error(error);
|
||||
}
|
||||
}
|
||||
|
||||
Future<String> _loadAsset() async {
|
||||
return await rootBundle.loadString(Assets.activities);
|
||||
}
|
||||
|
||||
List<Activity> _parse(String localData) {
|
||||
final parsed = (jsonDecode(localData) as List).cast<Map<String, dynamic>>();
|
||||
|
||||
return parsed.map<Activity>((json) => Activity.fromJson(json)).toList();
|
||||
}
|
||||
}
|
||||
|
||||
@ -0,0 +1,15 @@
|
||||
import 'package:compass_model/model.dart';
|
||||
|
||||
import '../../../domain/models/booking/booking_summary.dart';
|
||||
import '../../../utils/result.dart';
|
||||
|
||||
abstract class BookingRepository {
|
||||
/// Returns the list of [BookingSummary] for the current user.
|
||||
Future<Result<List<BookingSummary>>> getBookingsList();
|
||||
|
||||
/// Returns a full [Booking] given the id.
|
||||
Future<Result<Booking>> getBooking(int id);
|
||||
|
||||
/// Creates a new [Booking].
|
||||
Future<Result<void>> createBooking(Booking booking);
|
||||
}
|
||||
@ -0,0 +1,75 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:compass_model/model.dart';
|
||||
import 'package:collection/collection.dart';
|
||||
import '../../../domain/models/booking/booking_summary.dart';
|
||||
import '../../../utils/result.dart';
|
||||
|
||||
import '../../services/local/local_data_service.dart';
|
||||
import 'booking_repository.dart';
|
||||
|
||||
class BookingRepositoryLocal implements BookingRepository {
|
||||
BookingRepositoryLocal({
|
||||
required LocalDataService localDataService,
|
||||
}) : _localDataService = localDataService;
|
||||
|
||||
final _bookings = List<Booking>.empty(growable: true);
|
||||
final LocalDataService _localDataService;
|
||||
|
||||
@override
|
||||
Future<Result<void>> createBooking(Booking booking) async {
|
||||
_bookings.add(booking);
|
||||
return Result.ok(null);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<Result<Booking>> getBooking(int id) async {
|
||||
await _createDefaultBooking();
|
||||
|
||||
if (id >= _bookings.length || id < 0) {
|
||||
return Result.error(Exception('Invalid id: $id'));
|
||||
}
|
||||
|
||||
return Result.ok(_bookings[id]);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<Result<List<BookingSummary>>> getBookingsList() async {
|
||||
await _createDefaultBooking();
|
||||
return Result.ok(_createSummaries());
|
||||
}
|
||||
|
||||
List<BookingSummary> _createSummaries() {
|
||||
return _bookings
|
||||
.mapIndexed(
|
||||
(index, booking) => BookingSummary(
|
||||
id: index,
|
||||
name:
|
||||
'${booking.destination.name}, ${booking.destination.continent}',
|
||||
startDate: booking.startDate,
|
||||
endDate: booking.endDate,
|
||||
),
|
||||
)
|
||||
.toList();
|
||||
}
|
||||
|
||||
Future<void> _createDefaultBooking() async {
|
||||
// create a default booking the first time
|
||||
if (_bookings.isEmpty) {
|
||||
final destination = (await _localDataService.getDestinations()).first;
|
||||
final activities = (await _localDataService.getActivities())
|
||||
.where((activity) => activity.destinationRef == destination.ref)
|
||||
.take(4)
|
||||
.toList();
|
||||
|
||||
_bookings.add(
|
||||
Booking(
|
||||
startDate: DateTime(2024, 1, 1),
|
||||
endDate: DateTime(2024, 2, 1),
|
||||
destination: destination,
|
||||
activity: activities,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,103 @@
|
||||
import 'package:compass_model/model.dart';
|
||||
|
||||
import '../../../domain/models/booking/booking_summary.dart';
|
||||
import '../../../utils/result.dart';
|
||||
import '../../services/api/api_client.dart';
|
||||
import '../../services/api/model/booking/booking_api_model.dart';
|
||||
import 'booking_repository.dart';
|
||||
|
||||
class BookingRepositoryRemote implements BookingRepository {
|
||||
BookingRepositoryRemote({
|
||||
required ApiClient apiClient,
|
||||
}) : _apiClient = apiClient;
|
||||
|
||||
final ApiClient _apiClient;
|
||||
|
||||
List<Destination>? _cachedDestinations;
|
||||
|
||||
@override
|
||||
Future<Result<void>> createBooking(Booking booking) async {
|
||||
try {
|
||||
final BookingApiModel bookingApiModel = BookingApiModel(
|
||||
startDate: booking.startDate,
|
||||
endDate: booking.endDate,
|
||||
name: '${booking.destination.name}, ${booking.destination.continent}',
|
||||
destinationRef: booking.destination.ref,
|
||||
activitiesRef:
|
||||
booking.activity.map((activity) => activity.ref).toList(),
|
||||
);
|
||||
return _apiClient.postBooking(bookingApiModel);
|
||||
} on Exception catch (e) {
|
||||
return Result.error(e);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Future<Result<Booking>> getBooking(int id) async {
|
||||
try {
|
||||
// Get booking by ID from server
|
||||
final resultBooking = await _apiClient.getBooking(id);
|
||||
if (resultBooking is Error<BookingApiModel>) {
|
||||
return Result.error(resultBooking.error);
|
||||
}
|
||||
final booking = resultBooking.asOk.value;
|
||||
|
||||
// Load destinations if not loaded yet
|
||||
if (_cachedDestinations == null) {
|
||||
final resultDestination = await _apiClient.getDestinations();
|
||||
if (resultDestination is Error<List<Destination>>) {
|
||||
return Result.error(resultDestination.error);
|
||||
}
|
||||
_cachedDestinations = resultDestination.asOk.value;
|
||||
}
|
||||
|
||||
// Get destination for booking
|
||||
final destination = _cachedDestinations!.firstWhere(
|
||||
(destination) => destination.ref == booking.destinationRef);
|
||||
|
||||
final resultActivities =
|
||||
await _apiClient.getActivityByDestination(destination.ref);
|
||||
|
||||
if (resultActivities is Error<List<Activity>>) {
|
||||
return Result.error(resultActivities.error);
|
||||
}
|
||||
final activities = resultActivities.asOk.value
|
||||
.where((activity) => booking.activitiesRef.contains(activity.ref))
|
||||
.toList();
|
||||
|
||||
return Result.ok(
|
||||
Booking(
|
||||
startDate: booking.startDate,
|
||||
endDate: booking.endDate,
|
||||
destination: destination,
|
||||
activity: activities,
|
||||
),
|
||||
);
|
||||
} on Exception catch (e) {
|
||||
return Result.error(e);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Future<Result<List<BookingSummary>>> getBookingsList() async {
|
||||
try {
|
||||
final result = await _apiClient.getBookings();
|
||||
if (result is Error<List<BookingApiModel>>) {
|
||||
return Result.error(result.error);
|
||||
}
|
||||
final bookingsApi = result.asOk.value;
|
||||
return Result.ok(bookingsApi
|
||||
.map(
|
||||
(bookingApi) => BookingSummary(
|
||||
id: bookingApi.id!,
|
||||
name: bookingApi.name,
|
||||
startDate: bookingApi.startDate,
|
||||
endDate: bookingApi.endDate,
|
||||
),
|
||||
)
|
||||
.toList());
|
||||
} on Exception catch (e) {
|
||||
return Result.error(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,45 +1,19 @@
|
||||
import 'package:compass_model/model.dart';
|
||||
|
||||
import '../../../utils/result.dart';
|
||||
import '../../services/local/local_data_service.dart';
|
||||
import 'continent_repository.dart';
|
||||
|
||||
/// Local data source with all possible continents.
|
||||
class ContinentRepositoryLocal implements ContinentRepository {
|
||||
ContinentRepositoryLocal({
|
||||
required LocalDataService localDataService,
|
||||
}) : _localDataService = localDataService;
|
||||
|
||||
final LocalDataService _localDataService;
|
||||
|
||||
@override
|
||||
Future<Result<List<Continent>>> getContinents() async {
|
||||
return Future.value(
|
||||
Result.ok(
|
||||
[
|
||||
const Continent(
|
||||
name: 'Europe',
|
||||
imageUrl: 'https://rstr.in/google/tripedia/TmR12QdlVTT',
|
||||
),
|
||||
const Continent(
|
||||
name: 'Asia',
|
||||
imageUrl: 'https://rstr.in/google/tripedia/VJ8BXlQg8O1',
|
||||
),
|
||||
const Continent(
|
||||
name: 'South America',
|
||||
imageUrl: 'https://rstr.in/google/tripedia/flm_-o1aI8e',
|
||||
),
|
||||
const Continent(
|
||||
name: 'Africa',
|
||||
imageUrl: 'https://rstr.in/google/tripedia/-nzi8yFOBpF',
|
||||
),
|
||||
const Continent(
|
||||
name: 'North America',
|
||||
imageUrl: 'https://rstr.in/google/tripedia/jlbgFDrSUVE',
|
||||
),
|
||||
const Continent(
|
||||
name: 'Oceania',
|
||||
imageUrl: 'https://rstr.in/google/tripedia/vxyrDE-fZVL',
|
||||
),
|
||||
const Continent(
|
||||
name: 'Australia',
|
||||
imageUrl: 'https://rstr.in/google/tripedia/z6vy6HeRyvZ',
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
return Future.value(Result.ok(_localDataService.getContinents()));
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,36 +1,25 @@
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:compass_model/model.dart';
|
||||
import 'package:flutter/services.dart' show rootBundle;
|
||||
|
||||
import '../../../config/assets.dart';
|
||||
import '../../../utils/result.dart';
|
||||
import '../../services/local/local_data_service.dart';
|
||||
import 'destination_repository.dart';
|
||||
|
||||
/// Local implementation of DestinationRepository
|
||||
/// Uses data from assets folder
|
||||
class DestinationRepositoryLocal implements DestinationRepository {
|
||||
DestinationRepositoryLocal({
|
||||
required LocalDataService localDataService,
|
||||
}) : _localDataService = localDataService;
|
||||
|
||||
final LocalDataService _localDataService;
|
||||
|
||||
/// Obtain list of destinations from local assets
|
||||
@override
|
||||
Future<Result<List<Destination>>> getDestinations() async {
|
||||
try {
|
||||
final localData = await _loadAsset();
|
||||
final list = _parse(localData);
|
||||
return Result.ok(list);
|
||||
return Result.ok(await _localDataService.getDestinations());
|
||||
} on Exception catch (error) {
|
||||
return Result.error(error);
|
||||
}
|
||||
}
|
||||
|
||||
Future<String> _loadAsset() async {
|
||||
return await rootBundle.loadString(Assets.destinations);
|
||||
}
|
||||
|
||||
List<Destination> _parse(String localData) {
|
||||
final parsed = (jsonDecode(localData) as List).cast<Map<String, dynamic>>();
|
||||
|
||||
return parsed
|
||||
.map<Destination>((json) => Destination.fromJson(json))
|
||||
.toList();
|
||||
}
|
||||
}
|
||||
|
||||
@ -0,0 +1,31 @@
|
||||
import 'package:freezed_annotation/freezed_annotation.dart';
|
||||
|
||||
part 'booking_api_model.freezed.dart';
|
||||
part 'booking_api_model.g.dart';
|
||||
|
||||
@freezed
|
||||
class BookingApiModel with _$BookingApiModel {
|
||||
const factory BookingApiModel({
|
||||
/// Booking ID. Generated when stored in server.
|
||||
int? id,
|
||||
|
||||
/// Start date of the trip
|
||||
required DateTime startDate,
|
||||
|
||||
/// End date of the trip
|
||||
required DateTime endDate,
|
||||
|
||||
/// Booking name
|
||||
/// Should be "Destination, Continent"
|
||||
required String name,
|
||||
|
||||
/// Destination of the trip
|
||||
required String destinationRef,
|
||||
|
||||
/// List of chosen activities
|
||||
required List<String> activitiesRef,
|
||||
}) = _BookingApiModel;
|
||||
|
||||
factory BookingApiModel.fromJson(Map<String, Object?> json) =>
|
||||
_$BookingApiModelFromJson(json);
|
||||
}
|
||||
@ -0,0 +1,317 @@
|
||||
// coverage:ignore-file
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
// ignore_for_file: type=lint
|
||||
// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark
|
||||
|
||||
part of 'booking_api_model.dart';
|
||||
|
||||
// **************************************************************************
|
||||
// FreezedGenerator
|
||||
// **************************************************************************
|
||||
|
||||
T _$identity<T>(T value) => value;
|
||||
|
||||
final _privateConstructorUsedError = UnsupportedError(
|
||||
'It seems like you constructed your class using `MyClass._()`. This constructor is only meant to be used by freezed and you are not supposed to need it nor use it.\nPlease check the documentation here for more information: https://github.com/rrousselGit/freezed#adding-getters-and-methods-to-our-models');
|
||||
|
||||
BookingApiModel _$BookingApiModelFromJson(Map<String, dynamic> json) {
|
||||
return _BookingApiModel.fromJson(json);
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
mixin _$BookingApiModel {
|
||||
/// Booking ID. Generated when stored in server.
|
||||
int? get id => throw _privateConstructorUsedError;
|
||||
|
||||
/// Start date of the trip
|
||||
DateTime get startDate => throw _privateConstructorUsedError;
|
||||
|
||||
/// End date of the trip
|
||||
DateTime get endDate => throw _privateConstructorUsedError;
|
||||
|
||||
/// Booking name
|
||||
/// Should be "Destination, Continent"
|
||||
String get name => throw _privateConstructorUsedError;
|
||||
|
||||
/// Destination of the trip
|
||||
String get destinationRef => throw _privateConstructorUsedError;
|
||||
|
||||
/// List of chosen activities
|
||||
List<String> get activitiesRef => throw _privateConstructorUsedError;
|
||||
|
||||
/// Serializes this BookingApiModel to a JSON map.
|
||||
Map<String, dynamic> toJson() => throw _privateConstructorUsedError;
|
||||
|
||||
/// Create a copy of BookingApiModel
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||
$BookingApiModelCopyWith<BookingApiModel> get copyWith =>
|
||||
throw _privateConstructorUsedError;
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
abstract class $BookingApiModelCopyWith<$Res> {
|
||||
factory $BookingApiModelCopyWith(
|
||||
BookingApiModel value, $Res Function(BookingApiModel) then) =
|
||||
_$BookingApiModelCopyWithImpl<$Res, BookingApiModel>;
|
||||
@useResult
|
||||
$Res call(
|
||||
{int? id,
|
||||
DateTime startDate,
|
||||
DateTime endDate,
|
||||
String name,
|
||||
String destinationRef,
|
||||
List<String> activitiesRef});
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
class _$BookingApiModelCopyWithImpl<$Res, $Val extends BookingApiModel>
|
||||
implements $BookingApiModelCopyWith<$Res> {
|
||||
_$BookingApiModelCopyWithImpl(this._value, this._then);
|
||||
|
||||
// ignore: unused_field
|
||||
final $Val _value;
|
||||
// ignore: unused_field
|
||||
final $Res Function($Val) _then;
|
||||
|
||||
/// Create a copy of BookingApiModel
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@pragma('vm:prefer-inline')
|
||||
@override
|
||||
$Res call({
|
||||
Object? id = freezed,
|
||||
Object? startDate = null,
|
||||
Object? endDate = null,
|
||||
Object? name = null,
|
||||
Object? destinationRef = null,
|
||||
Object? activitiesRef = null,
|
||||
}) {
|
||||
return _then(_value.copyWith(
|
||||
id: freezed == id
|
||||
? _value.id
|
||||
: id // ignore: cast_nullable_to_non_nullable
|
||||
as int?,
|
||||
startDate: null == startDate
|
||||
? _value.startDate
|
||||
: startDate // ignore: cast_nullable_to_non_nullable
|
||||
as DateTime,
|
||||
endDate: null == endDate
|
||||
? _value.endDate
|
||||
: endDate // ignore: cast_nullable_to_non_nullable
|
||||
as DateTime,
|
||||
name: null == name
|
||||
? _value.name
|
||||
: name // ignore: cast_nullable_to_non_nullable
|
||||
as String,
|
||||
destinationRef: null == destinationRef
|
||||
? _value.destinationRef
|
||||
: destinationRef // ignore: cast_nullable_to_non_nullable
|
||||
as String,
|
||||
activitiesRef: null == activitiesRef
|
||||
? _value.activitiesRef
|
||||
: activitiesRef // ignore: cast_nullable_to_non_nullable
|
||||
as List<String>,
|
||||
) as $Val);
|
||||
}
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
abstract class _$$BookingApiModelImplCopyWith<$Res>
|
||||
implements $BookingApiModelCopyWith<$Res> {
|
||||
factory _$$BookingApiModelImplCopyWith(_$BookingApiModelImpl value,
|
||||
$Res Function(_$BookingApiModelImpl) then) =
|
||||
__$$BookingApiModelImplCopyWithImpl<$Res>;
|
||||
@override
|
||||
@useResult
|
||||
$Res call(
|
||||
{int? id,
|
||||
DateTime startDate,
|
||||
DateTime endDate,
|
||||
String name,
|
||||
String destinationRef,
|
||||
List<String> activitiesRef});
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
class __$$BookingApiModelImplCopyWithImpl<$Res>
|
||||
extends _$BookingApiModelCopyWithImpl<$Res, _$BookingApiModelImpl>
|
||||
implements _$$BookingApiModelImplCopyWith<$Res> {
|
||||
__$$BookingApiModelImplCopyWithImpl(
|
||||
_$BookingApiModelImpl _value, $Res Function(_$BookingApiModelImpl) _then)
|
||||
: super(_value, _then);
|
||||
|
||||
/// Create a copy of BookingApiModel
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@pragma('vm:prefer-inline')
|
||||
@override
|
||||
$Res call({
|
||||
Object? id = freezed,
|
||||
Object? startDate = null,
|
||||
Object? endDate = null,
|
||||
Object? name = null,
|
||||
Object? destinationRef = null,
|
||||
Object? activitiesRef = null,
|
||||
}) {
|
||||
return _then(_$BookingApiModelImpl(
|
||||
id: freezed == id
|
||||
? _value.id
|
||||
: id // ignore: cast_nullable_to_non_nullable
|
||||
as int?,
|
||||
startDate: null == startDate
|
||||
? _value.startDate
|
||||
: startDate // ignore: cast_nullable_to_non_nullable
|
||||
as DateTime,
|
||||
endDate: null == endDate
|
||||
? _value.endDate
|
||||
: endDate // ignore: cast_nullable_to_non_nullable
|
||||
as DateTime,
|
||||
name: null == name
|
||||
? _value.name
|
||||
: name // ignore: cast_nullable_to_non_nullable
|
||||
as String,
|
||||
destinationRef: null == destinationRef
|
||||
? _value.destinationRef
|
||||
: destinationRef // ignore: cast_nullable_to_non_nullable
|
||||
as String,
|
||||
activitiesRef: null == activitiesRef
|
||||
? _value._activitiesRef
|
||||
: activitiesRef // ignore: cast_nullable_to_non_nullable
|
||||
as List<String>,
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
@JsonSerializable()
|
||||
class _$BookingApiModelImpl implements _BookingApiModel {
|
||||
const _$BookingApiModelImpl(
|
||||
{this.id,
|
||||
required this.startDate,
|
||||
required this.endDate,
|
||||
required this.name,
|
||||
required this.destinationRef,
|
||||
required final List<String> activitiesRef})
|
||||
: _activitiesRef = activitiesRef;
|
||||
|
||||
factory _$BookingApiModelImpl.fromJson(Map<String, dynamic> json) =>
|
||||
_$$BookingApiModelImplFromJson(json);
|
||||
|
||||
/// Booking ID. Generated when stored in server.
|
||||
@override
|
||||
final int? id;
|
||||
|
||||
/// Start date of the trip
|
||||
@override
|
||||
final DateTime startDate;
|
||||
|
||||
/// End date of the trip
|
||||
@override
|
||||
final DateTime endDate;
|
||||
|
||||
/// Booking name
|
||||
/// Should be "Destination, Continent"
|
||||
@override
|
||||
final String name;
|
||||
|
||||
/// Destination of the trip
|
||||
@override
|
||||
final String destinationRef;
|
||||
|
||||
/// List of chosen activities
|
||||
final List<String> _activitiesRef;
|
||||
|
||||
/// List of chosen activities
|
||||
@override
|
||||
List<String> get activitiesRef {
|
||||
if (_activitiesRef is EqualUnmodifiableListView) return _activitiesRef;
|
||||
// ignore: implicit_dynamic_type
|
||||
return EqualUnmodifiableListView(_activitiesRef);
|
||||
}
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'BookingApiModel(id: $id, startDate: $startDate, endDate: $endDate, name: $name, destinationRef: $destinationRef, activitiesRef: $activitiesRef)';
|
||||
}
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
return identical(this, other) ||
|
||||
(other.runtimeType == runtimeType &&
|
||||
other is _$BookingApiModelImpl &&
|
||||
(identical(other.id, id) || other.id == id) &&
|
||||
(identical(other.startDate, startDate) ||
|
||||
other.startDate == startDate) &&
|
||||
(identical(other.endDate, endDate) || other.endDate == endDate) &&
|
||||
(identical(other.name, name) || other.name == name) &&
|
||||
(identical(other.destinationRef, destinationRef) ||
|
||||
other.destinationRef == destinationRef) &&
|
||||
const DeepCollectionEquality()
|
||||
.equals(other._activitiesRef, _activitiesRef));
|
||||
}
|
||||
|
||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||
@override
|
||||
int get hashCode => Object.hash(runtimeType, id, startDate, endDate, name,
|
||||
destinationRef, const DeepCollectionEquality().hash(_activitiesRef));
|
||||
|
||||
/// Create a copy of BookingApiModel
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||
@override
|
||||
@pragma('vm:prefer-inline')
|
||||
_$$BookingApiModelImplCopyWith<_$BookingApiModelImpl> get copyWith =>
|
||||
__$$BookingApiModelImplCopyWithImpl<_$BookingApiModelImpl>(
|
||||
this, _$identity);
|
||||
|
||||
@override
|
||||
Map<String, dynamic> toJson() {
|
||||
return _$$BookingApiModelImplToJson(
|
||||
this,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
abstract class _BookingApiModel implements BookingApiModel {
|
||||
const factory _BookingApiModel(
|
||||
{final int? id,
|
||||
required final DateTime startDate,
|
||||
required final DateTime endDate,
|
||||
required final String name,
|
||||
required final String destinationRef,
|
||||
required final List<String> activitiesRef}) = _$BookingApiModelImpl;
|
||||
|
||||
factory _BookingApiModel.fromJson(Map<String, dynamic> json) =
|
||||
_$BookingApiModelImpl.fromJson;
|
||||
|
||||
/// Booking ID. Generated when stored in server.
|
||||
@override
|
||||
int? get id;
|
||||
|
||||
/// Start date of the trip
|
||||
@override
|
||||
DateTime get startDate;
|
||||
|
||||
/// End date of the trip
|
||||
@override
|
||||
DateTime get endDate;
|
||||
|
||||
/// Booking name
|
||||
/// Should be "Destination, Continent"
|
||||
@override
|
||||
String get name;
|
||||
|
||||
/// Destination of the trip
|
||||
@override
|
||||
String get destinationRef;
|
||||
|
||||
/// List of chosen activities
|
||||
@override
|
||||
List<String> get activitiesRef;
|
||||
|
||||
/// Create a copy of BookingApiModel
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@override
|
||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||
_$$BookingApiModelImplCopyWith<_$BookingApiModelImpl> get copyWith =>
|
||||
throw _privateConstructorUsedError;
|
||||
}
|
||||
@ -0,0 +1,31 @@
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
|
||||
part of 'booking_api_model.dart';
|
||||
|
||||
// **************************************************************************
|
||||
// JsonSerializableGenerator
|
||||
// **************************************************************************
|
||||
|
||||
_$BookingApiModelImpl _$$BookingApiModelImplFromJson(
|
||||
Map<String, dynamic> json) =>
|
||||
_$BookingApiModelImpl(
|
||||
id: (json['id'] as num?)?.toInt(),
|
||||
startDate: DateTime.parse(json['startDate'] as String),
|
||||
endDate: DateTime.parse(json['endDate'] as String),
|
||||
name: json['name'] as String,
|
||||
destinationRef: json['destinationRef'] as String,
|
||||
activitiesRef: (json['activitiesRef'] as List<dynamic>)
|
||||
.map((e) => e as String)
|
||||
.toList(),
|
||||
);
|
||||
|
||||
Map<String, dynamic> _$$BookingApiModelImplToJson(
|
||||
_$BookingApiModelImpl instance) =>
|
||||
<String, dynamic>{
|
||||
'id': instance.id,
|
||||
'startDate': instance.startDate.toIso8601String(),
|
||||
'endDate': instance.endDate.toIso8601String(),
|
||||
'name': instance.name,
|
||||
'destinationRef': instance.destinationRef,
|
||||
'activitiesRef': instance.activitiesRef,
|
||||
};
|
||||
@ -0,0 +1,56 @@
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:compass_model/model.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
|
||||
import '../../../config/assets.dart';
|
||||
|
||||
class LocalDataService {
|
||||
List<Continent> getContinents() {
|
||||
return [
|
||||
const Continent(
|
||||
name: 'Europe',
|
||||
imageUrl: 'https://rstr.in/google/tripedia/TmR12QdlVTT',
|
||||
),
|
||||
const Continent(
|
||||
name: 'Asia',
|
||||
imageUrl: 'https://rstr.in/google/tripedia/VJ8BXlQg8O1',
|
||||
),
|
||||
const Continent(
|
||||
name: 'South America',
|
||||
imageUrl: 'https://rstr.in/google/tripedia/flm_-o1aI8e',
|
||||
),
|
||||
const Continent(
|
||||
name: 'Africa',
|
||||
imageUrl: 'https://rstr.in/google/tripedia/-nzi8yFOBpF',
|
||||
),
|
||||
const Continent(
|
||||
name: 'North America',
|
||||
imageUrl: 'https://rstr.in/google/tripedia/jlbgFDrSUVE',
|
||||
),
|
||||
const Continent(
|
||||
name: 'Oceania',
|
||||
imageUrl: 'https://rstr.in/google/tripedia/vxyrDE-fZVL',
|
||||
),
|
||||
const Continent(
|
||||
name: 'Australia',
|
||||
imageUrl: 'https://rstr.in/google/tripedia/z6vy6HeRyvZ',
|
||||
),
|
||||
];
|
||||
}
|
||||
|
||||
Future<List<Activity>> getActivities() async {
|
||||
final json = await _loadStringAsset(Assets.activities);
|
||||
return json.map<Activity>((json) => Activity.fromJson(json)).toList();
|
||||
}
|
||||
|
||||
Future<List<Destination>> getDestinations() async {
|
||||
final json = await _loadStringAsset(Assets.destinations);
|
||||
return json.map<Destination>((json) => Destination.fromJson(json)).toList();
|
||||
}
|
||||
|
||||
Future<List<Map<String, dynamic>>> _loadStringAsset(String asset) async {
|
||||
final localData = await rootBundle.loadString(asset);
|
||||
return (jsonDecode(localData) as List).cast<Map<String, dynamic>>();
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,30 @@
|
||||
import 'package:freezed_annotation/freezed_annotation.dart';
|
||||
|
||||
part 'booking_summary.freezed.dart';
|
||||
part 'booking_summary.g.dart';
|
||||
|
||||
/// BookingSummary contains the necessary data to display a booking
|
||||
/// in the user home screen, but lacks the rest of the booking data
|
||||
/// like activitities or destination.
|
||||
///
|
||||
/// Use the [BookingRepository] to obtain a full [Booking]
|
||||
/// using the [BookingSummary.id].
|
||||
@freezed
|
||||
class BookingSummary with _$BookingSummary {
|
||||
const factory BookingSummary({
|
||||
/// Booking id
|
||||
required int id,
|
||||
|
||||
/// Name to be displayed
|
||||
required String name,
|
||||
|
||||
/// Start date of the booking
|
||||
required DateTime startDate,
|
||||
|
||||
/// End date of the booking
|
||||
required DateTime endDate,
|
||||
}) = _BookingSummary;
|
||||
|
||||
factory BookingSummary.fromJson(Map<String, Object?> json) =>
|
||||
_$BookingSummaryFromJson(json);
|
||||
}
|
||||
@ -0,0 +1,243 @@
|
||||
// coverage:ignore-file
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
// ignore_for_file: type=lint
|
||||
// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark
|
||||
|
||||
part of 'booking_summary.dart';
|
||||
|
||||
// **************************************************************************
|
||||
// FreezedGenerator
|
||||
// **************************************************************************
|
||||
|
||||
T _$identity<T>(T value) => value;
|
||||
|
||||
final _privateConstructorUsedError = UnsupportedError(
|
||||
'It seems like you constructed your class using `MyClass._()`. This constructor is only meant to be used by freezed and you are not supposed to need it nor use it.\nPlease check the documentation here for more information: https://github.com/rrousselGit/freezed#adding-getters-and-methods-to-our-models');
|
||||
|
||||
BookingSummary _$BookingSummaryFromJson(Map<String, dynamic> json) {
|
||||
return _BookingSummary.fromJson(json);
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
mixin _$BookingSummary {
|
||||
/// Booking id
|
||||
int get id => throw _privateConstructorUsedError;
|
||||
|
||||
/// Destination name to be displayed.
|
||||
String get name => throw _privateConstructorUsedError;
|
||||
|
||||
/// Start date of the booking.
|
||||
DateTime get startDate => throw _privateConstructorUsedError;
|
||||
|
||||
/// End date of the booking
|
||||
DateTime get endDate => throw _privateConstructorUsedError;
|
||||
|
||||
/// Serializes this BookingSummary to a JSON map.
|
||||
Map<String, dynamic> toJson() => throw _privateConstructorUsedError;
|
||||
|
||||
/// Create a copy of BookingSummary
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||
$BookingSummaryCopyWith<BookingSummary> get copyWith =>
|
||||
throw _privateConstructorUsedError;
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
abstract class $BookingSummaryCopyWith<$Res> {
|
||||
factory $BookingSummaryCopyWith(
|
||||
BookingSummary value, $Res Function(BookingSummary) then) =
|
||||
_$BookingSummaryCopyWithImpl<$Res, BookingSummary>;
|
||||
@useResult
|
||||
$Res call({int id, String name, DateTime startDate, DateTime endDate});
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
class _$BookingSummaryCopyWithImpl<$Res, $Val extends BookingSummary>
|
||||
implements $BookingSummaryCopyWith<$Res> {
|
||||
_$BookingSummaryCopyWithImpl(this._value, this._then);
|
||||
|
||||
// ignore: unused_field
|
||||
final $Val _value;
|
||||
// ignore: unused_field
|
||||
final $Res Function($Val) _then;
|
||||
|
||||
/// Create a copy of BookingSummary
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@pragma('vm:prefer-inline')
|
||||
@override
|
||||
$Res call({
|
||||
Object? id = null,
|
||||
Object? name = null,
|
||||
Object? startDate = null,
|
||||
Object? endDate = null,
|
||||
}) {
|
||||
return _then(_value.copyWith(
|
||||
id: null == id
|
||||
? _value.id
|
||||
: id // ignore: cast_nullable_to_non_nullable
|
||||
as int,
|
||||
name: null == name
|
||||
? _value.name
|
||||
: name // ignore: cast_nullable_to_non_nullable
|
||||
as String,
|
||||
startDate: null == startDate
|
||||
? _value.startDate
|
||||
: startDate // ignore: cast_nullable_to_non_nullable
|
||||
as DateTime,
|
||||
endDate: null == endDate
|
||||
? _value.endDate
|
||||
: endDate // ignore: cast_nullable_to_non_nullable
|
||||
as DateTime,
|
||||
) as $Val);
|
||||
}
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
abstract class _$$BookingSummaryImplCopyWith<$Res>
|
||||
implements $BookingSummaryCopyWith<$Res> {
|
||||
factory _$$BookingSummaryImplCopyWith(_$BookingSummaryImpl value,
|
||||
$Res Function(_$BookingSummaryImpl) then) =
|
||||
__$$BookingSummaryImplCopyWithImpl<$Res>;
|
||||
@override
|
||||
@useResult
|
||||
$Res call({int id, String name, DateTime startDate, DateTime endDate});
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
class __$$BookingSummaryImplCopyWithImpl<$Res>
|
||||
extends _$BookingSummaryCopyWithImpl<$Res, _$BookingSummaryImpl>
|
||||
implements _$$BookingSummaryImplCopyWith<$Res> {
|
||||
__$$BookingSummaryImplCopyWithImpl(
|
||||
_$BookingSummaryImpl _value, $Res Function(_$BookingSummaryImpl) _then)
|
||||
: super(_value, _then);
|
||||
|
||||
/// Create a copy of BookingSummary
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@pragma('vm:prefer-inline')
|
||||
@override
|
||||
$Res call({
|
||||
Object? id = null,
|
||||
Object? name = null,
|
||||
Object? startDate = null,
|
||||
Object? endDate = null,
|
||||
}) {
|
||||
return _then(_$BookingSummaryImpl(
|
||||
id: null == id
|
||||
? _value.id
|
||||
: id // ignore: cast_nullable_to_non_nullable
|
||||
as int,
|
||||
name: null == name
|
||||
? _value.name
|
||||
: name // ignore: cast_nullable_to_non_nullable
|
||||
as String,
|
||||
startDate: null == startDate
|
||||
? _value.startDate
|
||||
: startDate // ignore: cast_nullable_to_non_nullable
|
||||
as DateTime,
|
||||
endDate: null == endDate
|
||||
? _value.endDate
|
||||
: endDate // ignore: cast_nullable_to_non_nullable
|
||||
as DateTime,
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
@JsonSerializable()
|
||||
class _$BookingSummaryImpl implements _BookingSummary {
|
||||
const _$BookingSummaryImpl(
|
||||
{required this.id,
|
||||
required this.name,
|
||||
required this.startDate,
|
||||
required this.endDate});
|
||||
|
||||
factory _$BookingSummaryImpl.fromJson(Map<String, dynamic> json) =>
|
||||
_$$BookingSummaryImplFromJson(json);
|
||||
|
||||
/// Booking id
|
||||
@override
|
||||
final int id;
|
||||
|
||||
/// Destination name to be displayed.
|
||||
@override
|
||||
final String name;
|
||||
|
||||
/// Start date of the booking.
|
||||
@override
|
||||
final DateTime startDate;
|
||||
|
||||
/// End date of the booking
|
||||
@override
|
||||
final DateTime endDate;
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'BookingSummary(id: $id, name: $name, startDate: $startDate, endDate: $endDate)';
|
||||
}
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
return identical(this, other) ||
|
||||
(other.runtimeType == runtimeType &&
|
||||
other is _$BookingSummaryImpl &&
|
||||
(identical(other.id, id) || other.id == id) &&
|
||||
(identical(other.name, name) || other.name == name) &&
|
||||
(identical(other.startDate, startDate) ||
|
||||
other.startDate == startDate) &&
|
||||
(identical(other.endDate, endDate) || other.endDate == endDate));
|
||||
}
|
||||
|
||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||
@override
|
||||
int get hashCode => Object.hash(runtimeType, id, name, startDate, endDate);
|
||||
|
||||
/// Create a copy of BookingSummary
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||
@override
|
||||
@pragma('vm:prefer-inline')
|
||||
_$$BookingSummaryImplCopyWith<_$BookingSummaryImpl> get copyWith =>
|
||||
__$$BookingSummaryImplCopyWithImpl<_$BookingSummaryImpl>(
|
||||
this, _$identity);
|
||||
|
||||
@override
|
||||
Map<String, dynamic> toJson() {
|
||||
return _$$BookingSummaryImplToJson(
|
||||
this,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
abstract class _BookingSummary implements BookingSummary {
|
||||
const factory _BookingSummary(
|
||||
{required final int id,
|
||||
required final String name,
|
||||
required final DateTime startDate,
|
||||
required final DateTime endDate}) = _$BookingSummaryImpl;
|
||||
|
||||
factory _BookingSummary.fromJson(Map<String, dynamic> json) =
|
||||
_$BookingSummaryImpl.fromJson;
|
||||
|
||||
/// Booking id
|
||||
@override
|
||||
int get id;
|
||||
|
||||
/// Destination name to be displayed.
|
||||
@override
|
||||
String get name;
|
||||
|
||||
/// Start date of the booking.
|
||||
@override
|
||||
DateTime get startDate;
|
||||
|
||||
/// End date of the booking
|
||||
@override
|
||||
DateTime get endDate;
|
||||
|
||||
/// Create a copy of BookingSummary
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@override
|
||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||
_$$BookingSummaryImplCopyWith<_$BookingSummaryImpl> get copyWith =>
|
||||
throw _privateConstructorUsedError;
|
||||
}
|
||||
@ -0,0 +1,24 @@
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
|
||||
part of 'booking_summary.dart';
|
||||
|
||||
// **************************************************************************
|
||||
// JsonSerializableGenerator
|
||||
// **************************************************************************
|
||||
|
||||
_$BookingSummaryImpl _$$BookingSummaryImplFromJson(Map<String, dynamic> json) =>
|
||||
_$BookingSummaryImpl(
|
||||
id: (json['id'] as num).toInt(),
|
||||
name: json['name'] as String,
|
||||
startDate: DateTime.parse(json['startDate'] as String),
|
||||
endDate: DateTime.parse(json['endDate'] as String),
|
||||
);
|
||||
|
||||
Map<String, dynamic> _$$BookingSummaryImplToJson(
|
||||
_$BookingSummaryImpl instance) =>
|
||||
<String, dynamic>{
|
||||
'id': instance.id,
|
||||
'name': instance.name,
|
||||
'startDate': instance.startDate.toIso8601String(),
|
||||
'endDate': instance.endDate.toIso8601String(),
|
||||
};
|
||||
@ -0,0 +1,13 @@
|
||||
class Routes {
|
||||
static const home = '/';
|
||||
static const login = '/login';
|
||||
static const search = '/$searchRelative';
|
||||
static const searchRelative = 'search';
|
||||
static const results = '/$resultsRelative';
|
||||
static const resultsRelative = 'results';
|
||||
static const activities = '/$activitiesRelative';
|
||||
static const activitiesRelative = 'activities';
|
||||
static const booking = '/$bookingRelative';
|
||||
static const bookingRelative = 'booking';
|
||||
static String bookingWithId(int id) => '$booking/$id';
|
||||
}
|
||||
@ -1,50 +0,0 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import '../../core/localization/applocalization.dart';
|
||||
import '../../core/themes/dimens.dart';
|
||||
import '../../core/ui/blur_filter.dart';
|
||||
import '../view_models/booking_viewmodel.dart';
|
||||
|
||||
class BookingShareButton extends StatelessWidget {
|
||||
const BookingShareButton({
|
||||
super.key,
|
||||
required this.viewModel,
|
||||
});
|
||||
|
||||
final BookingViewModel viewModel;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Positioned(
|
||||
left: 0,
|
||||
right: 0,
|
||||
bottom: 0,
|
||||
child: SafeArea(
|
||||
bottom: true,
|
||||
top: false,
|
||||
child: ClipRect(
|
||||
child: SizedBox(
|
||||
height: (Dimens.of(context).paddingScreenVertical * 2) + 64,
|
||||
child: BackdropFilter(
|
||||
filter: kBlurFilter,
|
||||
child: Padding(
|
||||
padding: Dimens.of(context).edgeInsetsScreenSymmetric,
|
||||
child: ListenableBuilder(
|
||||
listenable: viewModel,
|
||||
builder: (context, _) {
|
||||
return FilledButton(
|
||||
key: const Key('share-button'),
|
||||
onPressed: viewModel.booking != null
|
||||
? viewModel.shareBooking.execute
|
||||
: null,
|
||||
child: Text(AppLocalization.of(context).shareTrip),
|
||||
);
|
||||
}),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,41 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:logging/logging.dart';
|
||||
|
||||
import '../../../data/repositories/booking/booking_repository.dart';
|
||||
import '../../../domain/models/booking/booking_summary.dart';
|
||||
import '../../../utils/command.dart';
|
||||
import '../../../utils/result.dart';
|
||||
|
||||
class HomeViewModel extends ChangeNotifier {
|
||||
HomeViewModel({
|
||||
required BookingRepository bookingRepository,
|
||||
}) : _bookingRepository = bookingRepository {
|
||||
load = Command0(_load)..execute();
|
||||
}
|
||||
|
||||
final BookingRepository _bookingRepository;
|
||||
final _log = Logger('HomeViewModel');
|
||||
List<BookingSummary> _bookings = [];
|
||||
|
||||
late Command0 load;
|
||||
|
||||
List<BookingSummary> get bookings => _bookings;
|
||||
|
||||
Future<Result> _load() async {
|
||||
try {
|
||||
final result = await _bookingRepository.getBookingsList();
|
||||
switch (result) {
|
||||
case Ok<List<BookingSummary>>():
|
||||
_bookings = result.value;
|
||||
_log.fine('Loaded bookings');
|
||||
case Error<List<BookingSummary>>():
|
||||
_log.warning('Failed to load bookings', result.error);
|
||||
}
|
||||
return result;
|
||||
} finally {
|
||||
notifyListeners();
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,122 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
|
||||
import '../../../domain/models/booking/booking_summary.dart';
|
||||
import '../../../routing/routes.dart';
|
||||
import '../../auth/logout/view_models/logout_viewmodel.dart';
|
||||
import '../../auth/logout/widgets/logout_button.dart';
|
||||
import '../../core/localization/applocalization.dart';
|
||||
import '../../core/themes/dimens.dart';
|
||||
import '../../core/ui/date_format_start_end.dart';
|
||||
import '../view_models/home_viewmodel.dart';
|
||||
|
||||
class HomeScreen extends StatelessWidget {
|
||||
const HomeScreen({
|
||||
super.key,
|
||||
required this.viewModel,
|
||||
});
|
||||
|
||||
final HomeViewModel viewModel;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
floatingActionButton: FloatingActionButton.extended(
|
||||
// Workaround for https://github.com/flutter/flutter/issues/115358#issuecomment-2117157419
|
||||
heroTag: null,
|
||||
key: const ValueKey('booking-button'),
|
||||
onPressed: () => context.go(Routes.search),
|
||||
label: Text(AppLocalization.of(context).bookNewTrip),
|
||||
icon: const Icon(Icons.add_location_outlined),
|
||||
),
|
||||
body: SafeArea(
|
||||
top: true,
|
||||
bottom: true,
|
||||
child: ListenableBuilder(
|
||||
listenable: viewModel,
|
||||
builder: (context, _) {
|
||||
return CustomScrollView(
|
||||
slivers: [
|
||||
SliverToBoxAdapter(
|
||||
child: Padding(
|
||||
padding: EdgeInsets.symmetric(
|
||||
vertical: Dimens.of(context).paddingScreenVertical,
|
||||
horizontal: Dimens.of(context).paddingScreenHorizontal,
|
||||
),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text(
|
||||
AppLocalization.of(context).yourBookings,
|
||||
style: Theme.of(context).textTheme.headlineMedium,
|
||||
),
|
||||
LogoutButton(
|
||||
viewModel: LogoutViewModel(
|
||||
authRepository: context.read(),
|
||||
itineraryConfigRepository: context.read(),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
SliverList.builder(
|
||||
itemCount: viewModel.bookings.length,
|
||||
itemBuilder: (_, index) => _Booking(
|
||||
key: ValueKey(index),
|
||||
booking: viewModel.bookings[index],
|
||||
onTap: () => context.push(
|
||||
Routes.bookingWithId(viewModel.bookings[index].id)),
|
||||
),
|
||||
)
|
||||
],
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _Booking extends StatelessWidget {
|
||||
const _Booking({
|
||||
super.key,
|
||||
required this.booking,
|
||||
required this.onTap,
|
||||
});
|
||||
|
||||
final BookingSummary booking;
|
||||
final GestureTapCallback onTap;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return InkWell(
|
||||
onTap: onTap,
|
||||
child: Padding(
|
||||
padding: EdgeInsets.symmetric(
|
||||
horizontal: Dimens.of(context).paddingScreenHorizontal,
|
||||
vertical: Dimens.paddingVertical,
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
booking.name,
|
||||
style: Theme.of(context).textTheme.titleLarge,
|
||||
),
|
||||
Text(
|
||||
dateFormatStartEnd(
|
||||
DateTimeRange(
|
||||
start: booking.startDate,
|
||||
end: booking.endDate,
|
||||
),
|
||||
),
|
||||
style: Theme.of(context).textTheme.bodyLarge,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,40 @@
|
||||
import 'package:compass_app/data/repositories/booking/booking_repository.dart';
|
||||
import 'package:compass_app/data/repositories/booking/booking_repository_remote.dart';
|
||||
import 'package:compass_app/utils/result.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
|
||||
import '../../../../testing/fakes/services/fake_api_client.dart';
|
||||
import '../../../../testing/models/booking.dart';
|
||||
|
||||
void main() {
|
||||
group('BookingRepositoryRemote tests', () {
|
||||
late BookingRepository bookingRepository;
|
||||
late FakeApiClient fakeApiClient;
|
||||
|
||||
setUp(() {
|
||||
fakeApiClient = FakeApiClient();
|
||||
bookingRepository = BookingRepositoryRemote(
|
||||
apiClient: fakeApiClient,
|
||||
);
|
||||
});
|
||||
|
||||
test('should get booking', () async {
|
||||
final result = await bookingRepository.getBooking(0);
|
||||
final booking = result.asOk.value;
|
||||
expect(booking, kBooking);
|
||||
});
|
||||
|
||||
test('should create booking', () async {
|
||||
expect(fakeApiClient.bookings, isEmpty);
|
||||
final result = await bookingRepository.createBooking(kBooking);
|
||||
expect(result, isA<Ok<void>>());
|
||||
expect(fakeApiClient.bookings.first, kBookingApiModel);
|
||||
});
|
||||
|
||||
test('should get list of booking', () async {
|
||||
final result = await bookingRepository.getBookingsList();
|
||||
final list = result.asOk.value;
|
||||
expect(list, [kBookingSummary]);
|
||||
});
|
||||
});
|
||||
}
|
||||
@ -0,0 +1,76 @@
|
||||
import 'package:compass_app/data/repositories/auth/auth_repository.dart';
|
||||
import 'package:compass_app/data/repositories/itinerary_config/itinerary_config_repository.dart';
|
||||
import 'package:compass_app/routing/routes.dart';
|
||||
import 'package:compass_app/ui/home/view_models/home_viewmodel.dart';
|
||||
import 'package:compass_app/ui/home/widgets/home_screen.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:mocktail/mocktail.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
|
||||
import '../../../../testing/app.dart';
|
||||
import '../../../../testing/fakes/repositories/fake_auth_repository.dart';
|
||||
import '../../../../testing/fakes/repositories/fake_booking_repository.dart';
|
||||
import '../../../../testing/fakes/repositories/fake_itinerary_config_repository.dart';
|
||||
import '../../../../testing/mocks.dart';
|
||||
import '../../../../testing/models/booking.dart';
|
||||
|
||||
void main() {
|
||||
group('HomeScreen tests', () {
|
||||
late HomeViewModel viewModel;
|
||||
late MockGoRouter goRouter;
|
||||
|
||||
setUp(() {
|
||||
viewModel = HomeViewModel(
|
||||
bookingRepository: FakeBookingRepository()..createBooking(kBooking),
|
||||
);
|
||||
goRouter = MockGoRouter();
|
||||
when(() => goRouter.push(any())).thenAnswer((_) => Future.value(null));
|
||||
});
|
||||
|
||||
loadWidget(WidgetTester tester) async {
|
||||
await testApp(
|
||||
tester,
|
||||
ChangeNotifierProvider.value(
|
||||
value: FakeAuthRepository() as AuthRepository,
|
||||
child: Provider.value(
|
||||
value: FakeItineraryConfigRepository() as ItineraryConfigRepository,
|
||||
child: HomeScreen(viewModel: viewModel),
|
||||
),
|
||||
),
|
||||
goRouter: goRouter,
|
||||
);
|
||||
}
|
||||
|
||||
testWidgets('should load screen', (tester) async {
|
||||
await loadWidget(tester);
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(find.byType(HomeScreen), findsOneWidget);
|
||||
});
|
||||
|
||||
testWidgets('should navigate to search', (tester) async {
|
||||
await loadWidget(tester);
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
// Tap on create a booking FAB
|
||||
await tester.tap(find.byKey(const ValueKey('booking-button')));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
// Should navigate to results screen
|
||||
verify(() => goRouter.go(Routes.search)).called(1);
|
||||
});
|
||||
|
||||
testWidgets('should open existing booking', (tester) async {
|
||||
await loadWidget(tester);
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
// Tap on booking (created from kBooking)
|
||||
await tester.tap(find.text('name1, Europe'));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
// Should navigate to results screen
|
||||
verify(() => goRouter.push(Routes.bookingWithId(0))).called(1);
|
||||
});
|
||||
});
|
||||
}
|
||||
@ -0,0 +1,39 @@
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:compass_app/data/repositories/booking/booking_repository.dart';
|
||||
import 'package:compass_app/domain/models/booking/booking_summary.dart';
|
||||
import 'package:compass_app/utils/result.dart';
|
||||
import 'package:compass_model/src/model/booking/booking.dart';
|
||||
|
||||
class FakeBookingRepository implements BookingRepository {
|
||||
List<Booking> bookings = List.empty(growable: true);
|
||||
|
||||
@override
|
||||
Future<Result<void>> createBooking(Booking booking) async {
|
||||
bookings.add(booking);
|
||||
return Result.ok(null);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<Result<Booking>> getBooking(int id) async {
|
||||
return Result.ok(bookings[id]);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<Result<List<BookingSummary>>> getBookingsList() async {
|
||||
return Result.ok(_createSummaries());
|
||||
}
|
||||
|
||||
List<BookingSummary> _createSummaries() {
|
||||
return bookings
|
||||
.mapIndexed(
|
||||
(index, booking) => BookingSummary(
|
||||
id: index,
|
||||
name:
|
||||
'${booking.destination.name}, ${booking.destination.continent}',
|
||||
startDate: booking.startDate,
|
||||
endDate: booking.endDate,
|
||||
),
|
||||
)
|
||||
.toList();
|
||||
}
|
||||
}
|
||||
@ -1,4 +1,18 @@
|
||||
import 'dart:convert';
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:compass_model/model.dart';
|
||||
|
||||
class Assets {
|
||||
static const activities = '../app/assets/activities.json';
|
||||
static const destinations = '../app/assets/destinations.json';
|
||||
static const _activities = '../app/assets/activities.json';
|
||||
static const _destinations = '../app/assets/destinations.json';
|
||||
|
||||
static final List<Destination> destinations =
|
||||
(json.decode(File(Assets._destinations).readAsStringSync()) as List)
|
||||
.map((element) => Destination.fromJson(element))
|
||||
.toList();
|
||||
static final List<Activity> activities =
|
||||
(json.decode(File(Assets._activities).readAsStringSync()) as List)
|
||||
.map((element) => Activity.fromJson(element))
|
||||
.toList();
|
||||
}
|
||||
|
||||
@ -0,0 +1,31 @@
|
||||
import 'package:freezed_annotation/freezed_annotation.dart';
|
||||
|
||||
part 'booking.freezed.dart';
|
||||
part 'booking.g.dart';
|
||||
|
||||
@freezed
|
||||
class Booking with _$Booking {
|
||||
const factory Booking({
|
||||
/// Booking ID. Generated when stored in server.
|
||||
int? id,
|
||||
|
||||
/// Start date of the trip
|
||||
required DateTime startDate,
|
||||
|
||||
/// End date of the trip
|
||||
required DateTime endDate,
|
||||
|
||||
/// Booking name
|
||||
/// Should be "Destination, Continent"
|
||||
required String name,
|
||||
|
||||
/// Destination of the trip
|
||||
required String destinationRef,
|
||||
|
||||
/// List of chosen activities
|
||||
required List<String> activitiesRef,
|
||||
}) = _Booking;
|
||||
|
||||
factory Booking.fromJson(Map<String, Object?> json) =>
|
||||
_$BookingFromJson(json);
|
||||
}
|
||||
@ -0,0 +1,312 @@
|
||||
// coverage:ignore-file
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
// ignore_for_file: type=lint
|
||||
// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark
|
||||
|
||||
part of 'booking.dart';
|
||||
|
||||
// **************************************************************************
|
||||
// FreezedGenerator
|
||||
// **************************************************************************
|
||||
|
||||
T _$identity<T>(T value) => value;
|
||||
|
||||
final _privateConstructorUsedError = UnsupportedError(
|
||||
'It seems like you constructed your class using `MyClass._()`. This constructor is only meant to be used by freezed and you are not supposed to need it nor use it.\nPlease check the documentation here for more information: https://github.com/rrousselGit/freezed#adding-getters-and-methods-to-our-models');
|
||||
|
||||
Booking _$BookingFromJson(Map<String, dynamic> json) {
|
||||
return _Booking.fromJson(json);
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
mixin _$Booking {
|
||||
/// Booking ID. Generated when stored in server.
|
||||
int? get id => throw _privateConstructorUsedError;
|
||||
|
||||
/// Start date of the trip
|
||||
DateTime get startDate => throw _privateConstructorUsedError;
|
||||
|
||||
/// End date of the trip
|
||||
DateTime get endDate => throw _privateConstructorUsedError;
|
||||
|
||||
/// Booking display name
|
||||
/// Should be "Destination, Continent"
|
||||
String get name => throw _privateConstructorUsedError;
|
||||
|
||||
/// Destination of the trip
|
||||
String get destinationRef => throw _privateConstructorUsedError;
|
||||
|
||||
/// List of chosen activities
|
||||
List<String> get activitiesRef => throw _privateConstructorUsedError;
|
||||
|
||||
/// Serializes this Booking to a JSON map.
|
||||
Map<String, dynamic> toJson() => throw _privateConstructorUsedError;
|
||||
|
||||
/// Create a copy of Booking
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||
$BookingCopyWith<Booking> get copyWith => throw _privateConstructorUsedError;
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
abstract class $BookingCopyWith<$Res> {
|
||||
factory $BookingCopyWith(Booking value, $Res Function(Booking) then) =
|
||||
_$BookingCopyWithImpl<$Res, Booking>;
|
||||
@useResult
|
||||
$Res call(
|
||||
{int? id,
|
||||
DateTime startDate,
|
||||
DateTime endDate,
|
||||
String name,
|
||||
String destinationRef,
|
||||
List<String> activitiesRef});
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
class _$BookingCopyWithImpl<$Res, $Val extends Booking>
|
||||
implements $BookingCopyWith<$Res> {
|
||||
_$BookingCopyWithImpl(this._value, this._then);
|
||||
|
||||
// ignore: unused_field
|
||||
final $Val _value;
|
||||
// ignore: unused_field
|
||||
final $Res Function($Val) _then;
|
||||
|
||||
/// Create a copy of Booking
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@pragma('vm:prefer-inline')
|
||||
@override
|
||||
$Res call({
|
||||
Object? id = freezed,
|
||||
Object? startDate = null,
|
||||
Object? endDate = null,
|
||||
Object? name = null,
|
||||
Object? destinationRef = null,
|
||||
Object? activitiesRef = null,
|
||||
}) {
|
||||
return _then(_value.copyWith(
|
||||
id: freezed == id
|
||||
? _value.id
|
||||
: id // ignore: cast_nullable_to_non_nullable
|
||||
as int?,
|
||||
startDate: null == startDate
|
||||
? _value.startDate
|
||||
: startDate // ignore: cast_nullable_to_non_nullable
|
||||
as DateTime,
|
||||
endDate: null == endDate
|
||||
? _value.endDate
|
||||
: endDate // ignore: cast_nullable_to_non_nullable
|
||||
as DateTime,
|
||||
name: null == name
|
||||
? _value.name
|
||||
: name // ignore: cast_nullable_to_non_nullable
|
||||
as String,
|
||||
destinationRef: null == destinationRef
|
||||
? _value.destinationRef
|
||||
: destinationRef // ignore: cast_nullable_to_non_nullable
|
||||
as String,
|
||||
activitiesRef: null == activitiesRef
|
||||
? _value.activitiesRef
|
||||
: activitiesRef // ignore: cast_nullable_to_non_nullable
|
||||
as List<String>,
|
||||
) as $Val);
|
||||
}
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
abstract class _$$BookingImplCopyWith<$Res> implements $BookingCopyWith<$Res> {
|
||||
factory _$$BookingImplCopyWith(
|
||||
_$BookingImpl value, $Res Function(_$BookingImpl) then) =
|
||||
__$$BookingImplCopyWithImpl<$Res>;
|
||||
@override
|
||||
@useResult
|
||||
$Res call(
|
||||
{int? id,
|
||||
DateTime startDate,
|
||||
DateTime endDate,
|
||||
String name,
|
||||
String destinationRef,
|
||||
List<String> activitiesRef});
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
class __$$BookingImplCopyWithImpl<$Res>
|
||||
extends _$BookingCopyWithImpl<$Res, _$BookingImpl>
|
||||
implements _$$BookingImplCopyWith<$Res> {
|
||||
__$$BookingImplCopyWithImpl(
|
||||
_$BookingImpl _value, $Res Function(_$BookingImpl) _then)
|
||||
: super(_value, _then);
|
||||
|
||||
/// Create a copy of Booking
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@pragma('vm:prefer-inline')
|
||||
@override
|
||||
$Res call({
|
||||
Object? id = freezed,
|
||||
Object? startDate = null,
|
||||
Object? endDate = null,
|
||||
Object? name = null,
|
||||
Object? destinationRef = null,
|
||||
Object? activitiesRef = null,
|
||||
}) {
|
||||
return _then(_$BookingImpl(
|
||||
id: freezed == id
|
||||
? _value.id
|
||||
: id // ignore: cast_nullable_to_non_nullable
|
||||
as int?,
|
||||
startDate: null == startDate
|
||||
? _value.startDate
|
||||
: startDate // ignore: cast_nullable_to_non_nullable
|
||||
as DateTime,
|
||||
endDate: null == endDate
|
||||
? _value.endDate
|
||||
: endDate // ignore: cast_nullable_to_non_nullable
|
||||
as DateTime,
|
||||
name: null == name
|
||||
? _value.name
|
||||
: name // ignore: cast_nullable_to_non_nullable
|
||||
as String,
|
||||
destinationRef: null == destinationRef
|
||||
? _value.destinationRef
|
||||
: destinationRef // ignore: cast_nullable_to_non_nullable
|
||||
as String,
|
||||
activitiesRef: null == activitiesRef
|
||||
? _value._activitiesRef
|
||||
: activitiesRef // ignore: cast_nullable_to_non_nullable
|
||||
as List<String>,
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
@JsonSerializable()
|
||||
class _$BookingImpl implements _Booking {
|
||||
const _$BookingImpl(
|
||||
{this.id,
|
||||
required this.startDate,
|
||||
required this.endDate,
|
||||
required this.name,
|
||||
required this.destinationRef,
|
||||
required final List<String> activitiesRef})
|
||||
: _activitiesRef = activitiesRef;
|
||||
|
||||
factory _$BookingImpl.fromJson(Map<String, dynamic> json) =>
|
||||
_$$BookingImplFromJson(json);
|
||||
|
||||
/// Booking ID. Generated when stored in server.
|
||||
@override
|
||||
final int? id;
|
||||
|
||||
/// Start date of the trip
|
||||
@override
|
||||
final DateTime startDate;
|
||||
|
||||
/// End date of the trip
|
||||
@override
|
||||
final DateTime endDate;
|
||||
|
||||
/// Booking display name
|
||||
/// Should be "Destination, Continent"
|
||||
@override
|
||||
final String name;
|
||||
|
||||
/// Destination of the trip
|
||||
@override
|
||||
final String destinationRef;
|
||||
|
||||
/// List of chosen activities
|
||||
final List<String> _activitiesRef;
|
||||
|
||||
/// List of chosen activities
|
||||
@override
|
||||
List<String> get activitiesRef {
|
||||
if (_activitiesRef is EqualUnmodifiableListView) return _activitiesRef;
|
||||
// ignore: implicit_dynamic_type
|
||||
return EqualUnmodifiableListView(_activitiesRef);
|
||||
}
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'Booking(id: $id, startDate: $startDate, endDate: $endDate, name: $name, destinationRef: $destinationRef, activitiesRef: $activitiesRef)';
|
||||
}
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
return identical(this, other) ||
|
||||
(other.runtimeType == runtimeType &&
|
||||
other is _$BookingImpl &&
|
||||
(identical(other.id, id) || other.id == id) &&
|
||||
(identical(other.startDate, startDate) ||
|
||||
other.startDate == startDate) &&
|
||||
(identical(other.endDate, endDate) || other.endDate == endDate) &&
|
||||
(identical(other.name, name) || other.name == name) &&
|
||||
(identical(other.destinationRef, destinationRef) ||
|
||||
other.destinationRef == destinationRef) &&
|
||||
const DeepCollectionEquality()
|
||||
.equals(other._activitiesRef, _activitiesRef));
|
||||
}
|
||||
|
||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||
@override
|
||||
int get hashCode => Object.hash(runtimeType, id, startDate, endDate, name,
|
||||
destinationRef, const DeepCollectionEquality().hash(_activitiesRef));
|
||||
|
||||
/// Create a copy of Booking
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||
@override
|
||||
@pragma('vm:prefer-inline')
|
||||
_$$BookingImplCopyWith<_$BookingImpl> get copyWith =>
|
||||
__$$BookingImplCopyWithImpl<_$BookingImpl>(this, _$identity);
|
||||
|
||||
@override
|
||||
Map<String, dynamic> toJson() {
|
||||
return _$$BookingImplToJson(
|
||||
this,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
abstract class _Booking implements Booking {
|
||||
const factory _Booking(
|
||||
{final int? id,
|
||||
required final DateTime startDate,
|
||||
required final DateTime endDate,
|
||||
required final String name,
|
||||
required final String destinationRef,
|
||||
required final List<String> activitiesRef}) = _$BookingImpl;
|
||||
|
||||
factory _Booking.fromJson(Map<String, dynamic> json) = _$BookingImpl.fromJson;
|
||||
|
||||
/// Booking ID. Generated when stored in server.
|
||||
@override
|
||||
int? get id;
|
||||
|
||||
/// Start date of the trip
|
||||
@override
|
||||
DateTime get startDate;
|
||||
|
||||
/// End date of the trip
|
||||
@override
|
||||
DateTime get endDate;
|
||||
|
||||
/// Booking display name
|
||||
/// Should be "Destination, Continent"
|
||||
@override
|
||||
String get name;
|
||||
|
||||
/// Destination of the trip
|
||||
@override
|
||||
String get destinationRef;
|
||||
|
||||
/// List of chosen activities
|
||||
@override
|
||||
List<String> get activitiesRef;
|
||||
|
||||
/// Create a copy of Booking
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@override
|
||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||
_$$BookingImplCopyWith<_$BookingImpl> get copyWith =>
|
||||
throw _privateConstructorUsedError;
|
||||
}
|
||||
@ -0,0 +1,29 @@
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
|
||||
part of 'booking.dart';
|
||||
|
||||
// **************************************************************************
|
||||
// JsonSerializableGenerator
|
||||
// **************************************************************************
|
||||
|
||||
_$BookingImpl _$$BookingImplFromJson(Map<String, dynamic> json) =>
|
||||
_$BookingImpl(
|
||||
id: (json['id'] as num?)?.toInt(),
|
||||
startDate: DateTime.parse(json['startDate'] as String),
|
||||
endDate: DateTime.parse(json['endDate'] as String),
|
||||
name: json['name'] as String,
|
||||
destinationRef: json['destinationRef'] as String,
|
||||
activitiesRef: (json['activitiesRef'] as List<dynamic>)
|
||||
.map((e) => e as String)
|
||||
.toList(),
|
||||
);
|
||||
|
||||
Map<String, dynamic> _$$BookingImplToJson(_$BookingImpl instance) =>
|
||||
<String, dynamic>{
|
||||
'id': instance.id,
|
||||
'startDate': instance.startDate.toIso8601String(),
|
||||
'endDate': instance.endDate.toIso8601String(),
|
||||
'name': instance.name,
|
||||
'destinationRef': instance.destinationRef,
|
||||
'activitiesRef': instance.activitiesRef,
|
||||
};
|
||||
@ -0,0 +1,93 @@
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:shelf/shelf.dart';
|
||||
import 'package:shelf_router/shelf_router.dart';
|
||||
|
||||
import '../config/assets.dart';
|
||||
import '../model/booking/booking.dart';
|
||||
|
||||
/// Allows the user to save and restore bookings.
|
||||
///
|
||||
/// Bookings are stored in memory for demo purposes,
|
||||
/// so they are lost when the server stops.
|
||||
///
|
||||
/// For demo purposes, this API includes a default pre-generated booking.
|
||||
///
|
||||
/// The [Booking.id] is also the index in the bookings list.
|
||||
class BookingApi {
|
||||
BookingApi() {
|
||||
// Create a default booking
|
||||
var destination = Assets.destinations.first;
|
||||
final activitiesRef = Assets.activities
|
||||
.where((activity) => activity.destinationRef == destination.ref)
|
||||
.map((activity) => activity.ref)
|
||||
.toList();
|
||||
_bookings.insert(
|
||||
0,
|
||||
Booking(
|
||||
id: 0,
|
||||
name: '${destination.name}, ${destination.continent}',
|
||||
startDate: DateTime(2024, 7, 20),
|
||||
endDate: DateTime(2024, 8, 15),
|
||||
destinationRef: destination.ref,
|
||||
activitiesRef: activitiesRef,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// Bookings are kept in memory for demo purposes.
|
||||
// To keep things simple, the id is also the index in the list.
|
||||
final List<Booking> _bookings = List.empty(growable: true);
|
||||
|
||||
Router get router {
|
||||
final router = Router();
|
||||
|
||||
// Get User bookings
|
||||
router.get('/', (Request request) {
|
||||
return Response.ok(
|
||||
json.encode(_bookings),
|
||||
headers: {'Content-Type': 'application/json'},
|
||||
);
|
||||
});
|
||||
|
||||
// Get a booking by id
|
||||
router.get('/<id>', (Request request, String id) {
|
||||
final index = int.parse(id);
|
||||
if (index < 0 || index >= _bookings.length) {
|
||||
return Response.notFound('Invalid id');
|
||||
}
|
||||
return Response.ok(
|
||||
json.encode(_bookings[index]),
|
||||
headers: {'Content-Type': 'application/json'},
|
||||
);
|
||||
});
|
||||
|
||||
// Save a new booking
|
||||
router.post('/', (Request request) async {
|
||||
final body = await request.readAsString();
|
||||
final booking = Booking.fromJson(json.decode(body));
|
||||
|
||||
if (booking.id != null) {
|
||||
// POST endpoint only allows newly created bookings
|
||||
return Response.badRequest(
|
||||
body: 'Booking already has id, use PUT instead.');
|
||||
}
|
||||
|
||||
// Add ID to new booking
|
||||
final id = _bookings.length;
|
||||
final bookingWithId = booking.copyWith(id: id);
|
||||
|
||||
// Store booking
|
||||
_bookings.add(bookingWithId);
|
||||
|
||||
// Respond with newly created booking
|
||||
return Response(
|
||||
201, // created
|
||||
body: json.encode(bookingWithId),
|
||||
headers: {'Content-Type': 'application/json'},
|
||||
);
|
||||
});
|
||||
|
||||
return router;
|
||||
}
|
||||
}
|
||||
Loading…
Reference in new issue