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:
@@ -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,
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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,
|
||||
];
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
];
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
),
|
||||
);
|
||||
}
|
||||
},
|
||||
),
|
||||
),
|
||||
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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'];
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user