diff --git a/apps/mobile/packages/features/client/hubs/lib/src/data/repositories_impl/hub_repository_impl.dart b/apps/mobile/packages/features/client/hubs/lib/src/data/repositories_impl/hub_repository_impl.dart index 190bc0ad..bdfa6198 100644 --- a/apps/mobile/packages/features/client/hubs/lib/src/data/repositories_impl/hub_repository_impl.dart +++ b/apps/mobile/packages/features/client/hubs/lib/src/data/repositories_impl/hub_repository_impl.dart @@ -1,9 +1,13 @@ +import 'dart:convert'; + import 'package:firebase_auth/firebase_auth.dart' as firebase; 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_domain/krow_domain.dart' as domain; import '../../domain/repositories/hub_repository_interface.dart'; +import '../../util/hubs_constants.dart'; /// Implementation of [HubRepositoryInterface] backed by Data Connect. class HubRepositoryImpl implements HubRepositoryInterface { @@ -27,10 +31,24 @@ class HubRepositoryImpl implements HubRepositoryInterface { Future createHub({ required String name, required String address, + String? placeId, + double? latitude, + double? longitude, + String? city, + String? state, + String? street, + String? country, + String? zipCode, }) async { final dc.GetBusinessesByUserIdBusinesses business = await _getBusinessForCurrentUser(); 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 result = await _dataConnect .createTeamHub( @@ -38,7 +56,14 @@ class HubRepositoryImpl implements HubRepositoryInterface { hubName: name, 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(); final String? createdId = result.data?.teamHub_insert.id; if (createdId == null) { @@ -192,4 +217,99 @@ class HubRepositoryImpl implements HubRepositoryInterface { ) .toList(); } + + Future<_PlaceAddress?> _fetchPlaceAddress(String placeId) async { + final Uri uri = Uri.https( + 'maps.googleapis.com', + '/maps/api/place/details/json', + { + '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 payload = + json.decode(response.body) as Map; + if (payload['status'] != 'OK') { + return null; + } + + final Map? result = + payload['result'] as Map?; + final List? components = + result?['address_components'] as List?; + 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 component = entry as Map; + final List types = component['types'] as List? ?? []; + 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 = [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; } diff --git a/apps/mobile/packages/features/client/hubs/lib/src/domain/arguments/create_hub_arguments.dart b/apps/mobile/packages/features/client/hubs/lib/src/domain/arguments/create_hub_arguments.dart index a978f3a2..8518d9f0 100644 --- a/apps/mobile/packages/features/client/hubs/lib/src/domain/arguments/create_hub_arguments.dart +++ b/apps/mobile/packages/features/client/hubs/lib/src/domain/arguments/create_hub_arguments.dart @@ -10,11 +10,42 @@ class CreateHubArguments extends UseCaseArgument { /// The physical address of the hub. 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. /// /// 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 - List get props => [name, address]; + List get props => [ + name, + address, + placeId, + latitude, + longitude, + city, + state, + street, + country, + zipCode, + ]; } diff --git a/apps/mobile/packages/features/client/hubs/lib/src/domain/repositories/hub_repository_interface.dart b/apps/mobile/packages/features/client/hubs/lib/src/domain/repositories/hub_repository_interface.dart index 5b03fced..5580e6e4 100644 --- a/apps/mobile/packages/features/client/hubs/lib/src/domain/repositories/hub_repository_interface.dart +++ b/apps/mobile/packages/features/client/hubs/lib/src/domain/repositories/hub_repository_interface.dart @@ -15,7 +15,18 @@ abstract interface class HubRepositoryInterface { /// /// Takes the [name] and [address] of the new hub. /// Returns the created [Hub] entity. - Future createHub({required String name, required String address}); + Future 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]. Future deleteHub(String id); diff --git a/apps/mobile/packages/features/client/hubs/lib/src/domain/usecases/create_hub_usecase.dart b/apps/mobile/packages/features/client/hubs/lib/src/domain/usecases/create_hub_usecase.dart index bbfc1403..50550bc1 100644 --- a/apps/mobile/packages/features/client/hubs/lib/src/domain/usecases/create_hub_usecase.dart +++ b/apps/mobile/packages/features/client/hubs/lib/src/domain/usecases/create_hub_usecase.dart @@ -21,6 +21,14 @@ class CreateHubUseCase implements UseCase { return _repository.createHub( name: arguments.name, 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, ); } } diff --git a/apps/mobile/packages/features/client/hubs/lib/src/presentation/blocs/client_hubs_bloc.dart b/apps/mobile/packages/features/client/hubs/lib/src/presentation/blocs/client_hubs_bloc.dart index be1ecc42..2359f296 100644 --- a/apps/mobile/packages/features/client/hubs/lib/src/presentation/blocs/client_hubs_bloc.dart +++ b/apps/mobile/packages/features/client/hubs/lib/src/presentation/blocs/client_hubs_bloc.dart @@ -84,7 +84,18 @@ class ClientHubsBloc extends Bloc emit(state.copyWith(status: ClientHubsStatus.actionInProgress)); try { 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 hubs = await _getHubsUseCase(); emit( diff --git a/apps/mobile/packages/features/client/hubs/lib/src/presentation/blocs/client_hubs_event.dart b/apps/mobile/packages/features/client/hubs/lib/src/presentation/blocs/client_hubs_event.dart index a42a4843..428eb774 100644 --- a/apps/mobile/packages/features/client/hubs/lib/src/presentation/blocs/client_hubs_event.dart +++ b/apps/mobile/packages/features/client/hubs/lib/src/presentation/blocs/client_hubs_event.dart @@ -18,11 +18,41 @@ class ClientHubsFetched extends ClientHubsEvent { class ClientHubsAddRequested extends ClientHubsEvent { final String name; 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 - List get props => [name, address]; + List get props => [ + name, + address, + placeId, + latitude, + longitude, + city, + state, + street, + country, + zipCode, + ]; } /// Event triggered to delete a hub. diff --git a/apps/mobile/packages/features/client/hubs/lib/src/presentation/pages/client_hubs_page.dart b/apps/mobile/packages/features/client/hubs/lib/src/presentation/pages/client_hubs_page.dart index 67e84b41..d3e13cdc 100644 --- a/apps/mobile/packages/features/client/hubs/lib/src/presentation/pages/client_hubs_page.dart +++ b/apps/mobile/packages/features/client/hubs/lib/src/presentation/pages/client_hubs_page.dart @@ -106,9 +106,21 @@ class ClientHubsPage extends StatelessWidget { ), if (state.showAddHubDialog) AddHubDialog( - onCreate: (String name, String address) { + onCreate: ( + String name, + String address, { + String? placeId, + double? latitude, + double? longitude, + }) { BlocProvider.of(context).add( - ClientHubsAddRequested(name: name, address: address), + ClientHubsAddRequested( + name: name, + address: address, + placeId: placeId, + latitude: latitude, + longitude: longitude, + ), ); }, onCancel: () => BlocProvider.of( diff --git a/apps/mobile/packages/features/client/hubs/lib/src/presentation/widgets/add_hub_dialog.dart b/apps/mobile/packages/features/client/hubs/lib/src/presentation/widgets/add_hub_dialog.dart index f9124071..7d95c749 100644 --- a/apps/mobile/packages/features/client/hubs/lib/src/presentation/widgets/add_hub_dialog.dart +++ b/apps/mobile/packages/features/client/hubs/lib/src/presentation/widgets/add_hub_dialog.dart @@ -1,13 +1,20 @@ import 'package:design_system/design_system.dart'; import 'package:flutter/material.dart'; import 'package:core_localization/core_localization.dart'; +import 'package:google_places_flutter/model/prediction.dart'; import 'hub_address_autocomplete.dart'; /// A dialog for adding a new hub. class AddHubDialog extends StatefulWidget { /// 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. final VoidCallback onCancel; @@ -26,18 +33,22 @@ class AddHubDialog extends StatefulWidget { class _AddHubDialogState extends State { late final TextEditingController _nameController; late final TextEditingController _addressController; + late final FocusNode _addressFocusNode; + Prediction? _selectedPrediction; @override void initState() { super.initState(); _nameController = TextEditingController(); _addressController = TextEditingController(); + _addressFocusNode = FocusNode(); } @override void dispose() { _nameController.dispose(); _addressController.dispose(); + _addressFocusNode.dispose(); super.dispose(); } @@ -79,6 +90,10 @@ class _AddHubDialogState extends State { HubAddressAutocomplete( controller: _addressController, hintText: t.client_hubs.add_hub_dialog.address_hint, + focusNode: _addressFocusNode, + onSelected: (Prediction prediction) { + _selectedPrediction = prediction; + }, ), const SizedBox(height: UiConstants.space8), Row( @@ -97,6 +112,13 @@ class _AddHubDialogState extends State { widget.onCreate( _nameController.text, _addressController.text, + placeId: _selectedPrediction?.placeId, + latitude: double.tryParse( + _selectedPrediction?.lat ?? '', + ), + longitude: double.tryParse( + _selectedPrediction?.lng ?? '', + ), ); } }, diff --git a/apps/mobile/packages/features/client/hubs/lib/src/presentation/widgets/hub_address_autocomplete.dart b/apps/mobile/packages/features/client/hubs/lib/src/presentation/widgets/hub_address_autocomplete.dart index ef269f50..784cf094 100644 --- a/apps/mobile/packages/features/client/hubs/lib/src/presentation/widgets/hub_address_autocomplete.dart +++ b/apps/mobile/packages/features/client/hubs/lib/src/presentation/widgets/hub_address_autocomplete.dart @@ -9,28 +9,34 @@ class HubAddressAutocomplete extends StatelessWidget { const HubAddressAutocomplete({ required this.controller, required this.hintText, + this.focusNode, + this.onSelected, super.key, }); final TextEditingController controller; final String hintText; + final FocusNode? focusNode; + final void Function(Prediction prediction)? onSelected; @override Widget build(BuildContext context) { return GooglePlaceAutoCompleteTextField( textEditingController: controller, + focusNode: focusNode, googleAPIKey: HubsConstants.googlePlacesApiKey, debounceTime: 500, countries: HubsConstants.supportedCountries, - isLatLngRequired: false, + isLatLngRequired: true, getPlaceDetailWithLatLng: (Prediction prediction) { - // Handle lat/lng if needed in the future + onSelected?.call(prediction); }, itemClick: (Prediction prediction) { controller.text = prediction.description ?? ''; controller.selection = TextSelection.fromPosition( TextPosition(offset: controller.text.length), ); + onSelected?.call(prediction); }, itemBuilder: (_, _, Prediction prediction) { return Padding( diff --git a/apps/mobile/packages/features/client/hubs/pubspec.yaml b/apps/mobile/packages/features/client/hubs/pubspec.yaml index a5394d89..1eaf1911 100644 --- a/apps/mobile/packages/features/client/hubs/pubspec.yaml +++ b/apps/mobile/packages/features/client/hubs/pubspec.yaml @@ -31,6 +31,7 @@ dependencies: firebase_auth: ^6.1.4 firebase_data_connect: ^0.2.2+2 google_places_flutter: ^2.1.1 + http: ^1.2.2 dev_dependencies: flutter_test: