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_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<domain.Hub> 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<dc.CreateTeamHubData, dc.CreateTeamHubVariables> 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',
<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.
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<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.
/// 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].
Future<void> deleteHub(String id);

View File

@@ -21,6 +21,14 @@ class CreateHubUseCase implements UseCase<CreateHubArguments, Hub> {
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,
);
}
}

View File

@@ -84,7 +84,18 @@ class ClientHubsBloc extends Bloc<ClientHubsEvent, ClientHubsState>
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<Hub> hubs = await _getHubsUseCase();
emit(

View File

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

View File

@@ -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<ClientHubsBloc>(context).add(
ClientHubsAddRequested(name: name, address: address),
ClientHubsAddRequested(
name: name,
address: address,
placeId: placeId,
latitude: latitude,
longitude: longitude,
),
);
},
onCancel: () => BlocProvider.of<ClientHubsBloc>(

View File

@@ -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<AddHubDialog> {
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<AddHubDialog> {
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<AddHubDialog> {
widget.onCreate(
_nameController.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({
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(

View File

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