From 1f795414042e5a34d8840b43836a88981cdd4cf0 Mon Sep 17 00:00:00 2001 From: Achintha Isuru Date: Tue, 10 Mar 2026 11:46:11 -0400 Subject: [PATCH] feat: Implement UI for adding new shift manager during order creation --- .../hubs_connector_repository_impl.dart | 4 +- .../lib/src/create_order_module.dart | 16 ++- .../client_order_query_repository_impl.dart | 107 ++++++++++++++++++ .../lib/src/domain/models/order_hub.dart | 72 ++++++++++++ .../lib/src/domain/models/order_manager.dart | 20 ++++ .../lib/src/domain/models/order_role.dart | 28 +++++ ...ient_order_query_repository_interface.dart | 39 +++++++ .../one_time_order/one_time_order_bloc.dart | 105 ++++++----------- .../permanent_order/permanent_order_bloc.dart | 86 +++++--------- .../recurring_order/recurring_order_bloc.dart | 98 +++++++--------- .../widgets/hub_manager_selector.dart | 3 +- 11 files changed, 393 insertions(+), 185 deletions(-) create mode 100644 apps/mobile/packages/features/client/orders/create_order/lib/src/data/repositories_impl/client_order_query_repository_impl.dart create mode 100644 apps/mobile/packages/features/client/orders/create_order/lib/src/domain/models/order_hub.dart create mode 100644 apps/mobile/packages/features/client/orders/create_order/lib/src/domain/models/order_manager.dart create mode 100644 apps/mobile/packages/features/client/orders/create_order/lib/src/domain/models/order_role.dart create mode 100644 apps/mobile/packages/features/client/orders/create_order/lib/src/domain/repositories/client_order_query_repository_interface.dart diff --git a/apps/mobile/packages/data_connect/lib/src/connectors/hubs/data/repositories/hubs_connector_repository_impl.dart b/apps/mobile/packages/data_connect/lib/src/connectors/hubs/data/repositories/hubs_connector_repository_impl.dart index c046918c..c48ac0a4 100644 --- a/apps/mobile/packages/data_connect/lib/src/connectors/hubs/data/repositories/hubs_connector_repository_impl.dart +++ b/apps/mobile/packages/data_connect/lib/src/connectors/hubs/data/repositories/hubs_connector_repository_impl.dart @@ -1,10 +1,12 @@ // 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 'dart:convert'; -import 'package:firebase_data_connect/src/core/ref.dart'; + +import 'package:firebase_data_connect/firebase_data_connect.dart'; import 'package:http/http.dart' as http; import 'package:krow_core/core.dart'; import 'package:krow_data_connect/krow_data_connect.dart' as dc; import 'package:krow_domain/krow_domain.dart'; + import '../../domain/repositories/hubs_connector_repository.dart'; /// Implementation of [HubsConnectorRepository]. diff --git a/apps/mobile/packages/features/client/orders/create_order/lib/src/create_order_module.dart b/apps/mobile/packages/features/client/orders/create_order/lib/src/create_order_module.dart index 84a33c9a..b5491474 100644 --- a/apps/mobile/packages/features/client/orders/create_order/lib/src/create_order_module.dart +++ b/apps/mobile/packages/features/client/orders/create_order/lib/src/create_order_module.dart @@ -4,7 +4,9 @@ import 'package:krow_core/core.dart'; import 'package:krow_data_connect/krow_data_connect.dart'; import 'package:krow_data_connect/krow_data_connect.dart' as dc; import 'data/repositories_impl/client_create_order_repository_impl.dart'; +import 'data/repositories_impl/client_order_query_repository_impl.dart'; import 'domain/repositories/client_create_order_repository_interface.dart'; +import 'domain/repositories/client_order_query_repository_interface.dart'; import 'domain/usecases/create_one_time_order_usecase.dart'; import 'domain/usecases/create_permanent_order_usecase.dart'; import 'domain/usecases/create_recurring_order_usecase.dart'; @@ -40,6 +42,12 @@ class ClientCreateOrderModule extends Module { ), ); + i.addLazySingleton( + () => ClientOrderQueryRepositoryImpl( + service: i.get(), + ), + ); + // UseCases i.addLazySingleton(CreateOneTimeOrderUseCase.new); i.addLazySingleton(CreatePermanentOrderUseCase.new); @@ -58,7 +66,13 @@ class ClientCreateOrderModule extends Module { ), ); i.add(OneTimeOrderBloc.new); - i.add(PermanentOrderBloc.new); + i.add( + () => PermanentOrderBloc( + i.get(), + i.get(), + i.get(), + ), + ); i.add(RecurringOrderBloc.new); } diff --git a/apps/mobile/packages/features/client/orders/create_order/lib/src/data/repositories_impl/client_order_query_repository_impl.dart b/apps/mobile/packages/features/client/orders/create_order/lib/src/data/repositories_impl/client_order_query_repository_impl.dart new file mode 100644 index 00000000..723b536e --- /dev/null +++ b/apps/mobile/packages/features/client/orders/create_order/lib/src/data/repositories_impl/client_order_query_repository_impl.dart @@ -0,0 +1,107 @@ +import 'package:krow_data_connect/krow_data_connect.dart' as dc; +import 'package:krow_domain/krow_domain.dart'; + +import '../../domain/models/order_hub.dart'; +import '../../domain/models/order_manager.dart'; +import '../../domain/models/order_role.dart'; +import '../../domain/repositories/client_order_query_repository_interface.dart'; + +/// Data layer implementation of [ClientOrderQueryRepositoryInterface]. +/// +/// Delegates all backend calls to [dc.DataConnectService] using the +/// `_service.run()` pattern for automatic auth validation, token refresh, +/// and retry logic. Each method maps Data Connect response types to the +/// corresponding clean domain models. +class ClientOrderQueryRepositoryImpl + implements ClientOrderQueryRepositoryInterface { + /// Creates an instance backed by the given [service]. + ClientOrderQueryRepositoryImpl({required dc.DataConnectService service}) + : _service = service; + + final dc.DataConnectService _service; + + @override + Future> getVendors() async { + return _service.run(() async { + final result = await _service.connector.listVendors().execute(); + return result.data.vendors + .map( + (dc.ListVendorsVendors vendor) => Vendor( + id: vendor.id, + name: vendor.companyName, + rates: const {}, + ), + ) + .toList(); + }); + } + + @override + Future> getRolesByVendor(String vendorId) async { + return _service.run(() async { + final result = await _service.connector + .listRolesByVendorId(vendorId: vendorId) + .execute(); + return result.data.roles + .map( + (dc.ListRolesByVendorIdRoles role) => OrderRole( + id: role.id, + name: role.name, + costPerHour: role.costPerHour, + ), + ) + .toList(); + }); + } + + @override + Future> getHubsByOwner(String ownerId) async { + return _service.run(() async { + final result = await _service.connector + .listTeamHubsByOwnerId(ownerId: ownerId) + .execute(); + return result.data.teamHubs + .map( + (dc.ListTeamHubsByOwnerIdTeamHubs hub) => OrderHub( + id: hub.id, + name: hub.hubName, + address: hub.address, + placeId: hub.placeId, + latitude: hub.latitude, + longitude: hub.longitude, + city: hub.city, + state: hub.state, + street: hub.street, + country: hub.country, + zipCode: hub.zipCode, + ), + ) + .toList(); + }); + } + + @override + Future> getManagersByHub(String hubId) async { + return _service.run(() async { + final result = await _service.connector.listTeamMembers().execute(); + return result.data.teamMembers + .where( + (dc.ListTeamMembersTeamMembers member) => + member.teamHubId == hubId && + member.role is dc.Known && + (member.role as dc.Known).value == + dc.TeamMemberRole.MANAGER, + ) + .map( + (dc.ListTeamMembersTeamMembers member) => OrderManager( + id: member.id, + name: member.user.fullName ?? 'Unknown', + ), + ) + .toList(); + }); + } + + @override + Future getBusinessId() => _service.getBusinessId(); +} diff --git a/apps/mobile/packages/features/client/orders/create_order/lib/src/domain/models/order_hub.dart b/apps/mobile/packages/features/client/orders/create_order/lib/src/domain/models/order_hub.dart new file mode 100644 index 00000000..b0526c93 --- /dev/null +++ b/apps/mobile/packages/features/client/orders/create_order/lib/src/domain/models/order_hub.dart @@ -0,0 +1,72 @@ +import 'package:equatable/equatable.dart'; + +/// A team hub (location) available for order assignment. +/// +/// This domain model represents a physical hub location owned by the business. +/// It is used to populate hub selection dropdowns and to attach location +/// details when creating shifts for an order. +class OrderHub extends Equatable { + /// Creates an [OrderHub] with the required [id], [name], and [address], + /// plus optional geo-location and address component fields. + const OrderHub({ + required this.id, + required this.name, + required this.address, + this.placeId, + this.latitude, + this.longitude, + this.city, + this.state, + this.street, + this.country, + this.zipCode, + }); + + /// Unique identifier of the hub. + final String id; + + /// Human-readable display name of the hub. + final String name; + + /// Full street address of the hub. + final String address; + + /// Google Places ID, if available. + final String? placeId; + + /// Geographic latitude of the hub. + final double? latitude; + + /// Geographic longitude of the hub. + final double? longitude; + + /// City where the hub is located. + final String? city; + + /// State or province where the hub is located. + final String? state; + + /// Street name portion of the address. + final String? street; + + /// Country where the hub is located. + final String? country; + + /// Postal / ZIP code of the hub. + final String? zipCode; + + @override + List get props => [ + id, + name, + address, + placeId, + latitude, + longitude, + city, + state, + street, + country, + zipCode, + ]; +} diff --git a/apps/mobile/packages/features/client/orders/create_order/lib/src/domain/models/order_manager.dart b/apps/mobile/packages/features/client/orders/create_order/lib/src/domain/models/order_manager.dart new file mode 100644 index 00000000..8097fae1 --- /dev/null +++ b/apps/mobile/packages/features/client/orders/create_order/lib/src/domain/models/order_manager.dart @@ -0,0 +1,20 @@ +import 'package:equatable/equatable.dart'; + +/// A hub manager available for assignment to an order. +/// +/// This domain model represents a team member with a MANAGER role at a +/// specific hub. It is used to populate the manager selection dropdown +/// when creating or editing an order. +class OrderManager extends Equatable { + /// Creates an [OrderManager] with the given [id] and [name]. + const OrderManager({required this.id, required this.name}); + + /// Unique identifier of the manager (team member ID). + final String id; + + /// Full display name of the manager. + final String name; + + @override + List get props => [id, name]; +} diff --git a/apps/mobile/packages/features/client/orders/create_order/lib/src/domain/models/order_role.dart b/apps/mobile/packages/features/client/orders/create_order/lib/src/domain/models/order_role.dart new file mode 100644 index 00000000..fec66427 --- /dev/null +++ b/apps/mobile/packages/features/client/orders/create_order/lib/src/domain/models/order_role.dart @@ -0,0 +1,28 @@ +import 'package:equatable/equatable.dart'; + +/// A role available for staffing positions within an order. +/// +/// This domain model represents a staffing role fetched from the backend, +/// decoupled from any data layer dependencies. It carries the role identity +/// and its hourly cost so the presentation layer can populate dropdowns +/// and calculate estimates. +class OrderRole extends Equatable { + /// Creates an [OrderRole] with the given [id], [name], and [costPerHour]. + const OrderRole({ + required this.id, + required this.name, + required this.costPerHour, + }); + + /// Unique identifier of the role. + final String id; + + /// Human-readable display name of the role. + final String name; + + /// Hourly cost rate for this role. + final double costPerHour; + + @override + List get props => [id, name, costPerHour]; +} diff --git a/apps/mobile/packages/features/client/orders/create_order/lib/src/domain/repositories/client_order_query_repository_interface.dart b/apps/mobile/packages/features/client/orders/create_order/lib/src/domain/repositories/client_order_query_repository_interface.dart new file mode 100644 index 00000000..1ab9a2c7 --- /dev/null +++ b/apps/mobile/packages/features/client/orders/create_order/lib/src/domain/repositories/client_order_query_repository_interface.dart @@ -0,0 +1,39 @@ +import 'package:krow_domain/krow_domain.dart'; + +import '../models/order_hub.dart'; +import '../models/order_manager.dart'; +import '../models/order_role.dart'; + +/// Interface for querying order-related reference data. +/// +/// This repository centralises the read-only queries that the order creation +/// BLoCs need (vendors, roles, hubs, managers) so that they no longer depend +/// directly on [DataConnectService] or the `krow_data_connect` package. +/// +/// Implementations live in the data layer and translate backend responses +/// into clean domain models. +abstract interface class ClientOrderQueryRepositoryInterface { + /// Returns the list of available vendors. + /// + /// The returned [Vendor] objects come from the shared `krow_domain` package + /// because `Vendor` is already a clean domain entity. + Future> getVendors(); + + /// Returns the roles offered by the vendor identified by [vendorId]. + Future> getRolesByVendor(String vendorId); + + /// Returns the team hubs owned by the business identified by [ownerId]. + Future> getHubsByOwner(String ownerId); + + /// Returns the managers assigned to the hub identified by [hubId]. + /// + /// Only team members with the MANAGER role at the given hub are included. + Future> getManagersByHub(String hubId); + + /// Returns the current business ID from the active client session. + /// + /// This allows BLoCs to resolve the business ID without depending on + /// the data layer's session store directly, keeping the presentation + /// layer free from `krow_data_connect` imports. + Future getBusinessId(); +} diff --git a/apps/mobile/packages/features/client/orders/create_order/lib/src/presentation/blocs/one_time_order/one_time_order_bloc.dart b/apps/mobile/packages/features/client/orders/create_order/lib/src/presentation/blocs/one_time_order/one_time_order_bloc.dart index 7c3a4435..8ebfb27c 100644 --- a/apps/mobile/packages/features/client/orders/create_order/lib/src/presentation/blocs/one_time_order/one_time_order_bloc.dart +++ b/apps/mobile/packages/features/client/orders/create_order/lib/src/presentation/blocs/one_time_order/one_time_order_bloc.dart @@ -1,10 +1,12 @@ import 'package:client_create_order/src/domain/arguments/one_time_order_arguments.dart'; +import 'package:client_create_order/src/domain/models/order_hub.dart'; +import 'package:client_create_order/src/domain/models/order_manager.dart'; +import 'package:client_create_order/src/domain/models/order_role.dart'; +import 'package:client_create_order/src/domain/repositories/client_order_query_repository_interface.dart'; import 'package:client_create_order/src/domain/usecases/create_one_time_order_usecase.dart'; import 'package:client_create_order/src/domain/usecases/get_order_details_for_reorder_usecase.dart'; -import 'package:firebase_data_connect/firebase_data_connect.dart' as fdc; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:krow_core/core.dart'; -import 'package:krow_data_connect/krow_data_connect.dart' as dc; import 'package:krow_domain/krow_domain.dart'; import 'one_time_order_event.dart'; @@ -18,7 +20,7 @@ class OneTimeOrderBloc extends Bloc OneTimeOrderBloc( this._createOneTimeOrderUseCase, this._getOrderDetailsForReorderUseCase, - this._service, + this._queryRepository, ) : super(OneTimeOrderState.initial()) { on(_onVendorsLoaded); on(_onVendorChanged); @@ -39,25 +41,11 @@ class OneTimeOrderBloc extends Bloc } final CreateOneTimeOrderUseCase _createOneTimeOrderUseCase; final GetOrderDetailsForReorderUseCase _getOrderDetailsForReorderUseCase; - final dc.DataConnectService _service; + final ClientOrderQueryRepositoryInterface _queryRepository; Future _loadVendors() async { final List? vendors = await handleErrorWithResult( - action: () async { - final fdc.QueryResult result = await _service - .connector - .listVendors() - .execute(); - return result.data.vendors - .map( - (dc.ListVendorsVendors vendor) => Vendor( - id: vendor.id, - name: vendor.companyName, - rates: const {}, - ), - ) - .toList(); - }, + action: () => _queryRepository.getVendors(), onError: (_) => add(const OneTimeOrderVendorsLoaded([])), ); @@ -72,19 +60,14 @@ class OneTimeOrderBloc extends Bloc ) async { final List? roles = await handleErrorWithResult( action: () async { - final fdc.QueryResult< - dc.ListRolesByVendorIdData, - dc.ListRolesByVendorIdVariables - > - result = await _service.connector - .listRolesByVendorId(vendorId: vendorId) - .execute(); - return result.data.roles + final List result = + await _queryRepository.getRolesByVendor(vendorId); + return result .map( - (dc.ListRolesByVendorIdRoles role) => OneTimeOrderRoleOption( - id: role.id, - name: role.name, - costPerHour: role.costPerHour, + (OrderRole r) => OneTimeOrderRoleOption( + id: r.id, + name: r.name, + costPerHour: r.costPerHour, ), ) .toList(); @@ -101,28 +84,23 @@ class OneTimeOrderBloc extends Bloc Future _loadHubs() async { final List? hubs = await handleErrorWithResult( action: () async { - final String businessId = await _service.getBusinessId(); - final fdc.QueryResult< - dc.ListTeamHubsByOwnerIdData, - dc.ListTeamHubsByOwnerIdVariables - > - result = await _service.connector - .listTeamHubsByOwnerId(ownerId: businessId) - .execute(); - return result.data.teamHubs + final String businessId = await _queryRepository.getBusinessId(); + final List result = + await _queryRepository.getHubsByOwner(businessId); + return result .map( - (dc.ListTeamHubsByOwnerIdTeamHubs hub) => OneTimeOrderHubOption( - id: hub.id, - name: hub.hubName, - address: hub.address, - placeId: hub.placeId, - latitude: hub.latitude, - longitude: hub.longitude, - city: hub.city, - state: hub.state, - street: hub.street, - country: hub.country, - zipCode: hub.zipCode, + (OrderHub h) => OneTimeOrderHubOption( + id: h.id, + name: h.name, + address: h.address, + placeId: h.placeId, + latitude: h.latitude, + longitude: h.longitude, + city: h.city, + state: h.state, + street: h.street, + country: h.country, + zipCode: h.zipCode, ), ) .toList(); @@ -140,23 +118,14 @@ class OneTimeOrderBloc extends Bloc final List? managers = await handleErrorWithResult( action: () async { - final fdc.QueryResult result = - await _service.connector.listTeamMembers().execute(); - - return result.data.teamMembers - .where( - (dc.ListTeamMembersTeamMembers member) => - member.teamHubId == hubId && - member.role is dc.Known && - (member.role as dc.Known).value == - dc.TeamMemberRole.MANAGER, - ) + final List result = + await _queryRepository.getManagersByHub(hubId); + return result .map( - (dc.ListTeamMembersTeamMembers member) => - OneTimeOrderManagerOption( - id: member.id, - name: member.user.fullName ?? 'Unknown', - ), + (OrderManager m) => OneTimeOrderManagerOption( + id: m.id, + name: m.name, + ), ) .toList(); }, diff --git a/apps/mobile/packages/features/client/orders/create_order/lib/src/presentation/blocs/permanent_order/permanent_order_bloc.dart b/apps/mobile/packages/features/client/orders/create_order/lib/src/presentation/blocs/permanent_order/permanent_order_bloc.dart index 4862958d..1f43713a 100644 --- a/apps/mobile/packages/features/client/orders/create_order/lib/src/presentation/blocs/permanent_order/permanent_order_bloc.dart +++ b/apps/mobile/packages/features/client/orders/create_order/lib/src/presentation/blocs/permanent_order/permanent_order_bloc.dart @@ -1,9 +1,11 @@ +import 'package:client_create_order/src/domain/models/order_hub.dart'; +import 'package:client_create_order/src/domain/models/order_manager.dart'; +import 'package:client_create_order/src/domain/models/order_role.dart'; +import 'package:client_create_order/src/domain/repositories/client_order_query_repository_interface.dart'; import 'package:client_create_order/src/domain/usecases/create_permanent_order_usecase.dart'; import 'package:client_create_order/src/domain/usecases/get_order_details_for_reorder_usecase.dart'; -import 'package:firebase_data_connect/firebase_data_connect.dart' as fdc; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:krow_core/core.dart'; -import 'package:krow_data_connect/krow_data_connect.dart' as dc; import 'package:krow_domain/krow_domain.dart' as domain; import 'permanent_order_event.dart'; @@ -17,7 +19,7 @@ class PermanentOrderBloc extends Bloc PermanentOrderBloc( this._createPermanentOrderUseCase, this._getOrderDetailsForReorderUseCase, - this._service, + this._queryRepository, ) : super(PermanentOrderState.initial()) { on(_onVendorsLoaded); on(_onVendorChanged); @@ -40,7 +42,7 @@ class PermanentOrderBloc extends Bloc final CreatePermanentOrderUseCase _createPermanentOrderUseCase; final GetOrderDetailsForReorderUseCase _getOrderDetailsForReorderUseCase; - final dc.DataConnectService _service; + final ClientOrderQueryRepositoryInterface _queryRepository; static const List _dayLabels = [ 'SUN', @@ -54,21 +56,7 @@ class PermanentOrderBloc extends Bloc Future _loadVendors() async { final List? vendors = await handleErrorWithResult( - action: () async { - final fdc.QueryResult result = await _service - .connector - .listVendors() - .execute(); - return result.data.vendors - .map( - (dc.ListVendorsVendors vendor) => domain.Vendor( - id: vendor.id, - name: vendor.companyName, - rates: const {}, - ), - ) - .toList(); - }, + action: () => _queryRepository.getVendors(), onError: (_) => add(const PermanentOrderVendorsLoaded([])), ); @@ -83,19 +71,14 @@ class PermanentOrderBloc extends Bloc ) async { final List? roles = await handleErrorWithResult( action: () async { - final fdc.QueryResult< - dc.ListRolesByVendorIdData, - dc.ListRolesByVendorIdVariables - > - result = await _service.connector - .listRolesByVendorId(vendorId: vendorId) - .execute(); - return result.data.roles + final List orderRoles = + await _queryRepository.getRolesByVendor(vendorId); + return orderRoles .map( - (dc.ListRolesByVendorIdRoles role) => PermanentOrderRoleOption( - id: role.id, - name: role.name, - costPerHour: role.costPerHour, + (OrderRole r) => PermanentOrderRoleOption( + id: r.id, + name: r.name, + costPerHour: r.costPerHour, ), ) .toList(); @@ -112,19 +95,17 @@ class PermanentOrderBloc extends Bloc Future _loadHubs() async { final List? hubs = await handleErrorWithResult( action: () async { - final String businessId = await _service.getBusinessId(); - final fdc.QueryResult< - dc.ListTeamHubsByOwnerIdData, - dc.ListTeamHubsByOwnerIdVariables - > - result = await _service.connector - .listTeamHubsByOwnerId(ownerId: businessId) - .execute(); - return result.data.teamHubs + final String? businessId = await _queryRepository.getBusinessId(); + if (businessId == null || businessId.isEmpty) { + return []; + } + final List orderHubs = + await _queryRepository.getHubsByOwner(businessId); + return orderHubs .map( - (dc.ListTeamHubsByOwnerIdTeamHubs hub) => PermanentOrderHubOption( + (OrderHub hub) => PermanentOrderHubOption( id: hub.id, - name: hub.hubName, + name: hub.name, address: hub.address, placeId: hub.placeId, latitude: hub.latitude, @@ -219,22 +200,13 @@ class PermanentOrderBloc extends Bloc final List? managers = await handleErrorWithResult( action: () async { - final fdc.QueryResult result = - await _service.connector.listTeamMembers().execute(); - - return result.data.teamMembers - .where( - (dc.ListTeamMembersTeamMembers member) => - member.teamHubId == hubId && - member.role is dc.Known && - (member.role as dc.Known).value == - dc.TeamMemberRole.MANAGER, - ) + final List orderManagers = + await _queryRepository.getManagersByHub(hubId); + return orderManagers .map( - (dc.ListTeamMembersTeamMembers member) => - PermanentOrderManagerOption( - id: member.id, - name: member.user.fullName ?? 'Unknown', + (OrderManager m) => PermanentOrderManagerOption( + id: m.id, + name: m.name, ), ) .toList(); diff --git a/apps/mobile/packages/features/client/orders/create_order/lib/src/presentation/blocs/recurring_order/recurring_order_bloc.dart b/apps/mobile/packages/features/client/orders/create_order/lib/src/presentation/blocs/recurring_order/recurring_order_bloc.dart index 2c51fef9..37e4f5cf 100644 --- a/apps/mobile/packages/features/client/orders/create_order/lib/src/presentation/blocs/recurring_order/recurring_order_bloc.dart +++ b/apps/mobile/packages/features/client/orders/create_order/lib/src/presentation/blocs/recurring_order/recurring_order_bloc.dart @@ -1,23 +1,32 @@ +import 'package:client_create_order/src/domain/models/order_hub.dart'; +import 'package:client_create_order/src/domain/models/order_manager.dart'; +import 'package:client_create_order/src/domain/models/order_role.dart'; +import 'package:client_create_order/src/domain/repositories/client_order_query_repository_interface.dart'; import 'package:client_create_order/src/domain/usecases/create_recurring_order_usecase.dart'; import 'package:client_create_order/src/domain/usecases/get_order_details_for_reorder_usecase.dart'; -import 'package:firebase_data_connect/firebase_data_connect.dart' as fdc; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:krow_core/core.dart'; -import 'package:krow_data_connect/krow_data_connect.dart' as dc; import 'package:krow_domain/krow_domain.dart' as domain; import 'recurring_order_event.dart'; import 'recurring_order_state.dart'; /// BLoC for managing the recurring order creation form. +/// +/// This BLoC delegates all backend queries to +/// [ClientOrderQueryRepositoryInterface] and order submission to +/// [CreateRecurringOrderUseCase], keeping the presentation layer free +/// from direct `krow_data_connect` imports. class RecurringOrderBloc extends Bloc with BlocErrorHandler, SafeBloc { + /// Creates a [RecurringOrderBloc] with the required use cases and + /// query repository. RecurringOrderBloc( this._createRecurringOrderUseCase, this._getOrderDetailsForReorderUseCase, - this._service, + this._queryRepository, ) : super(RecurringOrderState.initial()) { on(_onVendorsLoaded); on(_onVendorChanged); @@ -41,7 +50,7 @@ class RecurringOrderBloc extends Bloc final CreateRecurringOrderUseCase _createRecurringOrderUseCase; final GetOrderDetailsForReorderUseCase _getOrderDetailsForReorderUseCase; - final dc.DataConnectService _service; + final ClientOrderQueryRepositoryInterface _queryRepository; static const List _dayLabels = [ 'SUN', @@ -53,24 +62,14 @@ class RecurringOrderBloc extends Bloc 'SAT', ]; + /// Loads the list of available vendors from the query repository. Future _loadVendors() async { final List? vendors = await handleErrorWithResult( action: () async { - final fdc.QueryResult result = await _service - .connector - .listVendors() - .execute(); - return result.data.vendors - .map( - (dc.ListVendorsVendors vendor) => domain.Vendor( - id: vendor.id, - name: vendor.companyName, - rates: const {}, - ), - ) - .toList(); + return _queryRepository.getVendors(); }, - onError: (_) => add(const RecurringOrderVendorsLoaded([])), + onError: (_) => + add(const RecurringOrderVendorsLoaded([])), ); if (vendors != null) { @@ -78,25 +77,22 @@ class RecurringOrderBloc extends Bloc } } + /// Loads roles for the given [vendorId] and maps them to presentation + /// option models. Future _loadRolesForVendor( String vendorId, Emitter emit, ) async { final List? roles = await handleErrorWithResult( action: () async { - final fdc.QueryResult< - dc.ListRolesByVendorIdData, - dc.ListRolesByVendorIdVariables - > - result = await _service.connector - .listRolesByVendorId(vendorId: vendorId) - .execute(); - return result.data.roles + final List orderRoles = + await _queryRepository.getRolesByVendor(vendorId); + return orderRoles .map( - (dc.ListRolesByVendorIdRoles role) => RecurringOrderRoleOption( - id: role.id, - name: role.name, - costPerHour: role.costPerHour, + (OrderRole r) => RecurringOrderRoleOption( + id: r.id, + name: r.name, + costPerHour: r.costPerHour, ), ) .toList(); @@ -110,22 +106,19 @@ class RecurringOrderBloc extends Bloc } } + /// Loads team hubs for the current business owner and maps them to + /// presentation option models. Future _loadHubs() async { final List? hubs = await handleErrorWithResult( action: () async { - final String businessId = await _service.getBusinessId(); - final fdc.QueryResult< - dc.ListTeamHubsByOwnerIdData, - dc.ListTeamHubsByOwnerIdVariables - > - result = await _service.connector - .listTeamHubsByOwnerId(ownerId: businessId) - .execute(); - return result.data.teamHubs + final String businessId = await _queryRepository.getBusinessId(); + final List orderHubs = + await _queryRepository.getHubsByOwner(businessId); + return orderHubs .map( - (dc.ListTeamHubsByOwnerIdTeamHubs hub) => RecurringOrderHubOption( + (OrderHub hub) => RecurringOrderHubOption( id: hub.id, - name: hub.hubName, + name: hub.name, address: hub.address, placeId: hub.placeId, latitude: hub.latitude, @@ -213,6 +206,8 @@ class RecurringOrderBloc extends Bloc emit(state.copyWith(managers: event.managers)); } + /// Loads managers for the given [hubId] and maps them to presentation + /// option models. Future _loadManagersForHub( String hubId, Emitter emit, @@ -220,22 +215,13 @@ class RecurringOrderBloc extends Bloc final List? managers = await handleErrorWithResult( action: () async { - final fdc.QueryResult result = - await _service.connector.listTeamMembers().execute(); - - return result.data.teamMembers - .where( - (dc.ListTeamMembersTeamMembers member) => - member.teamHubId == hubId && - member.role is dc.Known && - (member.role as dc.Known).value == - dc.TeamMemberRole.MANAGER, - ) + final List orderManagers = + await _queryRepository.getManagersByHub(hubId); + return orderManagers .map( - (dc.ListTeamMembersTeamMembers member) => - RecurringOrderManagerOption( - id: member.id, - name: member.user.fullName ?? 'Unknown', + (OrderManager m) => RecurringOrderManagerOption( + id: m.id, + name: m.name, ), ) .toList(); diff --git a/apps/mobile/packages/features/client/orders/orders_common/lib/src/presentation/widgets/hub_manager_selector.dart b/apps/mobile/packages/features/client/orders/orders_common/lib/src/presentation/widgets/hub_manager_selector.dart index 185b9bef..f6d05571 100644 --- a/apps/mobile/packages/features/client/orders/orders_common/lib/src/presentation/widgets/hub_manager_selector.dart +++ b/apps/mobile/packages/features/client/orders/orders_common/lib/src/presentation/widgets/hub_manager_selector.dart @@ -35,10 +35,9 @@ class HubManagerSelector extends StatelessWidget { style: UiTypography.body1m.textPrimary, ), if (description != null) ...[ - const SizedBox(height: UiConstants.space2), Text(description!, style: UiTypography.body2r.textSecondary), ], - const SizedBox(height: UiConstants.space2), + const SizedBox(height: UiConstants.space3), InkWell( onTap: () => _showSelector(context), borderRadius: BorderRadius.circular(UiConstants.radiusBase),