feat: architecture overhaul, launchpad-style reports, and uber-style locations
- Strengthened Buffer Layer architecture to decouple Data Connect from Domain - Rewired Coverage, Performance, and Forecast reports to match Launchpad logic - Implemented Uber-style Preferred Locations search using Google Places API - Added session recovery logic to prevent crashes on app restart - Synchronized backend schemas & SDK for ShiftStatus enums - Fixed various build/compilation errors and localization duplicates
This commit is contained in:
@@ -1,38 +1,30 @@
|
||||
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_core/core.dart';
|
||||
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'
|
||||
show
|
||||
HubHasOrdersException,
|
||||
BusinessNotFoundException,
|
||||
NotAuthenticatedException;
|
||||
|
||||
import 'package:krow_domain/krow_domain.dart';
|
||||
import '../../domain/repositories/hub_repository_interface.dart';
|
||||
|
||||
/// Implementation of [HubRepositoryInterface] backed by Data Connect.
|
||||
/// Implementation of [HubRepositoryInterface] that delegates to [dc.HubsConnectorRepository].
|
||||
///
|
||||
/// This implementation follows the "Buffer Layer" pattern by using a dedicated
|
||||
/// connector repository from the data_connect package.
|
||||
class HubRepositoryImpl implements HubRepositoryInterface {
|
||||
HubRepositoryImpl({required dc.DataConnectService service})
|
||||
: _service = service;
|
||||
|
||||
final dc.HubsConnectorRepository _connectorRepository;
|
||||
final dc.DataConnectService _service;
|
||||
|
||||
HubRepositoryImpl({
|
||||
dc.HubsConnectorRepository? connectorRepository,
|
||||
dc.DataConnectService? service,
|
||||
}) : _connectorRepository = connectorRepository ??
|
||||
dc.DataConnectService.instance.getHubsRepository(),
|
||||
_service = service ?? dc.DataConnectService.instance;
|
||||
|
||||
@override
|
||||
Future<List<domain.Hub>> getHubs() async {
|
||||
return _service.run(() async {
|
||||
final dc.GetBusinessesByUserIdBusinesses business =
|
||||
await _getBusinessForCurrentUser();
|
||||
final String teamId = await _getOrCreateTeamId(business);
|
||||
return _fetchHubsForTeam(teamId: teamId, businessId: business.id);
|
||||
});
|
||||
Future<List<Hub>> getHubs() async {
|
||||
final businessId = await _service.getBusinessId();
|
||||
return _connectorRepository.getHubs(businessId: businessId);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<domain.Hub> createHub({
|
||||
Future<Hub> createHub({
|
||||
required String name,
|
||||
required String address,
|
||||
String? placeId,
|
||||
@@ -44,77 +36,26 @@ class HubRepositoryImpl implements HubRepositoryInterface {
|
||||
String? country,
|
||||
String? zipCode,
|
||||
}) async {
|
||||
return _service.run(() async {
|
||||
final dc.GetBusinessesByUserIdBusinesses business =
|
||||
await _getBusinessForCurrentUser();
|
||||
final String teamId = await _getOrCreateTeamId(business);
|
||||
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 _service.connector
|
||||
.createTeamHub(teamId: teamId, hubName: name, address: address)
|
||||
.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;
|
||||
|
||||
final List<domain.Hub> hubs = await _fetchHubsForTeam(
|
||||
teamId: teamId,
|
||||
businessId: business.id,
|
||||
);
|
||||
domain.Hub? createdHub;
|
||||
for (final domain.Hub hub in hubs) {
|
||||
if (hub.id == createdId) {
|
||||
createdHub = hub;
|
||||
break;
|
||||
}
|
||||
}
|
||||
return createdHub ??
|
||||
domain.Hub(
|
||||
id: createdId,
|
||||
businessId: business.id,
|
||||
name: name,
|
||||
address: address,
|
||||
nfcTagId: null,
|
||||
status: domain.HubStatus.active,
|
||||
);
|
||||
});
|
||||
final businessId = await _service.getBusinessId();
|
||||
return _connectorRepository.createHub(
|
||||
businessId: businessId,
|
||||
name: name,
|
||||
address: address,
|
||||
placeId: placeId,
|
||||
latitude: latitude,
|
||||
longitude: longitude,
|
||||
city: city,
|
||||
state: state,
|
||||
street: street,
|
||||
country: country,
|
||||
zipCode: zipCode,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> deleteHub(String id) async {
|
||||
return _service.run(() async {
|
||||
final String businessId = await _service.getBusinessId();
|
||||
|
||||
final QueryResult<
|
||||
dc.ListOrdersByBusinessAndTeamHubData,
|
||||
dc.ListOrdersByBusinessAndTeamHubVariables
|
||||
>
|
||||
result = await _service.connector
|
||||
.listOrdersByBusinessAndTeamHub(businessId: businessId, teamHubId: id)
|
||||
.execute();
|
||||
|
||||
if (result.data.orders.isNotEmpty) {
|
||||
throw HubHasOrdersException(
|
||||
technicalMessage: 'Hub $id has ${result.data.orders.length} orders',
|
||||
);
|
||||
}
|
||||
|
||||
await _service.connector.deleteTeamHub(id: id).execute();
|
||||
});
|
||||
final businessId = await _service.getBusinessId();
|
||||
return _connectorRepository.deleteHub(businessId: businessId, id: id);
|
||||
}
|
||||
|
||||
@override
|
||||
@@ -125,7 +66,7 @@ class HubRepositoryImpl implements HubRepositoryInterface {
|
||||
}
|
||||
|
||||
@override
|
||||
Future<domain.Hub> updateHub({
|
||||
Future<Hub> updateHub({
|
||||
required String id,
|
||||
String? name,
|
||||
String? address,
|
||||
@@ -138,283 +79,20 @@ class HubRepositoryImpl implements HubRepositoryInterface {
|
||||
String? country,
|
||||
String? zipCode,
|
||||
}) async {
|
||||
return _service.run(() async {
|
||||
final _PlaceAddress? placeAddress =
|
||||
placeId == null || placeId.isEmpty
|
||||
? null
|
||||
: await _fetchPlaceAddress(placeId);
|
||||
|
||||
final dc.UpdateTeamHubVariablesBuilder builder = _service.connector
|
||||
.updateTeamHub(id: id);
|
||||
|
||||
if (name != null) builder.hubName(name);
|
||||
if (address != null) builder.address(address);
|
||||
if (placeId != null || placeAddress != null) {
|
||||
builder.placeId(placeId ?? placeAddress?.street);
|
||||
}
|
||||
if (latitude != null) builder.latitude(latitude);
|
||||
if (longitude != null) builder.longitude(longitude);
|
||||
if (city != null || placeAddress?.city != null) {
|
||||
builder.city(city ?? placeAddress?.city);
|
||||
}
|
||||
if (state != null || placeAddress?.state != null) {
|
||||
builder.state(state ?? placeAddress?.state);
|
||||
}
|
||||
if (street != null || placeAddress?.street != null) {
|
||||
builder.street(street ?? placeAddress?.street);
|
||||
}
|
||||
if (country != null || placeAddress?.country != null) {
|
||||
builder.country(country ?? placeAddress?.country);
|
||||
}
|
||||
if (zipCode != null || placeAddress?.zipCode != null) {
|
||||
builder.zipCode(zipCode ?? placeAddress?.zipCode);
|
||||
}
|
||||
|
||||
await builder.execute();
|
||||
|
||||
final dc.GetBusinessesByUserIdBusinesses business =
|
||||
await _getBusinessForCurrentUser();
|
||||
final String teamId = await _getOrCreateTeamId(business);
|
||||
final List<domain.Hub> hubs = await _fetchHubsForTeam(
|
||||
teamId: teamId,
|
||||
businessId: business.id,
|
||||
);
|
||||
|
||||
for (final domain.Hub hub in hubs) {
|
||||
if (hub.id == id) return hub;
|
||||
}
|
||||
|
||||
// Fallback: return a reconstructed Hub from the update inputs.
|
||||
return domain.Hub(
|
||||
id: id,
|
||||
businessId: business.id,
|
||||
name: name ?? '',
|
||||
address: address ?? '',
|
||||
nfcTagId: null,
|
||||
status: domain.HubStatus.active,
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
Future<dc.GetBusinessesByUserIdBusinesses>
|
||||
_getBusinessForCurrentUser() async {
|
||||
final dc.ClientSession? session = dc.ClientSessionStore.instance.session;
|
||||
final dc.ClientBusinessSession? cachedBusiness = session?.business;
|
||||
if (cachedBusiness != null) {
|
||||
return dc.GetBusinessesByUserIdBusinesses(
|
||||
id: cachedBusiness.id,
|
||||
businessName: cachedBusiness.businessName,
|
||||
userId: _service.auth.currentUser?.uid ?? '',
|
||||
rateGroup: const dc.Known<dc.BusinessRateGroup>(
|
||||
dc.BusinessRateGroup.STANDARD,
|
||||
),
|
||||
status: const dc.Known<dc.BusinessStatus>(dc.BusinessStatus.ACTIVE),
|
||||
contactName: cachedBusiness.contactName,
|
||||
companyLogoUrl: cachedBusiness.companyLogoUrl,
|
||||
phone: null,
|
||||
email: cachedBusiness.email,
|
||||
hubBuilding: null,
|
||||
address: null,
|
||||
city: cachedBusiness.city,
|
||||
area: null,
|
||||
sector: null,
|
||||
notes: null,
|
||||
createdAt: null,
|
||||
updatedAt: null,
|
||||
);
|
||||
}
|
||||
|
||||
final firebase.User? user = _service.auth.currentUser;
|
||||
if (user == null) {
|
||||
throw const NotAuthenticatedException(
|
||||
technicalMessage: 'No Firebase user in currentUser',
|
||||
);
|
||||
}
|
||||
|
||||
final QueryResult<
|
||||
dc.GetBusinessesByUserIdData,
|
||||
dc.GetBusinessesByUserIdVariables
|
||||
>
|
||||
result = await _service.connector
|
||||
.getBusinessesByUserId(userId: user.uid)
|
||||
.execute();
|
||||
if (result.data.businesses.isEmpty) {
|
||||
await _service.auth.signOut();
|
||||
throw BusinessNotFoundException(
|
||||
technicalMessage: 'No business found for user ${user.uid}',
|
||||
);
|
||||
}
|
||||
|
||||
final dc.GetBusinessesByUserIdBusinesses business =
|
||||
result.data.businesses.first;
|
||||
if (session != null) {
|
||||
dc.ClientSessionStore.instance.setSession(
|
||||
dc.ClientSession(
|
||||
business: dc.ClientBusinessSession(
|
||||
id: business.id,
|
||||
businessName: business.businessName,
|
||||
email: business.email,
|
||||
city: business.city,
|
||||
contactName: business.contactName,
|
||||
companyLogoUrl: business.companyLogoUrl,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
return business;
|
||||
}
|
||||
|
||||
Future<String> _getOrCreateTeamId(
|
||||
dc.GetBusinessesByUserIdBusinesses business,
|
||||
) async {
|
||||
final QueryResult<dc.GetTeamsByOwnerIdData, dc.GetTeamsByOwnerIdVariables>
|
||||
teamsResult = await _service.connector
|
||||
.getTeamsByOwnerId(ownerId: business.id)
|
||||
.execute();
|
||||
if (teamsResult.data.teams.isNotEmpty) {
|
||||
return teamsResult.data.teams.first.id;
|
||||
}
|
||||
|
||||
final dc.CreateTeamVariablesBuilder createTeamBuilder = _service.connector
|
||||
.createTeam(
|
||||
teamName: '${business.businessName} Team',
|
||||
ownerId: business.id,
|
||||
ownerName: business.contactName ?? '',
|
||||
ownerRole: 'OWNER',
|
||||
);
|
||||
if (business.email != null) {
|
||||
createTeamBuilder.email(business.email);
|
||||
}
|
||||
|
||||
final OperationResult<dc.CreateTeamData, dc.CreateTeamVariables>
|
||||
createTeamResult = await createTeamBuilder.execute();
|
||||
final String teamId = createTeamResult.data.team_insert.id;
|
||||
|
||||
return teamId;
|
||||
}
|
||||
|
||||
Future<List<domain.Hub>> _fetchHubsForTeam({
|
||||
required String teamId,
|
||||
required String businessId,
|
||||
}) async {
|
||||
final QueryResult<
|
||||
dc.GetTeamHubsByTeamIdData,
|
||||
dc.GetTeamHubsByTeamIdVariables
|
||||
>
|
||||
hubsResult = await _service.connector
|
||||
.getTeamHubsByTeamId(teamId: teamId)
|
||||
.execute();
|
||||
|
||||
return hubsResult.data.teamHubs
|
||||
.map(
|
||||
(dc.GetTeamHubsByTeamIdTeamHubs hub) => domain.Hub(
|
||||
id: hub.id,
|
||||
businessId: businessId,
|
||||
name: hub.hubName,
|
||||
address: hub.address,
|
||||
nfcTagId: null,
|
||||
status: hub.isActive
|
||||
? domain.HubStatus.active
|
||||
: domain.HubStatus.inactive,
|
||||
),
|
||||
)
|
||||
.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': AppConfig.googleMapsApiKey,
|
||||
},
|
||||
final businessId = await _service.getBusinessId();
|
||||
return _connectorRepository.updateHub(
|
||||
businessId: businessId,
|
||||
id: id,
|
||||
name: name,
|
||||
address: address,
|
||||
placeId: placeId,
|
||||
latitude: latitude,
|
||||
longitude: longitude,
|
||||
city: city,
|
||||
state: state,
|
||||
street: street,
|
||||
country: country,
|
||||
zipCode: zipCode,
|
||||
);
|
||||
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;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user