feat: Migrate staff profile features from Data Connect to V2 REST API

- Removed data_connect package from mobile pubspec.yaml.
- Added documentation for V2 profile migration status and QA findings.
- Implemented new session management with ClientSessionStore and StaffSessionStore.
- Created V2SessionService for handling user sessions via the V2 API.
- Developed use cases for cancelling late worker assignments and submitting worker reviews.
- Added arguments and use cases for payment chart retrieval and profile completion checks.
- Implemented repository interfaces and their implementations for staff main and profile features.
- Ensured proper error handling and validation in use cases.
This commit is contained in:
Achintha Isuru
2026-03-16 22:45:06 -04:00
parent 4834266986
commit b31a615092
478 changed files with 10512 additions and 19854 deletions

View File

@@ -1,51 +1,46 @@
// ignore_for_file: always_specify_types, depend_on_referenced_packages, dead_code, dead_null_aware_expression, unused_local_variable, unused_import, sort_constructors_first, prefer_final_fields, prefer_const_constructors, deprecated_member_use, implicit_call_tearoffs
import 'package:krow_data_connect/krow_data_connect.dart' as dc;
import 'package:krow_core/core.dart';
import 'package:krow_domain/krow_domain.dart';
import '../../domain/repositories/hub_repository_interface.dart';
/// Implementation of [HubRepositoryInterface] that delegates to [dc.HubsConnectorRepository].
import 'package:client_hubs/src/domain/repositories/hub_repository_interface.dart';
/// Implementation of [HubRepositoryInterface] using the V2 REST API.
///
/// This implementation follows the "Buffer Layer" pattern by using a dedicated
/// connector repository from the data_connect package.
/// All backend calls go through [BaseApiService] with [V2ApiEndpoints].
class HubRepositoryImpl implements HubRepositoryInterface {
/// Creates a [HubRepositoryImpl].
HubRepositoryImpl({required BaseApiService apiService})
: _apiService = apiService;
HubRepositoryImpl({
dc.HubsConnectorRepository? connectorRepository,
dc.DataConnectService? service,
}) : _connectorRepository = connectorRepository ??
dc.DataConnectService.instance.getHubsRepository(),
_service = service ?? dc.DataConnectService.instance;
final dc.HubsConnectorRepository _connectorRepository;
final dc.DataConnectService _service;
/// The API service for HTTP requests.
final BaseApiService _apiService;
@override
Future<List<Hub>> getHubs() async {
final String businessId = await _service.getBusinessId();
return _connectorRepository.getHubs(businessId: businessId);
final ApiResponse response =
await _apiService.get(V2ApiEndpoints.clientHubs);
final List<dynamic> items =
(response.data as Map<String, dynamic>)['items'] as List<dynamic>;
return items
.map((dynamic json) => Hub.fromJson(json as Map<String, dynamic>))
.toList();
}
@override
Future<List<CostCenter>> getCostCenters() async {
return _service.run(() async {
final result = await _service.connector.listTeamHudDepartments().execute();
final Set<String> seen = <String>{};
final List<CostCenter> costCenters = <CostCenter>[];
for (final dc.ListTeamHudDepartmentsTeamHudDepartments dep
in result.data.teamHudDepartments) {
final String? cc = dep.costCenter;
if (cc != null && cc.isNotEmpty && !seen.contains(cc)) {
seen.add(cc);
costCenters.add(CostCenter(id: cc, name: dep.name, code: cc));
}
}
return costCenters;
});
final ApiResponse response =
await _apiService.get(V2ApiEndpoints.clientCostCenters);
final List<dynamic> items =
(response.data as Map<String, dynamic>)['items'] as List<dynamic>;
return items
.map((dynamic json) =>
CostCenter.fromJson(json as Map<String, dynamic>))
.toList();
}
@override
Future<Hub> createHub({
Future<String> createHub({
required String name,
required String address,
required String fullAddress,
String? placeId,
double? latitude,
double? longitude,
@@ -56,41 +51,32 @@ class HubRepositoryImpl implements HubRepositoryInterface {
String? zipCode,
String? costCenterId,
}) async {
final String 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,
costCenterId: costCenterId,
final ApiResponse response = await _apiService.post(
V2ApiEndpoints.clientHubCreate,
data: <String, dynamic>{
'name': name,
'fullAddress': fullAddress,
if (placeId != null) 'placeId': placeId,
if (latitude != null) 'latitude': latitude,
if (longitude != null) 'longitude': longitude,
if (city != null) 'city': city,
if (state != null) 'state': state,
if (street != null) 'street': street,
if (country != null) 'country': country,
if (zipCode != null) 'zipCode': zipCode,
if (costCenterId != null) 'costCenterId': costCenterId,
},
);
final Map<String, dynamic> data =
response.data as Map<String, dynamic>;
return data['hubId'] as String;
}
@override
Future<void> deleteHub(String id) async {
final String businessId = await _service.getBusinessId();
return _connectorRepository.deleteHub(businessId: businessId, id: id);
}
@override
Future<void> assignNfcTag({required String hubId, required String nfcTagId}) {
throw UnimplementedError(
'NFC tag assignment is not supported for team hubs.',
);
}
@override
Future<Hub> updateHub({
required String id,
Future<String> updateHub({
required String hubId,
String? name,
String? address,
String? fullAddress,
String? placeId,
double? latitude,
double? longitude,
@@ -101,22 +87,66 @@ class HubRepositoryImpl implements HubRepositoryInterface {
String? zipCode,
String? costCenterId,
}) async {
final String 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,
costCenterId: costCenterId,
final ApiResponse response = await _apiService.put(
V2ApiEndpoints.clientHubUpdate(hubId),
data: <String, dynamic>{
'hubId': hubId,
if (name != null) 'name': name,
if (fullAddress != null) 'fullAddress': fullAddress,
if (placeId != null) 'placeId': placeId,
if (latitude != null) 'latitude': latitude,
if (longitude != null) 'longitude': longitude,
if (city != null) 'city': city,
if (state != null) 'state': state,
if (street != null) 'street': street,
if (country != null) 'country': country,
if (zipCode != null) 'zipCode': zipCode,
if (costCenterId != null) 'costCenterId': costCenterId,
},
);
final Map<String, dynamic> data =
response.data as Map<String, dynamic>;
return data['hubId'] as String;
}
@override
Future<void> deleteHub(String hubId) async {
await _apiService.delete(V2ApiEndpoints.clientHubDelete(hubId));
}
@override
Future<void> assignNfcTag({
required String hubId,
required String nfcTagId,
}) async {
await _apiService.post(
V2ApiEndpoints.clientHubAssignNfc(hubId),
data: <String, dynamic>{'nfcTagId': nfcTagId},
);
}
@override
Future<List<HubManager>> getManagers(String hubId) async {
final ApiResponse response =
await _apiService.get(V2ApiEndpoints.clientHubManagers(hubId));
final List<dynamic> items =
(response.data as Map<String, dynamic>)['items'] as List<dynamic>;
return items
.map((dynamic json) =>
HubManager.fromJson(json as Map<String, dynamic>))
.toList();
}
@override
Future<void> assignManagers({
required String hubId,
required List<String> businessMembershipIds,
}) async {
await _apiService.post(
V2ApiEndpoints.clientHubAssignManagers(hubId),
data: <String, dynamic>{
'businessMembershipIds': businessMembershipIds,
},
);
}
}

View File

@@ -1,14 +1,12 @@
import 'package:krow_core/core.dart';
/// Represents the arguments required for the AssignNfcTagUseCase.
/// Arguments for the [AssignNfcTagUseCase].
///
/// Encapsulates the hub ID and the NFC tag ID to be assigned.
class AssignNfcTagArguments extends UseCaseArgument {
/// Creates an [AssignNfcTagArguments] instance.
///
/// Both [hubId] and [nfcTagId] are required.
const AssignNfcTagArguments({required this.hubId, required this.nfcTagId});
/// The unique identifier of the hub.
final String hubId;

View File

@@ -1,16 +1,13 @@
import 'package:krow_core/core.dart';
/// Represents the arguments required for the CreateHubUseCase.
/// Arguments for the [CreateHubUseCase].
///
/// Encapsulates the name and address of the hub to be created.
class CreateHubArguments extends UseCaseArgument {
/// Creates a [CreateHubArguments] instance.
///
/// Both [name] and [address] are required.
const CreateHubArguments({
required this.name,
required this.address,
required this.fullAddress,
this.placeId,
this.latitude,
this.longitude,
@@ -21,36 +18,52 @@ class CreateHubArguments extends UseCaseArgument {
this.zipCode,
this.costCenterId,
});
/// The name of the hub.
/// The display name of the hub.
final String name;
/// The physical address of the hub.
final String address;
/// The full street address.
final String fullAddress;
/// Google Place ID.
final String? placeId;
/// GPS latitude.
final double? latitude;
/// GPS longitude.
final double? longitude;
/// City.
final String? city;
/// State.
final String? state;
/// Street.
final String? street;
/// Country.
final String? country;
/// Zip code.
final String? zipCode;
/// The cost center of the hub.
/// Associated cost center ID.
final String? costCenterId;
@override
List<Object?> get props => <Object?>[
name,
address,
placeId,
latitude,
longitude,
city,
state,
street,
country,
zipCode,
costCenterId,
];
name,
fullAddress,
placeId,
latitude,
longitude,
city,
state,
street,
country,
zipCode,
costCenterId,
];
}

View File

@@ -2,13 +2,10 @@ import 'package:krow_domain/krow_domain.dart';
/// Interface for the Hub repository.
///
/// This repository defines the contract for hub-related operations in the
/// domain layer. It handles fetching, creating, deleting hubs and assigning
/// NFC tags. The implementation will be provided in the data layer.
/// Defines the contract for hub-related operations. The implementation
/// uses the V2 REST API via [BaseApiService].
abstract interface class HubRepositoryInterface {
/// Fetches the list of hubs for the current client.
///
/// Returns a list of [Hub] entities.
Future<List<Hub>> getHubs();
/// Fetches the list of available cost centers for the current business.
@@ -16,11 +13,10 @@ abstract interface class HubRepositoryInterface {
/// Creates a new hub.
///
/// Takes the [name] and [address] of the new hub.
/// Returns the created [Hub] entity.
Future<Hub> createHub({
/// Returns the created hub ID.
Future<String> createHub({
required String name,
required String address,
required String fullAddress,
String? placeId,
double? latitude,
double? longitude,
@@ -32,21 +28,19 @@ abstract interface class HubRepositoryInterface {
String? costCenterId,
});
/// Deletes a hub by its [id].
Future<void> deleteHub(String id);
/// Deletes a hub by its [hubId].
Future<void> deleteHub(String hubId);
/// Assigns an NFC tag to a hub.
///
/// Takes the [hubId] and the [nfcTagId] to be associated.
Future<void> assignNfcTag({required String hubId, required String nfcTagId});
/// Updates an existing hub by its [id].
/// Updates an existing hub by its [hubId].
///
/// All fields other than [id] are optional — only supplied values are updated.
Future<Hub> updateHub({
required String id,
/// Only supplied values are updated.
Future<String> updateHub({
required String hubId,
String? name,
String? address,
String? fullAddress,
String? placeId,
double? latitude,
double? longitude,
@@ -57,4 +51,13 @@ abstract interface class HubRepositoryInterface {
String? zipCode,
String? costCenterId,
});
/// Fetches managers assigned to a hub.
Future<List<HubManager>> getManagers(String hubId);
/// Assigns managers to a hub.
Future<void> assignManagers({
required String hubId,
required List<String> businessMembershipIds,
});
}

View File

@@ -1,17 +1,16 @@
import 'package:krow_core/core.dart';
import '../arguments/assign_nfc_tag_arguments.dart';
import '../repositories/hub_repository_interface.dart';
import 'package:client_hubs/src/domain/arguments/assign_nfc_tag_arguments.dart';
import 'package:client_hubs/src/domain/repositories/hub_repository_interface.dart';
/// Use case for assigning an NFC tag to a hub.
///
/// This use case handles the association of a physical NFC tag with a specific
/// hub by calling the [HubRepositoryInterface].
/// Handles the association of a physical NFC tag with a specific hub.
class AssignNfcTagUseCase implements UseCase<AssignNfcTagArguments, void> {
/// Creates an [AssignNfcTagUseCase].
///
/// Requires a [HubRepositoryInterface] to interact with the backend.
AssignNfcTagUseCase(this._repository);
/// The repository for hub operations.
final HubRepositoryInterface _repository;
@override

View File

@@ -1,26 +1,24 @@
import 'package:krow_core/core.dart';
import 'package:krow_domain/krow_domain.dart';
import '../arguments/create_hub_arguments.dart';
import '../repositories/hub_repository_interface.dart';
import 'package:client_hubs/src/domain/arguments/create_hub_arguments.dart';
import 'package:client_hubs/src/domain/repositories/hub_repository_interface.dart';
/// Use case for creating a new hub.
///
/// This use case orchestrates the creation of a hub by interacting with the
/// [HubRepositoryInterface]. It requires [CreateHubArguments] which includes
/// the name and address of the hub.
class CreateHubUseCase implements UseCase<CreateHubArguments, Hub> {
/// Orchestrates hub creation by delegating to [HubRepositoryInterface].
/// Returns the created hub ID.
class CreateHubUseCase implements UseCase<CreateHubArguments, String> {
/// Creates a [CreateHubUseCase].
///
/// Requires a [HubRepositoryInterface] to perform the actual creation.
CreateHubUseCase(this._repository);
/// The repository for hub operations.
final HubRepositoryInterface _repository;
@override
Future<Hub> call(CreateHubArguments arguments) {
Future<String> call(CreateHubArguments arguments) {
return _repository.createHub(
name: arguments.name,
address: arguments.address,
fullAddress: arguments.fullAddress,
placeId: arguments.placeId,
latitude: arguments.latitude,
longitude: arguments.longitude,

View File

@@ -1,16 +1,16 @@
import 'package:krow_core/core.dart';
import '../arguments/delete_hub_arguments.dart';
import '../repositories/hub_repository_interface.dart';
import 'package:client_hubs/src/domain/arguments/delete_hub_arguments.dart';
import 'package:client_hubs/src/domain/repositories/hub_repository_interface.dart';
/// Use case for deleting a hub.
///
/// This use case removes a hub from the system via the [HubRepositoryInterface].
/// Removes a hub from the system via [HubRepositoryInterface].
class DeleteHubUseCase implements UseCase<DeleteHubArguments, void> {
/// Creates a [DeleteHubUseCase].
///
/// Requires a [HubRepositoryInterface] to perform the deletion.
DeleteHubUseCase(this._repository);
/// The repository for hub operations.
final HubRepositoryInterface _repository;
@override

View File

@@ -1,13 +1,17 @@
import 'package:krow_domain/krow_domain.dart';
import '../repositories/hub_repository_interface.dart';
/// Usecase to fetch all available cost centers.
import 'package:client_hubs/src/domain/repositories/hub_repository_interface.dart';
/// Use case to fetch all available cost centers.
class GetCostCentersUseCase {
/// Creates a [GetCostCentersUseCase].
GetCostCentersUseCase({required HubRepositoryInterface repository})
: _repository = repository;
/// The repository for hub operations.
final HubRepositoryInterface _repository;
/// Executes the use case.
Future<List<CostCenter>> call() async {
return _repository.getCostCenters();
}

View File

@@ -1,17 +1,16 @@
import 'package:krow_core/core.dart';
import 'package:krow_domain/krow_domain.dart';
import '../repositories/hub_repository_interface.dart';
import 'package:client_hubs/src/domain/repositories/hub_repository_interface.dart';
/// Use case for fetching the list of hubs.
///
/// This use case retrieves all hubs associated with the current client
/// by interacting with the [HubRepositoryInterface].
/// Retrieves all hubs associated with the current client.
class GetHubsUseCase implements NoInputUseCase<List<Hub>> {
/// Creates a [GetHubsUseCase].
///
/// Requires a [HubRepositoryInterface] to fetch the data.
GetHubsUseCase(this._repository);
/// The repository for hub operations.
final HubRepositoryInterface _repository;
@override

View File

@@ -1,14 +1,14 @@
import 'package:krow_core/core.dart';
import 'package:krow_domain/krow_domain.dart';
import '../repositories/hub_repository_interface.dart';
import 'package:client_hubs/src/domain/repositories/hub_repository_interface.dart';
/// Arguments for the UpdateHubUseCase.
/// Arguments for the [UpdateHubUseCase].
class UpdateHubArguments extends UseCaseArgument {
/// Creates an [UpdateHubArguments] instance.
const UpdateHubArguments({
required this.id,
required this.hubId,
this.name,
this.address,
this.fullAddress,
this.placeId,
this.latitude,
this.longitude,
@@ -20,48 +20,75 @@ class UpdateHubArguments extends UseCaseArgument {
this.costCenterId,
});
final String id;
/// The hub ID to update.
final String hubId;
/// Updated name.
final String? name;
final String? address;
/// Updated full address.
final String? fullAddress;
/// Updated Google Place ID.
final String? placeId;
/// Updated latitude.
final double? latitude;
/// Updated longitude.
final double? longitude;
/// Updated city.
final String? city;
/// Updated state.
final String? state;
/// Updated street.
final String? street;
/// Updated country.
final String? country;
/// Updated zip code.
final String? zipCode;
/// Updated cost center ID.
final String? costCenterId;
@override
List<Object?> get props => <Object?>[
id,
name,
address,
placeId,
latitude,
longitude,
city,
state,
street,
country,
zipCode,
costCenterId,
];
hubId,
name,
fullAddress,
placeId,
latitude,
longitude,
city,
state,
street,
country,
zipCode,
costCenterId,
];
}
/// Use case for updating an existing hub.
class UpdateHubUseCase implements UseCase<UpdateHubArguments, Hub> {
UpdateHubUseCase(this.repository);
///
/// Returns the updated hub ID.
class UpdateHubUseCase implements UseCase<UpdateHubArguments, String> {
/// Creates an [UpdateHubUseCase].
UpdateHubUseCase(this._repository);
final HubRepositoryInterface repository;
/// The repository for hub operations.
final HubRepositoryInterface _repository;
@override
Future<Hub> call(UpdateHubArguments params) {
return repository.updateHub(
id: params.id,
Future<String> call(UpdateHubArguments params) {
return _repository.updateHub(
hubId: params.hubId,
name: params.name,
address: params.address,
fullAddress: params.fullAddress,
placeId: params.placeId,
latitude: params.latitude,
longitude: params.longitude,

View File

@@ -2,20 +2,21 @@ import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_modular/flutter_modular.dart';
import 'package:krow_core/core.dart';
import 'package:krow_domain/krow_domain.dart';
import '../../domain/usecases/get_hubs_usecase.dart';
import 'client_hubs_event.dart';
import 'client_hubs_state.dart';
/// BLoC responsible for managing the state of the Client Hubs feature.
import 'package:client_hubs/src/domain/usecases/get_hubs_usecase.dart';
import 'package:client_hubs/src/presentation/blocs/client_hubs_event.dart';
import 'package:client_hubs/src/presentation/blocs/client_hubs_state.dart';
/// BLoC responsible for managing the state of the Client Hubs list.
///
/// It orchestrates the flow between the UI and the domain layer by invoking
/// specific use cases for fetching hubs.
/// Invokes [GetHubsUseCase] to fetch hubs from the V2 API.
class ClientHubsBloc extends Bloc<ClientHubsEvent, ClientHubsState>
with BlocErrorHandler<ClientHubsState>
implements Disposable {
/// Creates a [ClientHubsBloc].
ClientHubsBloc({required GetHubsUseCase getHubsUseCase})
: _getHubsUseCase = getHubsUseCase,
super(const ClientHubsState()) {
: _getHubsUseCase = getHubsUseCase,
super(const ClientHubsState()) {
on<ClientHubsFetched>(_onFetched);
on<ClientHubsMessageCleared>(_onMessageCleared);
}
@@ -49,8 +50,7 @@ class ClientHubsBloc extends Bloc<ClientHubsEvent, ClientHubsState>
state.copyWith(
clearErrorMessage: true,
clearSuccessMessage: true,
status:
state.status == ClientHubsStatus.success ||
status: state.status == ClientHubsStatus.success ||
state.status == ClientHubsStatus.failure
? ClientHubsStatus.success
: state.status,

View File

@@ -1,24 +1,27 @@
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:krow_core/core.dart';
import 'package:krow_domain/krow_domain.dart';
import '../../../domain/arguments/create_hub_arguments.dart';
import '../../../domain/usecases/create_hub_usecase.dart';
import '../../../domain/usecases/update_hub_usecase.dart';
import '../../../domain/usecases/get_cost_centers_usecase.dart';
import 'edit_hub_event.dart';
import 'edit_hub_state.dart';
import 'package:client_hubs/src/domain/arguments/create_hub_arguments.dart';
import 'package:client_hubs/src/domain/usecases/create_hub_usecase.dart';
import 'package:client_hubs/src/domain/usecases/get_cost_centers_usecase.dart';
import 'package:client_hubs/src/domain/usecases/update_hub_usecase.dart';
import 'package:client_hubs/src/presentation/blocs/edit_hub/edit_hub_event.dart';
import 'package:client_hubs/src/presentation/blocs/edit_hub/edit_hub_state.dart';
/// Bloc for creating and updating hubs.
class EditHubBloc extends Bloc<EditHubEvent, EditHubState>
with BlocErrorHandler<EditHubState> {
/// Creates an [EditHubBloc].
EditHubBloc({
required CreateHubUseCase createHubUseCase,
required UpdateHubUseCase updateHubUseCase,
required GetCostCentersUseCase getCostCentersUseCase,
}) : _createHubUseCase = createHubUseCase,
_updateHubUseCase = updateHubUseCase,
_getCostCentersUseCase = getCostCentersUseCase,
super(const EditHubState()) {
}) : _createHubUseCase = createHubUseCase,
_updateHubUseCase = updateHubUseCase,
_getCostCentersUseCase = getCostCentersUseCase,
super(const EditHubState()) {
on<EditHubCostCentersLoadRequested>(_onCostCentersLoadRequested);
on<EditHubAddRequested>(_onAddRequested);
on<EditHubUpdateRequested>(_onUpdateRequested);
@@ -35,7 +38,8 @@ class EditHubBloc extends Bloc<EditHubEvent, EditHubState>
await handleError(
emit: emit.call,
action: () async {
final List<CostCenter> costCenters = await _getCostCentersUseCase.call();
final List<CostCenter> costCenters =
await _getCostCentersUseCase.call();
emit(state.copyWith(costCenters: costCenters));
},
onError: (String errorKey) => state.copyWith(
@@ -57,7 +61,7 @@ class EditHubBloc extends Bloc<EditHubEvent, EditHubState>
await _createHubUseCase.call(
CreateHubArguments(
name: event.name,
address: event.address,
fullAddress: event.fullAddress,
placeId: event.placeId,
latitude: event.latitude,
longitude: event.longitude,
@@ -92,9 +96,9 @@ class EditHubBloc extends Bloc<EditHubEvent, EditHubState>
action: () async {
await _updateHubUseCase.call(
UpdateHubArguments(
id: event.id,
hubId: event.hubId,
name: event.name,
address: event.address,
fullAddress: event.fullAddress,
placeId: event.placeId,
latitude: event.latitude,
longitude: event.longitude,

View File

@@ -2,6 +2,7 @@ import 'package:equatable/equatable.dart';
/// Base class for all edit hub events.
abstract class EditHubEvent extends Equatable {
/// Creates an [EditHubEvent].
const EditHubEvent();
@override
@@ -10,14 +11,16 @@ abstract class EditHubEvent extends Equatable {
/// Event triggered to load all available cost centers.
class EditHubCostCentersLoadRequested extends EditHubEvent {
/// Creates an [EditHubCostCentersLoadRequested].
const EditHubCostCentersLoadRequested();
}
/// Event triggered to add a new hub.
class EditHubAddRequested extends EditHubEvent {
/// Creates an [EditHubAddRequested].
const EditHubAddRequested({
required this.name,
required this.address,
required this.fullAddress,
this.placeId,
this.latitude,
this.longitude,
@@ -29,40 +32,62 @@ class EditHubAddRequested extends EditHubEvent {
this.costCenterId,
});
/// Hub name.
final String name;
final String address;
/// Full street address.
final String fullAddress;
/// Google Place ID.
final String? placeId;
/// GPS latitude.
final double? latitude;
/// GPS longitude.
final double? longitude;
/// City.
final String? city;
/// State.
final String? state;
/// Street.
final String? street;
/// Country.
final String? country;
/// Zip code.
final String? zipCode;
/// Cost center ID.
final String? costCenterId;
@override
List<Object?> get props => <Object?>[
name,
address,
placeId,
latitude,
longitude,
city,
state,
street,
country,
zipCode,
costCenterId,
];
name,
fullAddress,
placeId,
latitude,
longitude,
city,
state,
street,
country,
zipCode,
costCenterId,
];
}
/// Event triggered to update an existing hub.
class EditHubUpdateRequested extends EditHubEvent {
/// Creates an [EditHubUpdateRequested].
const EditHubUpdateRequested({
required this.id,
required this.hubId,
required this.name,
required this.address,
required this.fullAddress,
this.placeId,
this.latitude,
this.longitude,
@@ -74,32 +99,55 @@ class EditHubUpdateRequested extends EditHubEvent {
this.costCenterId,
});
final String id;
/// Hub ID to update.
final String hubId;
/// Updated name.
final String name;
final String address;
/// Updated full address.
final String fullAddress;
/// Updated Google Place ID.
final String? placeId;
/// Updated latitude.
final double? latitude;
/// Updated longitude.
final double? longitude;
/// Updated city.
final String? city;
/// Updated state.
final String? state;
/// Updated street.
final String? street;
/// Updated country.
final String? country;
/// Updated zip code.
final String? zipCode;
/// Updated cost center ID.
final String? costCenterId;
@override
List<Object?> get props => <Object?>[
id,
name,
address,
placeId,
latitude,
longitude,
city,
state,
street,
country,
zipCode,
costCenterId,
];
hubId,
name,
fullAddress,
placeId,
latitude,
longitude,
city,
state,
street,
country,
zipCode,
costCenterId,
];
}

View File

@@ -1,21 +1,23 @@
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:krow_core/core.dart';
import '../../../domain/arguments/assign_nfc_tag_arguments.dart';
import '../../../domain/arguments/delete_hub_arguments.dart';
import '../../../domain/usecases/assign_nfc_tag_usecase.dart';
import '../../../domain/usecases/delete_hub_usecase.dart';
import 'hub_details_event.dart';
import 'hub_details_state.dart';
import 'package:client_hubs/src/domain/arguments/assign_nfc_tag_arguments.dart';
import 'package:client_hubs/src/domain/arguments/delete_hub_arguments.dart';
import 'package:client_hubs/src/domain/usecases/assign_nfc_tag_usecase.dart';
import 'package:client_hubs/src/domain/usecases/delete_hub_usecase.dart';
import 'package:client_hubs/src/presentation/blocs/hub_details/hub_details_event.dart';
import 'package:client_hubs/src/presentation/blocs/hub_details/hub_details_state.dart';
/// Bloc for managing hub details and operations like delete and NFC assignment.
class HubDetailsBloc extends Bloc<HubDetailsEvent, HubDetailsState>
with BlocErrorHandler<HubDetailsState> {
/// Creates a [HubDetailsBloc].
HubDetailsBloc({
required DeleteHubUseCase deleteHubUseCase,
required AssignNfcTagUseCase assignNfcTagUseCase,
}) : _deleteHubUseCase = deleteHubUseCase,
_assignNfcTagUseCase = assignNfcTagUseCase,
super(const HubDetailsState()) {
}) : _deleteHubUseCase = deleteHubUseCase,
_assignNfcTagUseCase = assignNfcTagUseCase,
super(const HubDetailsState()) {
on<HubDetailsDeleteRequested>(_onDeleteRequested);
on<HubDetailsNfcTagAssignRequested>(_onNfcTagAssignRequested);
}
@@ -32,7 +34,7 @@ class HubDetailsBloc extends Bloc<HubDetailsEvent, HubDetailsState>
await handleError(
emit: emit.call,
action: () async {
await _deleteHubUseCase.call(DeleteHubArguments(hubId: event.id));
await _deleteHubUseCase.call(DeleteHubArguments(hubId: event.hubId));
emit(
state.copyWith(
status: HubDetailsStatus.deleted,

View File

@@ -2,6 +2,7 @@ import 'package:equatable/equatable.dart';
/// Base class for all hub details events.
abstract class HubDetailsEvent extends Equatable {
/// Creates a [HubDetailsEvent].
const HubDetailsEvent();
@override
@@ -10,21 +11,28 @@ abstract class HubDetailsEvent extends Equatable {
/// Event triggered to delete a hub.
class HubDetailsDeleteRequested extends HubDetailsEvent {
const HubDetailsDeleteRequested(this.id);
final String id;
/// Creates a [HubDetailsDeleteRequested].
const HubDetailsDeleteRequested(this.hubId);
/// The ID of the hub to delete.
final String hubId;
@override
List<Object?> get props => <Object?>[id];
List<Object?> get props => <Object?>[hubId];
}
/// Event triggered to assign an NFC tag to a hub.
class HubDetailsNfcTagAssignRequested extends HubDetailsEvent {
/// Creates a [HubDetailsNfcTagAssignRequested].
const HubDetailsNfcTagAssignRequested({
required this.hubId,
required this.nfcTagId,
});
/// The hub ID.
final String hubId;
/// The NFC tag ID.
final String nfcTagId;
@override

View File

@@ -5,19 +5,18 @@ import 'package:flutter_modular/flutter_modular.dart';
import 'package:core_localization/core_localization.dart';
import 'package:krow_core/core.dart';
import 'package:krow_domain/krow_domain.dart';
import '../blocs/client_hubs_bloc.dart';
import '../blocs/client_hubs_event.dart';
import '../blocs/client_hubs_state.dart';
import '../widgets/hub_card.dart';
import '../widgets/hub_empty_state.dart';
import '../widgets/hub_info_card.dart';
import '../widgets/hubs_page_skeleton.dart';
import 'package:client_hubs/src/presentation/blocs/client_hubs_bloc.dart';
import 'package:client_hubs/src/presentation/blocs/client_hubs_event.dart';
import 'package:client_hubs/src/presentation/blocs/client_hubs_state.dart';
import 'package:client_hubs/src/presentation/widgets/hub_card.dart';
import 'package:client_hubs/src/presentation/widgets/hub_empty_state.dart';
import 'package:client_hubs/src/presentation/widgets/hub_info_card.dart';
import 'package:client_hubs/src/presentation/widgets/hubs_page_skeleton.dart';
/// The main page for the client hubs feature.
///
/// This page follows the KROW Clean Architecture by being a [StatelessWidget]
/// and delegating all state management to the [ClientHubsBloc].
/// Delegates all state management to [ClientHubsBloc].
class ClientHubsPage extends StatelessWidget {
/// Creates a [ClientHubsPage].
const ClientHubsPage({super.key});
@@ -99,7 +98,8 @@ class ClientHubsPage extends StatelessWidget {
else if (state.hubs.isEmpty)
HubEmptyState(
onAddPressed: () async {
final bool? success = await Modular.to.toEditHub();
final bool? success =
await Modular.to.toEditHub();
if (success == true && context.mounted) {
BlocProvider.of<ClientHubsBloc>(
context,
@@ -112,8 +112,8 @@ class ClientHubsPage extends StatelessWidget {
(Hub hub) => HubCard(
hub: hub,
onTap: () async {
final bool? success = await Modular.to
.toHubDetails(hub);
final bool? success =
await Modular.to.toHubDetails(hub);
if (success == true && context.mounted) {
BlocProvider.of<ClientHubsBloc>(
context,

View File

@@ -5,15 +5,17 @@ import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_modular/flutter_modular.dart';
import 'package:krow_domain/krow_domain.dart';
import '../blocs/edit_hub/edit_hub_bloc.dart';
import '../blocs/edit_hub/edit_hub_event.dart';
import '../blocs/edit_hub/edit_hub_state.dart';
import '../widgets/hub_form.dart';
import 'package:client_hubs/src/presentation/blocs/edit_hub/edit_hub_bloc.dart';
import 'package:client_hubs/src/presentation/blocs/edit_hub/edit_hub_event.dart';
import 'package:client_hubs/src/presentation/blocs/edit_hub/edit_hub_state.dart';
import 'package:client_hubs/src/presentation/widgets/hub_form.dart';
/// A wrapper page that shows the hub form in a modal-style layout.
class EditHubPage extends StatelessWidget {
/// Creates an [EditHubPage].
const EditHubPage({this.hub, super.key});
/// The hub to edit, or null for creating a new hub.
final Hub? hub;
@override
@@ -64,40 +66,39 @@ class EditHubPage extends StatelessWidget {
hub: hub,
costCenters: state.costCenters,
onCancel: () => Modular.to.pop(),
onSave:
({
required String name,
required String address,
String? costCenterId,
String? placeId,
double? latitude,
double? longitude,
}) {
if (hub == null) {
BlocProvider.of<EditHubBloc>(context).add(
EditHubAddRequested(
name: name,
address: address,
costCenterId: costCenterId,
placeId: placeId,
latitude: latitude,
longitude: longitude,
),
);
} else {
BlocProvider.of<EditHubBloc>(context).add(
EditHubUpdateRequested(
id: hub!.id,
name: name,
address: address,
costCenterId: costCenterId,
placeId: placeId,
latitude: latitude,
longitude: longitude,
),
);
}
},
onSave: ({
required String name,
required String fullAddress,
String? costCenterId,
String? placeId,
double? latitude,
double? longitude,
}) {
if (hub == null) {
BlocProvider.of<EditHubBloc>(context).add(
EditHubAddRequested(
name: name,
fullAddress: fullAddress,
costCenterId: costCenterId,
placeId: placeId,
latitude: latitude,
longitude: longitude,
),
);
} else {
BlocProvider.of<EditHubBloc>(context).add(
EditHubUpdateRequested(
hubId: hub!.hubId,
name: name,
fullAddress: fullAddress,
costCenterId: costCenterId,
placeId: placeId,
latitude: latitude,
longitude: longitude,
),
);
}
},
),
),

View File

@@ -6,18 +6,20 @@ import 'package:flutter_modular/flutter_modular.dart';
import 'package:krow_core/core.dart';
import 'package:krow_domain/krow_domain.dart';
import '../blocs/hub_details/hub_details_bloc.dart';
import '../blocs/hub_details/hub_details_event.dart';
import '../blocs/hub_details/hub_details_state.dart';
import '../widgets/hub_details/hub_details_bottom_actions.dart';
import '../widgets/hub_details/hub_details_item.dart';
import 'package:client_hubs/src/presentation/blocs/hub_details/hub_details_bloc.dart';
import 'package:client_hubs/src/presentation/blocs/hub_details/hub_details_event.dart';
import 'package:client_hubs/src/presentation/blocs/hub_details/hub_details_state.dart';
import 'package:client_hubs/src/presentation/widgets/hub_details/hub_details_bottom_actions.dart';
import 'package:client_hubs/src/presentation/widgets/hub_details/hub_details_item.dart';
/// A read-only details page for a single [Hub].
///
/// Shows hub name, address, and NFC tag assignment.
/// Shows hub name, address, NFC tag, and cost center.
class HubDetailsPage extends StatelessWidget {
/// Creates a [HubDetailsPage].
const HubDetailsPage({required this.hub, super.key});
/// The hub to display.
final Hub hub;
@override
@@ -30,7 +32,7 @@ class HubDetailsPage extends StatelessWidget {
final String message = state.successKey == 'deleted'
? t.client_hubs.hub_details.deleted_success
: (state.successMessage ??
t.client_hubs.hub_details.deleted_success);
t.client_hubs.hub_details.deleted_success);
UiSnackbar.show(
context,
message: message,
@@ -50,11 +52,12 @@ class HubDetailsPage extends StatelessWidget {
child: BlocBuilder<HubDetailsBloc, HubDetailsState>(
builder: (BuildContext context, HubDetailsState state) {
final bool isLoading = state.status == HubDetailsStatus.loading;
final String displayAddress = hub.fullAddress ?? '';
return Scaffold(
appBar: UiAppBar(
title: hub.name,
subtitle: hub.address,
subtitle: displayAddress,
showBackButton: true,
),
bottomNavigationBar: HubDetailsBottomActions(
@@ -75,25 +78,21 @@ class HubDetailsPage extends StatelessWidget {
children: <Widget>[
HubDetailsItem(
label: t.client_hubs.hub_details.nfc_label,
value:
hub.nfcTagId ??
value: hub.nfcTagId ??
t.client_hubs.hub_details.nfc_not_assigned,
icon: UiIcons.nfc,
isHighlight: hub.nfcTagId != null,
),
const SizedBox(height: UiConstants.space4),
HubDetailsItem(
label:
t.client_hubs.hub_details.cost_center_label,
value: hub.costCenter != null
? '${hub.costCenter!.name} (${hub.costCenter!.code})'
: t
.client_hubs
.hub_details
.cost_center_none,
icon: UiIcons
.bank, // Using bank icon for cost center
isHighlight: hub.costCenter != null,
label: t
.client_hubs.hub_details.cost_center_label,
value: hub.costCenterName != null
? hub.costCenterName!
: t.client_hubs.hub_details
.cost_center_none,
icon: UiIcons.bank,
isHighlight: hub.costCenterId != null,
),
],
),
@@ -143,7 +142,8 @@ class HubDetailsPage extends StatelessWidget {
);
if (confirm == true) {
Modular.get<HubDetailsBloc>().add(HubDetailsDeleteRequested(hub.id));
Modular.get<HubDetailsBloc>()
.add(HubDetailsDeleteRequested(hub.hubId));
}
}
}

View File

@@ -4,11 +4,12 @@ import 'package:flutter/material.dart';
import 'package:google_places_flutter/model/prediction.dart';
import 'package:krow_domain/krow_domain.dart';
import '../hub_address_autocomplete.dart';
import 'edit_hub_field_label.dart';
import 'package:client_hubs/src/presentation/widgets/hub_address_autocomplete.dart';
import 'package:client_hubs/src/presentation/widgets/edit_hub/edit_hub_field_label.dart';
/// The form section for adding or editing a hub.
class EditHubFormSection extends StatelessWidget {
/// Creates an [EditHubFormSection].
const EditHubFormSection({
required this.formKey,
required this.nameController,
@@ -16,24 +17,45 @@ class EditHubFormSection extends StatelessWidget {
required this.addressFocusNode,
required this.onAddressSelected,
required this.onSave,
required this.onCostCenterChanged,
this.costCenters = const <CostCenter>[],
this.selectedCostCenterId,
required this.onCostCenterChanged,
this.isSaving = false,
this.isEdit = false,
super.key,
});
/// Form key for validation.
final GlobalKey<FormState> formKey;
/// Controller for the name field.
final TextEditingController nameController;
/// Controller for the address field.
final TextEditingController addressController;
/// Focus node for the address field.
final FocusNode addressFocusNode;
/// Callback when an address prediction is selected.
final ValueChanged<Prediction> onAddressSelected;
/// Callback when the save button is pressed.
final VoidCallback onSave;
/// Available cost centers.
final List<CostCenter> costCenters;
/// Currently selected cost center ID.
final String? selectedCostCenterId;
/// Callback when the cost center selection changes.
final ValueChanged<String?> onCostCenterChanged;
/// Whether a save operation is in progress.
final bool isSaving;
/// Whether this is an edit (vs. create) operation.
final bool isEdit;
@override
@@ -43,7 +65,7 @@ class EditHubFormSection extends StatelessWidget {
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: <Widget>[
// ── Name field ──────────────────────────────────
// -- Name field --
EditHubFieldLabel(t.client_hubs.edit_hub.name_label),
TextFormField(
controller: nameController,
@@ -60,7 +82,7 @@ class EditHubFormSection extends StatelessWidget {
const SizedBox(height: UiConstants.space4),
// ── Address field ────────────────────────────────
// -- Address field --
EditHubFieldLabel(t.client_hubs.edit_hub.address_label),
HubAddressAutocomplete(
controller: addressController,
@@ -71,6 +93,7 @@ class EditHubFormSection extends StatelessWidget {
const SizedBox(height: UiConstants.space4),
// -- Cost Center --
EditHubFieldLabel(t.client_hubs.edit_hub.cost_center_label),
InkWell(
onTap: () => _showCostCenterSelector(context),
@@ -116,7 +139,7 @@ class EditHubFormSection extends StatelessWidget {
const SizedBox(height: UiConstants.space8),
// ── Save button ──────────────────────────────────
// -- Save button --
UiButton.primary(
onPressed: isSaving ? null : onSave,
text: isEdit
@@ -157,8 +180,9 @@ class EditHubFormSection extends StatelessWidget {
String _getCostCenterName(String id) {
try {
final CostCenter cc = costCenters.firstWhere((CostCenter item) => item.id == id);
return cc.code != null ? '${cc.name} (${cc.code})' : cc.name;
final CostCenter cc =
costCenters.firstWhere((CostCenter item) => item.costCenterId == id);
return cc.name;
} catch (_) {
return id;
}
@@ -181,24 +205,27 @@ class EditHubFormSection extends StatelessWidget {
width: double.maxFinite,
child: ConstrainedBox(
constraints: const BoxConstraints(maxHeight: 400),
child : costCenters.isEmpty
? Padding(
padding: const EdgeInsets.symmetric(horizontal: 24),
child: Text(t.client_hubs.edit_hub.cost_centers_empty),
)
child: costCenters.isEmpty
? Padding(
padding: const EdgeInsets.symmetric(horizontal: 24),
child: Text(t.client_hubs.edit_hub.cost_centers_empty),
)
: ListView.builder(
shrinkWrap: true,
itemCount: costCenters.length,
itemBuilder: (BuildContext context, int index) {
final CostCenter cc = costCenters[index];
return ListTile(
contentPadding: const EdgeInsets.symmetric(horizontal: 24),
title: Text(cc.name, style: UiTypography.body1m.textPrimary),
subtitle: cc.code != null ? Text(cc.code!, style: UiTypography.body2r.textSecondary) : null,
onTap: () => Navigator.of(context).pop(cc),
);
},
),
shrinkWrap: true,
itemCount: costCenters.length,
itemBuilder: (BuildContext context, int index) {
final CostCenter cc = costCenters[index];
return ListTile(
contentPadding:
const EdgeInsets.symmetric(horizontal: 24),
title: Text(
cc.name,
style: UiTypography.body1m.textPrimary,
),
onTap: () => Navigator.of(context).pop(cc),
);
},
),
),
),
);
@@ -206,7 +233,7 @@ class EditHubFormSection extends StatelessWidget {
);
if (selected != null) {
onCostCenterChanged(selected.id);
onCostCenterChanged(selected.costCenterId);
}
}
}

View File

@@ -4,7 +4,7 @@ import 'package:google_places_flutter/google_places_flutter.dart';
import 'package:google_places_flutter/model/prediction.dart';
import 'package:krow_core/core.dart';
import '../../util/hubs_constants.dart';
import 'package:client_hubs/src/util/hubs_constants.dart';
class HubAddressAutocomplete extends StatelessWidget {
const HubAddressAutocomplete({
@@ -26,12 +26,11 @@ class HubAddressAutocomplete extends StatelessWidget {
Widget build(BuildContext context) {
return GooglePlaceAutoCompleteTextField(
textEditingController: controller,
boxDecoration: null,
boxDecoration: const BoxDecoration(),
focusNode: focusNode,
inputDecoration: decoration ?? const InputDecoration(),
googleAPIKey: AppConfig.googleMapsApiKey,
debounceTime: 500,
//countries: HubsConstants.supportedCountries,
isLatLngRequired: true,
getPlaceDetailWithLatLng: (Prediction prediction) {
onSelected?.call(prediction);

View File

@@ -17,6 +17,7 @@ class HubCard extends StatelessWidget {
@override
Widget build(BuildContext context) {
final bool hasNfc = hub.nfcTagId != null;
final String displayAddress = hub.fullAddress ?? '';
return GestureDetector(
onTap: onTap,
@@ -50,7 +51,7 @@ class HubCard extends StatelessWidget {
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
Text(hub.name, style: UiTypography.body1b.textPrimary),
if (hub.address.isNotEmpty)
if (displayAddress.isNotEmpty)
Padding(
padding: const EdgeInsets.only(top: UiConstants.space1),
child: Row(
@@ -64,7 +65,7 @@ class HubCard extends StatelessWidget {
const SizedBox(width: UiConstants.space1),
Flexible(
child: Text(
hub.address,
displayAddress,
style: UiTypography.footnote1r.textSecondary,
maxLines: 1,
overflow: TextOverflow.ellipsis,

View File

@@ -29,7 +29,7 @@ class HubDetailsHeader extends StatelessWidget {
const SizedBox(width: UiConstants.space1),
Expanded(
child: Text(
hub.address,
hub.fullAddress ?? '',
style: UiTypography.body2r.textSecondary,
maxLines: 2,
overflow: TextOverflow.ellipsis,

View File

@@ -4,11 +4,12 @@ import 'package:core_localization/core_localization.dart';
import 'package:google_places_flutter/model/prediction.dart';
import 'package:krow_domain/krow_domain.dart';
import 'hub_address_autocomplete.dart';
import 'edit_hub/edit_hub_field_label.dart';
import 'package:client_hubs/src/presentation/widgets/hub_address_autocomplete.dart';
import 'package:client_hubs/src/presentation/widgets/edit_hub/edit_hub_field_label.dart';
/// A bottom sheet dialog for adding or editing a hub.
/// A form for adding or editing a hub.
class HubForm extends StatefulWidget {
/// Creates a [HubForm].
const HubForm({
required this.onSave,
required this.onCancel,
@@ -17,17 +18,23 @@ class HubForm extends StatefulWidget {
super.key,
});
/// The hub to edit, or null for creating a new hub.
final Hub? hub;
/// Available cost centers.
final List<CostCenter> costCenters;
/// Callback when the form is saved.
final void Function({
required String name,
required String address,
required String fullAddress,
String? costCenterId,
String? placeId,
double? latitude,
double? longitude,
})
onSave;
}) onSave;
/// Callback when the form is cancelled.
final VoidCallback onCancel;
@override
@@ -45,9 +52,10 @@ class _HubFormState extends State<HubForm> {
void initState() {
super.initState();
_nameController = TextEditingController(text: widget.hub?.name);
_addressController = TextEditingController(text: widget.hub?.address);
_addressController =
TextEditingController(text: widget.hub?.fullAddress ?? '');
_addressFocusNode = FocusNode();
_selectedCostCenterId = widget.hub?.costCenter?.id;
_selectedCostCenterId = widget.hub?.costCenterId;
}
@override
@@ -72,7 +80,7 @@ class _HubFormState extends State<HubForm> {
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: <Widget>[
// ── Hub Name ────────────────────────────────
// -- Hub Name --
EditHubFieldLabel(t.client_hubs.add_hub_dialog.name_label),
const SizedBox(height: UiConstants.space2),
TextFormField(
@@ -91,12 +99,13 @@ class _HubFormState extends State<HubForm> {
const SizedBox(height: UiConstants.space4),
// ── Cost Center ─────────────────────────────
// -- Cost Center --
EditHubFieldLabel(t.client_hubs.add_hub_dialog.cost_center_label),
const SizedBox(height: UiConstants.space2),
InkWell(
onTap: _showCostCenterSelector,
borderRadius: BorderRadius.circular(UiConstants.radiusBase * 1.5),
borderRadius:
BorderRadius.circular(UiConstants.radiusBase * 1.5),
child: Container(
padding: const EdgeInsets.symmetric(
horizontal: UiConstants.space4,
@@ -144,7 +153,7 @@ class _HubFormState extends State<HubForm> {
const SizedBox(height: UiConstants.space4),
// ── Address ─────────────────────────────────
// -- Address --
EditHubFieldLabel(t.client_hubs.add_hub_dialog.address_label),
const SizedBox(height: UiConstants.space2),
HubAddressAutocomplete(
@@ -161,7 +170,7 @@ class _HubFormState extends State<HubForm> {
const SizedBox(height: UiConstants.space8),
// ── Save Button ─────────────────────────────
// -- Save Button --
Row(
children: <Widget>[
Expanded(
@@ -180,7 +189,7 @@ class _HubFormState extends State<HubForm> {
widget.onSave(
name: _nameController.text.trim(),
address: _addressController.text.trim(),
fullAddress: _addressController.text.trim(),
costCenterId: _selectedCostCenterId,
placeId: _selectedPrediction?.placeId,
latitude: double.tryParse(
@@ -223,11 +232,13 @@ class _HubFormState extends State<HubForm> {
),
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(UiConstants.radiusBase * 1.5),
borderSide: BorderSide(color: UiColors.primary.withValues(alpha: 0.1)),
borderSide:
BorderSide(color: UiColors.primary.withValues(alpha: 0.1)),
),
enabledBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(UiConstants.radiusBase * 1.5),
borderSide: BorderSide(color: UiColors.primary.withValues(alpha: 0.1)),
borderSide:
BorderSide(color: UiColors.primary.withValues(alpha: 0.1)),
),
focusedBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(UiConstants.radiusBase * 1.5),
@@ -239,7 +250,9 @@ class _HubFormState extends State<HubForm> {
String _getCostCenterName(String id) {
try {
return widget.costCenters.firstWhere((CostCenter cc) => cc.id == id).name;
return widget.costCenters
.firstWhere((CostCenter cc) => cc.costCenterId == id)
.name;
} catch (_) {
return id;
}
@@ -282,12 +295,6 @@ class _HubFormState extends State<HubForm> {
cc.name,
style: UiTypography.body1m.textPrimary,
),
subtitle: cc.code != null
? Text(
cc.code!,
style: UiTypography.body2r.textSecondary,
)
: null,
onTap: () => Navigator.of(context).pop(cc),
);
},
@@ -300,7 +307,7 @@ class _HubFormState extends State<HubForm> {
if (selected != null) {
setState(() {
_selectedCostCenterId = selected.id;
_selectedCostCenterId = selected.costCenterId;
});
}
}

View File

@@ -1,3 +1,5 @@
/// Constants used by the hubs feature.
class HubsConstants {
/// Supported country codes for address autocomplete.
static const List<String> supportedCountries = <String>['us'];
}