hubs ready
This commit is contained in:
@@ -1,9 +1,13 @@
|
|||||||
|
import 'dart:convert';
|
||||||
|
|
||||||
import 'package:firebase_auth/firebase_auth.dart' as firebase;
|
import 'package:firebase_auth/firebase_auth.dart' as firebase;
|
||||||
import 'package:firebase_data_connect/firebase_data_connect.dart';
|
import 'package:firebase_data_connect/firebase_data_connect.dart';
|
||||||
|
import 'package:http/http.dart' as http;
|
||||||
import 'package:krow_data_connect/krow_data_connect.dart' as dc;
|
import 'package:krow_data_connect/krow_data_connect.dart' as dc;
|
||||||
import 'package:krow_domain/krow_domain.dart' as domain;
|
import 'package:krow_domain/krow_domain.dart' as domain;
|
||||||
|
|
||||||
import '../../domain/repositories/hub_repository_interface.dart';
|
import '../../domain/repositories/hub_repository_interface.dart';
|
||||||
|
import '../../util/hubs_constants.dart';
|
||||||
|
|
||||||
/// Implementation of [HubRepositoryInterface] backed by Data Connect.
|
/// Implementation of [HubRepositoryInterface] backed by Data Connect.
|
||||||
class HubRepositoryImpl implements HubRepositoryInterface {
|
class HubRepositoryImpl implements HubRepositoryInterface {
|
||||||
@@ -27,10 +31,24 @@ class HubRepositoryImpl implements HubRepositoryInterface {
|
|||||||
Future<domain.Hub> createHub({
|
Future<domain.Hub> createHub({
|
||||||
required String name,
|
required String name,
|
||||||
required String address,
|
required String address,
|
||||||
|
String? placeId,
|
||||||
|
double? latitude,
|
||||||
|
double? longitude,
|
||||||
|
String? city,
|
||||||
|
String? state,
|
||||||
|
String? street,
|
||||||
|
String? country,
|
||||||
|
String? zipCode,
|
||||||
}) async {
|
}) async {
|
||||||
final dc.GetBusinessesByUserIdBusinesses business = await _getBusinessForCurrentUser();
|
final dc.GetBusinessesByUserIdBusinesses business = await _getBusinessForCurrentUser();
|
||||||
final String teamId = await _getOrCreateTeamId(business);
|
final String teamId = await _getOrCreateTeamId(business);
|
||||||
final String? city = business.city;
|
final _PlaceAddress? placeAddress =
|
||||||
|
placeId == null || placeId.isEmpty ? null : await _fetchPlaceAddress(placeId);
|
||||||
|
final String? cityValue = city ?? placeAddress?.city ?? business.city;
|
||||||
|
final String? stateValue = state ?? placeAddress?.state;
|
||||||
|
final String? streetValue = street ?? placeAddress?.street;
|
||||||
|
final String? countryValue = country ?? placeAddress?.country;
|
||||||
|
final String? zipCodeValue = zipCode ?? placeAddress?.zipCode;
|
||||||
|
|
||||||
final OperationResult<dc.CreateTeamHubData, dc.CreateTeamHubVariables> result = await _dataConnect
|
final OperationResult<dc.CreateTeamHubData, dc.CreateTeamHubVariables> result = await _dataConnect
|
||||||
.createTeamHub(
|
.createTeamHub(
|
||||||
@@ -38,7 +56,14 @@ class HubRepositoryImpl implements HubRepositoryInterface {
|
|||||||
hubName: name,
|
hubName: name,
|
||||||
address: address,
|
address: address,
|
||||||
)
|
)
|
||||||
.city(city?.isNotEmpty == true ? city : '')
|
.placeId(placeId)
|
||||||
|
.latitude(latitude)
|
||||||
|
.longitude(longitude)
|
||||||
|
.city(cityValue?.isNotEmpty == true ? cityValue : '')
|
||||||
|
.state(stateValue)
|
||||||
|
.street(streetValue)
|
||||||
|
.country(countryValue)
|
||||||
|
.zipCode(zipCodeValue)
|
||||||
.execute();
|
.execute();
|
||||||
final String? createdId = result.data?.teamHub_insert.id;
|
final String? createdId = result.data?.teamHub_insert.id;
|
||||||
if (createdId == null) {
|
if (createdId == null) {
|
||||||
@@ -192,4 +217,99 @@ class HubRepositoryImpl implements HubRepositoryInterface {
|
|||||||
)
|
)
|
||||||
.toList();
|
.toList();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<_PlaceAddress?> _fetchPlaceAddress(String placeId) async {
|
||||||
|
final Uri uri = Uri.https(
|
||||||
|
'maps.googleapis.com',
|
||||||
|
'/maps/api/place/details/json',
|
||||||
|
<String, String>{
|
||||||
|
'place_id': placeId,
|
||||||
|
'fields': 'address_component',
|
||||||
|
'key': HubsConstants.googlePlacesApiKey,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
try {
|
||||||
|
final http.Response response = await http.get(uri);
|
||||||
|
if (response.statusCode != 200) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
final Map<String, dynamic> payload =
|
||||||
|
json.decode(response.body) as Map<String, dynamic>;
|
||||||
|
if (payload['status'] != 'OK') {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
final Map<String, dynamic>? result =
|
||||||
|
payload['result'] as Map<String, dynamic>?;
|
||||||
|
final List<dynamic>? components =
|
||||||
|
result?['address_components'] as List<dynamic>?;
|
||||||
|
if (components == null || components.isEmpty) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
String? streetNumber;
|
||||||
|
String? route;
|
||||||
|
String? city;
|
||||||
|
String? state;
|
||||||
|
String? country;
|
||||||
|
String? zipCode;
|
||||||
|
|
||||||
|
for (final dynamic entry in components) {
|
||||||
|
final Map<String, dynamic> component = entry as Map<String, dynamic>;
|
||||||
|
final List<dynamic> types = component['types'] as List<dynamic>? ?? <dynamic>[];
|
||||||
|
final String? longName = component['long_name'] as String?;
|
||||||
|
final String? shortName = component['short_name'] as String?;
|
||||||
|
|
||||||
|
if (types.contains('street_number')) {
|
||||||
|
streetNumber = longName;
|
||||||
|
} else if (types.contains('route')) {
|
||||||
|
route = longName;
|
||||||
|
} else if (types.contains('locality')) {
|
||||||
|
city = longName;
|
||||||
|
} else if (types.contains('postal_town')) {
|
||||||
|
city ??= longName;
|
||||||
|
} else if (types.contains('administrative_area_level_2')) {
|
||||||
|
city ??= longName;
|
||||||
|
} else if (types.contains('administrative_area_level_1')) {
|
||||||
|
state = shortName ?? longName;
|
||||||
|
} else if (types.contains('country')) {
|
||||||
|
country = shortName ?? longName;
|
||||||
|
} else if (types.contains('postal_code')) {
|
||||||
|
zipCode = longName;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
final String? streetValue = <String?>[streetNumber, route]
|
||||||
|
.where((String? value) => value != null && value!.isNotEmpty)
|
||||||
|
.join(' ')
|
||||||
|
.trim();
|
||||||
|
|
||||||
|
return _PlaceAddress(
|
||||||
|
street: streetValue?.isEmpty == true ? null : streetValue,
|
||||||
|
city: city,
|
||||||
|
state: state,
|
||||||
|
country: country,
|
||||||
|
zipCode: zipCode,
|
||||||
|
);
|
||||||
|
} catch (_) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _PlaceAddress {
|
||||||
|
const _PlaceAddress({
|
||||||
|
this.street,
|
||||||
|
this.city,
|
||||||
|
this.state,
|
||||||
|
this.country,
|
||||||
|
this.zipCode,
|
||||||
|
});
|
||||||
|
|
||||||
|
final String? street;
|
||||||
|
final String? city;
|
||||||
|
final String? state;
|
||||||
|
final String? country;
|
||||||
|
final String? zipCode;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,11 +10,42 @@ class CreateHubArguments extends UseCaseArgument {
|
|||||||
/// The physical address of the hub.
|
/// The physical address of the hub.
|
||||||
final String address;
|
final String address;
|
||||||
|
|
||||||
|
final String? placeId;
|
||||||
|
final double? latitude;
|
||||||
|
final double? longitude;
|
||||||
|
final String? city;
|
||||||
|
final String? state;
|
||||||
|
final String? street;
|
||||||
|
final String? country;
|
||||||
|
final String? zipCode;
|
||||||
|
|
||||||
/// Creates a [CreateHubArguments] instance.
|
/// Creates a [CreateHubArguments] instance.
|
||||||
///
|
///
|
||||||
/// Both [name] and [address] are required.
|
/// Both [name] and [address] are required.
|
||||||
const CreateHubArguments({required this.name, required this.address});
|
const CreateHubArguments({
|
||||||
|
required this.name,
|
||||||
|
required this.address,
|
||||||
|
this.placeId,
|
||||||
|
this.latitude,
|
||||||
|
this.longitude,
|
||||||
|
this.city,
|
||||||
|
this.state,
|
||||||
|
this.street,
|
||||||
|
this.country,
|
||||||
|
this.zipCode,
|
||||||
|
});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
List<Object?> get props => <Object?>[name, address];
|
List<Object?> get props => <Object?>[
|
||||||
|
name,
|
||||||
|
address,
|
||||||
|
placeId,
|
||||||
|
latitude,
|
||||||
|
longitude,
|
||||||
|
city,
|
||||||
|
state,
|
||||||
|
street,
|
||||||
|
country,
|
||||||
|
zipCode,
|
||||||
|
];
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,7 +15,18 @@ abstract interface class HubRepositoryInterface {
|
|||||||
///
|
///
|
||||||
/// Takes the [name] and [address] of the new hub.
|
/// Takes the [name] and [address] of the new hub.
|
||||||
/// Returns the created [Hub] entity.
|
/// Returns the created [Hub] entity.
|
||||||
Future<Hub> createHub({required String name, required String address});
|
Future<Hub> createHub({
|
||||||
|
required String name,
|
||||||
|
required String address,
|
||||||
|
String? placeId,
|
||||||
|
double? latitude,
|
||||||
|
double? longitude,
|
||||||
|
String? city,
|
||||||
|
String? state,
|
||||||
|
String? street,
|
||||||
|
String? country,
|
||||||
|
String? zipCode,
|
||||||
|
});
|
||||||
|
|
||||||
/// Deletes a hub by its [id].
|
/// Deletes a hub by its [id].
|
||||||
Future<void> deleteHub(String id);
|
Future<void> deleteHub(String id);
|
||||||
|
|||||||
@@ -21,6 +21,14 @@ class CreateHubUseCase implements UseCase<CreateHubArguments, Hub> {
|
|||||||
return _repository.createHub(
|
return _repository.createHub(
|
||||||
name: arguments.name,
|
name: arguments.name,
|
||||||
address: arguments.address,
|
address: arguments.address,
|
||||||
|
placeId: arguments.placeId,
|
||||||
|
latitude: arguments.latitude,
|
||||||
|
longitude: arguments.longitude,
|
||||||
|
city: arguments.city,
|
||||||
|
state: arguments.state,
|
||||||
|
street: arguments.street,
|
||||||
|
country: arguments.country,
|
||||||
|
zipCode: arguments.zipCode,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -84,7 +84,18 @@ class ClientHubsBloc extends Bloc<ClientHubsEvent, ClientHubsState>
|
|||||||
emit(state.copyWith(status: ClientHubsStatus.actionInProgress));
|
emit(state.copyWith(status: ClientHubsStatus.actionInProgress));
|
||||||
try {
|
try {
|
||||||
await _createHubUseCase(
|
await _createHubUseCase(
|
||||||
CreateHubArguments(name: event.name, address: event.address),
|
CreateHubArguments(
|
||||||
|
name: event.name,
|
||||||
|
address: event.address,
|
||||||
|
placeId: event.placeId,
|
||||||
|
latitude: event.latitude,
|
||||||
|
longitude: event.longitude,
|
||||||
|
city: event.city,
|
||||||
|
state: event.state,
|
||||||
|
street: event.street,
|
||||||
|
country: event.country,
|
||||||
|
zipCode: event.zipCode,
|
||||||
|
),
|
||||||
);
|
);
|
||||||
final List<Hub> hubs = await _getHubsUseCase();
|
final List<Hub> hubs = await _getHubsUseCase();
|
||||||
emit(
|
emit(
|
||||||
|
|||||||
@@ -18,11 +18,41 @@ class ClientHubsFetched extends ClientHubsEvent {
|
|||||||
class ClientHubsAddRequested extends ClientHubsEvent {
|
class ClientHubsAddRequested extends ClientHubsEvent {
|
||||||
final String name;
|
final String name;
|
||||||
final String address;
|
final String address;
|
||||||
|
final String? placeId;
|
||||||
|
final double? latitude;
|
||||||
|
final double? longitude;
|
||||||
|
final String? city;
|
||||||
|
final String? state;
|
||||||
|
final String? street;
|
||||||
|
final String? country;
|
||||||
|
final String? zipCode;
|
||||||
|
|
||||||
const ClientHubsAddRequested({required this.name, required this.address});
|
const ClientHubsAddRequested({
|
||||||
|
required this.name,
|
||||||
|
required this.address,
|
||||||
|
this.placeId,
|
||||||
|
this.latitude,
|
||||||
|
this.longitude,
|
||||||
|
this.city,
|
||||||
|
this.state,
|
||||||
|
this.street,
|
||||||
|
this.country,
|
||||||
|
this.zipCode,
|
||||||
|
});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
List<Object?> get props => <Object?>[name, address];
|
List<Object?> get props => <Object?>[
|
||||||
|
name,
|
||||||
|
address,
|
||||||
|
placeId,
|
||||||
|
latitude,
|
||||||
|
longitude,
|
||||||
|
city,
|
||||||
|
state,
|
||||||
|
street,
|
||||||
|
country,
|
||||||
|
zipCode,
|
||||||
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Event triggered to delete a hub.
|
/// Event triggered to delete a hub.
|
||||||
|
|||||||
@@ -106,9 +106,21 @@ class ClientHubsPage extends StatelessWidget {
|
|||||||
),
|
),
|
||||||
if (state.showAddHubDialog)
|
if (state.showAddHubDialog)
|
||||||
AddHubDialog(
|
AddHubDialog(
|
||||||
onCreate: (String name, String address) {
|
onCreate: (
|
||||||
|
String name,
|
||||||
|
String address, {
|
||||||
|
String? placeId,
|
||||||
|
double? latitude,
|
||||||
|
double? longitude,
|
||||||
|
}) {
|
||||||
BlocProvider.of<ClientHubsBloc>(context).add(
|
BlocProvider.of<ClientHubsBloc>(context).add(
|
||||||
ClientHubsAddRequested(name: name, address: address),
|
ClientHubsAddRequested(
|
||||||
|
name: name,
|
||||||
|
address: address,
|
||||||
|
placeId: placeId,
|
||||||
|
latitude: latitude,
|
||||||
|
longitude: longitude,
|
||||||
|
),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
onCancel: () => BlocProvider.of<ClientHubsBloc>(
|
onCancel: () => BlocProvider.of<ClientHubsBloc>(
|
||||||
|
|||||||
@@ -1,13 +1,20 @@
|
|||||||
import 'package:design_system/design_system.dart';
|
import 'package:design_system/design_system.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:core_localization/core_localization.dart';
|
import 'package:core_localization/core_localization.dart';
|
||||||
|
import 'package:google_places_flutter/model/prediction.dart';
|
||||||
|
|
||||||
import 'hub_address_autocomplete.dart';
|
import 'hub_address_autocomplete.dart';
|
||||||
|
|
||||||
/// A dialog for adding a new hub.
|
/// A dialog for adding a new hub.
|
||||||
class AddHubDialog extends StatefulWidget {
|
class AddHubDialog extends StatefulWidget {
|
||||||
/// Callback when the "Create Hub" button is pressed.
|
/// Callback when the "Create Hub" button is pressed.
|
||||||
final Function(String name, String address) onCreate;
|
final void Function(
|
||||||
|
String name,
|
||||||
|
String address, {
|
||||||
|
String? placeId,
|
||||||
|
double? latitude,
|
||||||
|
double? longitude,
|
||||||
|
}) onCreate;
|
||||||
|
|
||||||
/// Callback when the dialog is cancelled.
|
/// Callback when the dialog is cancelled.
|
||||||
final VoidCallback onCancel;
|
final VoidCallback onCancel;
|
||||||
@@ -26,18 +33,22 @@ class AddHubDialog extends StatefulWidget {
|
|||||||
class _AddHubDialogState extends State<AddHubDialog> {
|
class _AddHubDialogState extends State<AddHubDialog> {
|
||||||
late final TextEditingController _nameController;
|
late final TextEditingController _nameController;
|
||||||
late final TextEditingController _addressController;
|
late final TextEditingController _addressController;
|
||||||
|
late final FocusNode _addressFocusNode;
|
||||||
|
Prediction? _selectedPrediction;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
_nameController = TextEditingController();
|
_nameController = TextEditingController();
|
||||||
_addressController = TextEditingController();
|
_addressController = TextEditingController();
|
||||||
|
_addressFocusNode = FocusNode();
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void dispose() {
|
void dispose() {
|
||||||
_nameController.dispose();
|
_nameController.dispose();
|
||||||
_addressController.dispose();
|
_addressController.dispose();
|
||||||
|
_addressFocusNode.dispose();
|
||||||
super.dispose();
|
super.dispose();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -79,6 +90,10 @@ class _AddHubDialogState extends State<AddHubDialog> {
|
|||||||
HubAddressAutocomplete(
|
HubAddressAutocomplete(
|
||||||
controller: _addressController,
|
controller: _addressController,
|
||||||
hintText: t.client_hubs.add_hub_dialog.address_hint,
|
hintText: t.client_hubs.add_hub_dialog.address_hint,
|
||||||
|
focusNode: _addressFocusNode,
|
||||||
|
onSelected: (Prediction prediction) {
|
||||||
|
_selectedPrediction = prediction;
|
||||||
|
},
|
||||||
),
|
),
|
||||||
const SizedBox(height: UiConstants.space8),
|
const SizedBox(height: UiConstants.space8),
|
||||||
Row(
|
Row(
|
||||||
@@ -97,6 +112,13 @@ class _AddHubDialogState extends State<AddHubDialog> {
|
|||||||
widget.onCreate(
|
widget.onCreate(
|
||||||
_nameController.text,
|
_nameController.text,
|
||||||
_addressController.text,
|
_addressController.text,
|
||||||
|
placeId: _selectedPrediction?.placeId,
|
||||||
|
latitude: double.tryParse(
|
||||||
|
_selectedPrediction?.lat ?? '',
|
||||||
|
),
|
||||||
|
longitude: double.tryParse(
|
||||||
|
_selectedPrediction?.lng ?? '',
|
||||||
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -9,28 +9,34 @@ class HubAddressAutocomplete extends StatelessWidget {
|
|||||||
const HubAddressAutocomplete({
|
const HubAddressAutocomplete({
|
||||||
required this.controller,
|
required this.controller,
|
||||||
required this.hintText,
|
required this.hintText,
|
||||||
|
this.focusNode,
|
||||||
|
this.onSelected,
|
||||||
super.key,
|
super.key,
|
||||||
});
|
});
|
||||||
|
|
||||||
final TextEditingController controller;
|
final TextEditingController controller;
|
||||||
final String hintText;
|
final String hintText;
|
||||||
|
final FocusNode? focusNode;
|
||||||
|
final void Function(Prediction prediction)? onSelected;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return GooglePlaceAutoCompleteTextField(
|
return GooglePlaceAutoCompleteTextField(
|
||||||
textEditingController: controller,
|
textEditingController: controller,
|
||||||
|
focusNode: focusNode,
|
||||||
googleAPIKey: HubsConstants.googlePlacesApiKey,
|
googleAPIKey: HubsConstants.googlePlacesApiKey,
|
||||||
debounceTime: 500,
|
debounceTime: 500,
|
||||||
countries: HubsConstants.supportedCountries,
|
countries: HubsConstants.supportedCountries,
|
||||||
isLatLngRequired: false,
|
isLatLngRequired: true,
|
||||||
getPlaceDetailWithLatLng: (Prediction prediction) {
|
getPlaceDetailWithLatLng: (Prediction prediction) {
|
||||||
// Handle lat/lng if needed in the future
|
onSelected?.call(prediction);
|
||||||
},
|
},
|
||||||
itemClick: (Prediction prediction) {
|
itemClick: (Prediction prediction) {
|
||||||
controller.text = prediction.description ?? '';
|
controller.text = prediction.description ?? '';
|
||||||
controller.selection = TextSelection.fromPosition(
|
controller.selection = TextSelection.fromPosition(
|
||||||
TextPosition(offset: controller.text.length),
|
TextPosition(offset: controller.text.length),
|
||||||
);
|
);
|
||||||
|
onSelected?.call(prediction);
|
||||||
},
|
},
|
||||||
itemBuilder: (_, _, Prediction prediction) {
|
itemBuilder: (_, _, Prediction prediction) {
|
||||||
return Padding(
|
return Padding(
|
||||||
|
|||||||
@@ -31,6 +31,7 @@ dependencies:
|
|||||||
firebase_auth: ^6.1.4
|
firebase_auth: ^6.1.4
|
||||||
firebase_data_connect: ^0.2.2+2
|
firebase_data_connect: ^0.2.2+2
|
||||||
google_places_flutter: ^2.1.1
|
google_places_flutter: ^2.1.1
|
||||||
|
http: ^1.2.2
|
||||||
|
|
||||||
dev_dependencies:
|
dev_dependencies:
|
||||||
flutter_test:
|
flutter_test:
|
||||||
|
|||||||
Reference in New Issue
Block a user