hubs ready

This commit is contained in:
José Salazar
2026-01-29 15:03:26 -05:00
parent 13a3f5a007
commit 0afb89e86a
10 changed files with 265 additions and 13 deletions

View File

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

View File

@@ -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,
];
} }

View File

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

View File

@@ -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,
); );
} }
} }

View File

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

View File

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

View File

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

View File

@@ -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 ?? '',
),
); );
} }
}, },

View File

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

View File

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