feat: Implement Google Places Autocomplete for Staff Location
- Implemented strictly filtered Google Places Autocomplete (cities only) for Staff Profile Setup. - Centralized Google Places API Key configuration in Core AppConfig. - Updated Client Hubs to use the centralized AppConfig. - Verified ViewOrdersCubit logic for weekly order summaries.
This commit is contained in:
@@ -4,3 +4,4 @@ export 'src/domain/arguments/usecase_argument.dart';
|
|||||||
export 'src/domain/usecases/usecase.dart';
|
export 'src/domain/usecases/usecase.dart';
|
||||||
export 'src/utils/date_time_utils.dart';
|
export 'src/utils/date_time_utils.dart';
|
||||||
export 'src/presentation/widgets/web_mobile_frame.dart';
|
export 'src/presentation/widgets/web_mobile_frame.dart';
|
||||||
|
export 'src/config/app_config.dart';
|
||||||
|
|||||||
5
apps/mobile/packages/core/lib/src/config/app_config.dart
Normal file
5
apps/mobile/packages/core/lib/src/config/app_config.dart
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
class AppConfig {
|
||||||
|
AppConfig._();
|
||||||
|
|
||||||
|
static const String googlePlacesApiKey = String.fromEnvironment('GOOGLE_PLACES_API_KEY');
|
||||||
|
}
|
||||||
@@ -1,4 +1,6 @@
|
|||||||
|
import 'package:krow_core/krow_core.dart';
|
||||||
|
|
||||||
class HubsConstants {
|
class HubsConstants {
|
||||||
static const String googlePlacesApiKey = String.fromEnvironment('GOOGLE_PLACES_API_KEY');
|
static const String googlePlacesApiKey = AppConfig.googlePlacesApiKey;
|
||||||
static const List<String> supportedCountries = <String>['us'];
|
static const List<String> supportedCountries = <String>['us'];
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -255,20 +255,12 @@ class ViewOrdersCubit extends Cubit<ViewOrdersState> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
int _calculateCategoryCount(String category) {
|
int _calculateCategoryCount(String category) {
|
||||||
if (state.selectedDate == null) return 0;
|
|
||||||
final String selectedDateStr = DateFormat(
|
|
||||||
'yyyy-MM-dd',
|
|
||||||
).format(state.selectedDate!);
|
|
||||||
final List<OrderItem> ordersOnDate = state.orders
|
|
||||||
.where((OrderItem s) => s.date == selectedDateStr)
|
|
||||||
.toList();
|
|
||||||
|
|
||||||
if (category == 'active') {
|
if (category == 'active') {
|
||||||
return ordersOnDate
|
return state.orders
|
||||||
.where((OrderItem s) => s.status == 'IN_PROGRESS')
|
.where((OrderItem s) => s.status == 'IN_PROGRESS')
|
||||||
.length;
|
.length;
|
||||||
} else if (category == 'completed') {
|
} else if (category == 'completed') {
|
||||||
return ordersOnDate
|
return state.orders
|
||||||
.where((OrderItem s) => s.status == 'COMPLETED')
|
.where((OrderItem s) => s.status == 'COMPLETED')
|
||||||
.length;
|
.length;
|
||||||
}
|
}
|
||||||
@@ -276,14 +268,7 @@ class ViewOrdersCubit extends Cubit<ViewOrdersState> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
int _calculateUpNextCount() {
|
int _calculateUpNextCount() {
|
||||||
if (state.selectedDate == null) return 0;
|
return state.orders
|
||||||
final String selectedDateStr = DateFormat(
|
|
||||||
'yyyy-MM-dd',
|
|
||||||
).format(state.selectedDate!);
|
|
||||||
final List<OrderItem> ordersOnDate = state.orders
|
|
||||||
.where((OrderItem s) => s.date == selectedDateStr)
|
|
||||||
.toList();
|
|
||||||
return ordersOnDate
|
|
||||||
.where(
|
.where(
|
||||||
(OrderItem s) =>
|
(OrderItem s) =>
|
||||||
// TODO(orders): move PENDING to its own tab once available.
|
// TODO(orders): move PENDING to its own tab once available.
|
||||||
|
|||||||
@@ -0,0 +1,82 @@
|
|||||||
|
import 'package:bloc_test/bloc_test.dart';
|
||||||
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
|
import 'package:mocktail/mocktail.dart';
|
||||||
|
import 'package:view_orders/src/presentation/blocs/view_orders_cubit.dart';
|
||||||
|
import 'package:view_orders/src/presentation/blocs/view_orders_state.dart';
|
||||||
|
import 'package:view_orders/src/domain/usecases/get_orders_use_case.dart';
|
||||||
|
import 'package:view_orders/src/domain/usecases/get_accepted_applications_for_day_use_case.dart';
|
||||||
|
import 'package:krow_domain/krow_domain.dart';
|
||||||
|
import 'package:view_orders/src/domain/arguments/orders_range_arguments.dart';
|
||||||
|
import 'package:view_orders/src/domain/arguments/orders_day_arguments.dart';
|
||||||
|
|
||||||
|
class MockGetOrdersUseCase extends Mock implements GetOrdersUseCase {}
|
||||||
|
class MockGetAcceptedAppsUseCase extends Mock implements GetAcceptedApplicationsForDayUseCase {}
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
group('ViewOrdersCubit', () {
|
||||||
|
late GetOrdersUseCase getOrdersUseCase;
|
||||||
|
late GetAcceptedApplicationsForDayUseCase getAcceptedAppsUseCase;
|
||||||
|
|
||||||
|
setUp(() {
|
||||||
|
getOrdersUseCase = MockGetOrdersUseCase();
|
||||||
|
getAcceptedAppsUseCase = MockGetAcceptedAppsUseCase();
|
||||||
|
registerFallbackValue(OrdersRangeArguments(start: DateTime.now(), end: DateTime.now()));
|
||||||
|
registerFallbackValue(OrdersDayArguments(day: DateTime.now()));
|
||||||
|
});
|
||||||
|
|
||||||
|
test('initial state is correct', () {
|
||||||
|
final cubit = ViewOrdersCubit(
|
||||||
|
getOrdersUseCase: getOrdersUseCase,
|
||||||
|
getAcceptedAppsUseCase: getAcceptedAppsUseCase,
|
||||||
|
);
|
||||||
|
expect(cubit.state.status, ViewOrdersStatus.initial);
|
||||||
|
cubit.close();
|
||||||
|
});
|
||||||
|
|
||||||
|
blocTest<ViewOrdersCubit, ViewOrdersState>(
|
||||||
|
'calculates upNextCount based on ALL loaded orders, not just the selected day',
|
||||||
|
build: () {
|
||||||
|
final mockOrders = [
|
||||||
|
// Order 1: Today (Matches selected date)
|
||||||
|
OrderItem(
|
||||||
|
id: '1', orderId: '1', title: 'Order 1', clientName: 'Client',
|
||||||
|
status: 'OPEN', date: '2026-02-04', startTime: '09:00', endTime: '17:00',
|
||||||
|
location: 'Loc', locationAddress: 'Addr', filled: 0, workersNeeded: 1,
|
||||||
|
hourlyRate: 20, hours: 8, totalValue: 160
|
||||||
|
),
|
||||||
|
// Order 2: Tomorrow (Different date)
|
||||||
|
OrderItem(
|
||||||
|
id: '2', orderId: '2', title: 'Order 2', clientName: 'Client',
|
||||||
|
status: 'OPEN', date: '2026-02-05', startTime: '09:00', endTime: '17:00',
|
||||||
|
location: 'Loc', locationAddress: 'Addr', filled: 0, workersNeeded: 1,
|
||||||
|
hourlyRate: 20, hours: 8, totalValue: 160
|
||||||
|
),
|
||||||
|
];
|
||||||
|
|
||||||
|
when(() => getOrdersUseCase(any())).thenAnswer((_) async => mockOrders);
|
||||||
|
when(() => getAcceptedAppsUseCase(any())).thenAnswer((_) async => {});
|
||||||
|
|
||||||
|
return ViewOrdersCubit(
|
||||||
|
getOrdersUseCase: getOrdersUseCase,
|
||||||
|
getAcceptedAppsUseCase: getAcceptedAppsUseCase,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
act: (cubit) async {
|
||||||
|
// Wait for init to trigger load
|
||||||
|
await Future.delayed(const Duration(milliseconds: 100));
|
||||||
|
|
||||||
|
// Select 'Today' (2026-02-04 matches Order 1)
|
||||||
|
cubit.selectDate(DateTime(2026, 02, 04));
|
||||||
|
},
|
||||||
|
verify: (cubit) {
|
||||||
|
// Assert:
|
||||||
|
// 1. filteredOrders should only have 1 order (the one for the selected date)
|
||||||
|
expect(cubit.state.filteredOrders.length, 1, reason: 'Should only show orders for selected filtered date');
|
||||||
|
expect(cubit.state.filteredOrders.first.id, '1');
|
||||||
|
|
||||||
|
// 2. upNextCount should have 2 orders (Total for the loaded week)
|
||||||
|
expect(cubit.state.upNextCount, 2, reason: 'Up Next count should include ALL orders in the week range');
|
||||||
|
},
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -0,0 +1,49 @@
|
|||||||
|
import 'dart:convert';
|
||||||
|
import 'package:http/http.dart' as http;
|
||||||
|
import 'package:krow_core/core.dart';
|
||||||
|
import '../../domain/repositories/place_repository.dart';
|
||||||
|
|
||||||
|
class PlaceRepositoryImpl implements PlaceRepository {
|
||||||
|
final http.Client _client;
|
||||||
|
|
||||||
|
PlaceRepositoryImpl({http.Client? client}) : _client = client ?? http.Client();
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<List<String>> searchCities(String query) async {
|
||||||
|
if (query.isEmpty) return [];
|
||||||
|
|
||||||
|
final Uri uri = Uri.https(
|
||||||
|
'maps.googleapis.com',
|
||||||
|
'/maps/api/place/autocomplete/json',
|
||||||
|
<String, String>{
|
||||||
|
'input': query,
|
||||||
|
'types': '(cities)',
|
||||||
|
'key': AppConfig.googlePlacesApiKey,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
try {
|
||||||
|
final http.Response response = await _client.get(uri);
|
||||||
|
|
||||||
|
if (response.statusCode == 200) {
|
||||||
|
final Map<String, dynamic> data = json.decode(response.body) as Map<String, dynamic>;
|
||||||
|
|
||||||
|
if (data['status'] == 'OK' || data['status'] == 'ZERO_RESULTS') {
|
||||||
|
final List<dynamic> predictions = data['predictions'] as List<dynamic>;
|
||||||
|
|
||||||
|
return predictions.map((dynamic prediction) {
|
||||||
|
return prediction['description'] as String;
|
||||||
|
}).toList();
|
||||||
|
} else {
|
||||||
|
// Handle other statuses (OVER_QUERY_LIMIT, REQUEST_DENIED, etc.)
|
||||||
|
// Returning empty list for now to avoid crashing UI, ideally log this.
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
throw Exception('Network Error: ${response.statusCode}');
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
rethrow;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
abstract class PlaceRepository {
|
||||||
|
/// Searches for cities matching the [query].
|
||||||
|
/// Returns a list of city names.
|
||||||
|
Future<List<String>> searchCities(String query);
|
||||||
|
}
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
import '../repositories/place_repository.dart';
|
||||||
|
|
||||||
|
class SearchCitiesUseCase {
|
||||||
|
final PlaceRepository _repository;
|
||||||
|
|
||||||
|
SearchCitiesUseCase(this._repository);
|
||||||
|
|
||||||
|
Future<List<String>> call(String query) {
|
||||||
|
return _repository.searchCities(query);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,6 +1,8 @@
|
|||||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||||
import '../../../domain/usecases/submit_profile_setup_usecase.dart';
|
import '../../../domain/usecases/submit_profile_setup_usecase.dart';
|
||||||
|
|
||||||
|
import '../../../domain/usecases/search_cities_usecase.dart';
|
||||||
|
|
||||||
import 'profile_setup_event.dart';
|
import 'profile_setup_event.dart';
|
||||||
import 'profile_setup_state.dart';
|
import 'profile_setup_state.dart';
|
||||||
|
|
||||||
@@ -11,7 +13,9 @@ export 'profile_setup_state.dart';
|
|||||||
class ProfileSetupBloc extends Bloc<ProfileSetupEvent, ProfileSetupState> {
|
class ProfileSetupBloc extends Bloc<ProfileSetupEvent, ProfileSetupState> {
|
||||||
ProfileSetupBloc({
|
ProfileSetupBloc({
|
||||||
required SubmitProfileSetup submitProfileSetup,
|
required SubmitProfileSetup submitProfileSetup,
|
||||||
|
required SearchCitiesUseCase searchCities,
|
||||||
}) : _submitProfileSetup = submitProfileSetup,
|
}) : _submitProfileSetup = submitProfileSetup,
|
||||||
|
_searchCities = searchCities,
|
||||||
super(const ProfileSetupState()) {
|
super(const ProfileSetupState()) {
|
||||||
on<ProfileSetupFullNameChanged>(_onFullNameChanged);
|
on<ProfileSetupFullNameChanged>(_onFullNameChanged);
|
||||||
on<ProfileSetupBioChanged>(_onBioChanged);
|
on<ProfileSetupBioChanged>(_onBioChanged);
|
||||||
@@ -20,9 +24,12 @@ class ProfileSetupBloc extends Bloc<ProfileSetupEvent, ProfileSetupState> {
|
|||||||
on<ProfileSetupSkillsChanged>(_onSkillsChanged);
|
on<ProfileSetupSkillsChanged>(_onSkillsChanged);
|
||||||
on<ProfileSetupIndustriesChanged>(_onIndustriesChanged);
|
on<ProfileSetupIndustriesChanged>(_onIndustriesChanged);
|
||||||
on<ProfileSetupSubmitted>(_onSubmitted);
|
on<ProfileSetupSubmitted>(_onSubmitted);
|
||||||
|
on<ProfileSetupLocationQueryChanged>(_onLocationQueryChanged);
|
||||||
|
on<ProfileSetupClearLocationSuggestions>(_onClearLocationSuggestions);
|
||||||
}
|
}
|
||||||
|
|
||||||
final SubmitProfileSetup _submitProfileSetup;
|
final SubmitProfileSetup _submitProfileSetup;
|
||||||
|
final SearchCitiesUseCase _searchCities;
|
||||||
|
|
||||||
/// Handles the [ProfileSetupFullNameChanged] event.
|
/// Handles the [ProfileSetupFullNameChanged] event.
|
||||||
void _onFullNameChanged(
|
void _onFullNameChanged(
|
||||||
@@ -99,4 +106,29 @@ class ProfileSetupBloc extends Bloc<ProfileSetupEvent, ProfileSetupState> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<void> _onLocationQueryChanged(
|
||||||
|
ProfileSetupLocationQueryChanged event,
|
||||||
|
Emitter<ProfileSetupState> emit,
|
||||||
|
) async {
|
||||||
|
if (event.query.isEmpty) {
|
||||||
|
emit(state.copyWith(locationSuggestions: []));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
final results = await _searchCities(event.query);
|
||||||
|
emit(state.copyWith(locationSuggestions: results));
|
||||||
|
} catch (e) {
|
||||||
|
// Quietly fail or clear
|
||||||
|
emit(state.copyWith(locationSuggestions: []));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void _onClearLocationSuggestions(
|
||||||
|
ProfileSetupClearLocationSuggestions event,
|
||||||
|
Emitter<ProfileSetupState> emit,
|
||||||
|
) {
|
||||||
|
emit(state.copyWith(locationSuggestions: []));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -80,6 +80,24 @@ class ProfileSetupIndustriesChanged extends ProfileSetupEvent {
|
|||||||
List<Object?> get props => <Object?>[industries];
|
List<Object?> get props => <Object?>[industries];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Event triggered when the location query changes.
|
||||||
|
class ProfileSetupLocationQueryChanged extends ProfileSetupEvent {
|
||||||
|
/// The search query.
|
||||||
|
final String query;
|
||||||
|
|
||||||
|
/// Creates a [ProfileSetupLocationQueryChanged] event.
|
||||||
|
const ProfileSetupLocationQueryChanged(this.query);
|
||||||
|
|
||||||
|
@override
|
||||||
|
List<Object?> get props => <Object?>[query];
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Event triggered when the location suggestions should be cleared.
|
||||||
|
class ProfileSetupClearLocationSuggestions extends ProfileSetupEvent {
|
||||||
|
/// Creates a [ProfileSetupClearLocationSuggestions] event.
|
||||||
|
const ProfileSetupClearLocationSuggestions();
|
||||||
|
}
|
||||||
|
|
||||||
/// Event triggered when the profile submission is requested.
|
/// Event triggered when the profile submission is requested.
|
||||||
class ProfileSetupSubmitted extends ProfileSetupEvent {
|
class ProfileSetupSubmitted extends ProfileSetupEvent {
|
||||||
/// Creates a [ProfileSetupSubmitted] event.
|
/// Creates a [ProfileSetupSubmitted] event.
|
||||||
|
|||||||
@@ -26,8 +26,8 @@ class ProfileSetupState extends Equatable {
|
|||||||
/// The current status of the profile setup process.
|
/// The current status of the profile setup process.
|
||||||
final ProfileSetupStatus status;
|
final ProfileSetupStatus status;
|
||||||
|
|
||||||
/// Error message if the status is [ProfileSetupStatus.failure].
|
/// List of location suggestions from the API.
|
||||||
final String? errorMessage;
|
final List<String> locationSuggestions;
|
||||||
|
|
||||||
/// Creates a [ProfileSetupState] instance.
|
/// Creates a [ProfileSetupState] instance.
|
||||||
const ProfileSetupState({
|
const ProfileSetupState({
|
||||||
@@ -39,6 +39,7 @@ class ProfileSetupState extends Equatable {
|
|||||||
this.industries = const <String>[],
|
this.industries = const <String>[],
|
||||||
this.status = ProfileSetupStatus.initial,
|
this.status = ProfileSetupStatus.initial,
|
||||||
this.errorMessage,
|
this.errorMessage,
|
||||||
|
this.locationSuggestions = const <String>[],
|
||||||
});
|
});
|
||||||
|
|
||||||
/// Creates a copy of the current state with updated values.
|
/// Creates a copy of the current state with updated values.
|
||||||
@@ -51,6 +52,7 @@ class ProfileSetupState extends Equatable {
|
|||||||
List<String>? industries,
|
List<String>? industries,
|
||||||
ProfileSetupStatus? status,
|
ProfileSetupStatus? status,
|
||||||
String? errorMessage,
|
String? errorMessage,
|
||||||
|
List<String>? locationSuggestions,
|
||||||
}) {
|
}) {
|
||||||
return ProfileSetupState(
|
return ProfileSetupState(
|
||||||
fullName: fullName ?? this.fullName,
|
fullName: fullName ?? this.fullName,
|
||||||
@@ -61,18 +63,20 @@ class ProfileSetupState extends Equatable {
|
|||||||
industries: industries ?? this.industries,
|
industries: industries ?? this.industries,
|
||||||
status: status ?? this.status,
|
status: status ?? this.status,
|
||||||
errorMessage: errorMessage,
|
errorMessage: errorMessage,
|
||||||
|
locationSuggestions: locationSuggestions ?? this.locationSuggestions,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
List<Object?> get props => <Object?>[
|
List<Object?> get props => <Object?>[
|
||||||
fullName,
|
fullName,
|
||||||
bio,
|
bio,
|
||||||
preferredLocations,
|
preferredLocations,
|
||||||
maxDistanceMiles,
|
maxDistanceMiles,
|
||||||
skills,
|
skills,
|
||||||
industries,
|
industries,
|
||||||
status,
|
status,
|
||||||
errorMessage,
|
errorMessage,
|
||||||
];
|
locationSuggestions,
|
||||||
|
];
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
|
import 'dart:async';
|
||||||
import 'package:design_system/design_system.dart';
|
import 'package:design_system/design_system.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||||
import 'package:staff_authentication/src/presentation/widgets/common/section_title_subtitle.dart';
|
import 'package:staff_authentication/src/presentation/widgets/common/section_title_subtitle.dart';
|
||||||
import 'package:staff_authentication/staff_authentication.dart';
|
import 'package:staff_authentication/staff_authentication.dart';
|
||||||
|
|
||||||
@@ -32,26 +34,38 @@ class ProfileSetupLocation extends StatefulWidget {
|
|||||||
|
|
||||||
class _ProfileSetupLocationState extends State<ProfileSetupLocation> {
|
class _ProfileSetupLocationState extends State<ProfileSetupLocation> {
|
||||||
final TextEditingController _locationController = TextEditingController();
|
final TextEditingController _locationController = TextEditingController();
|
||||||
|
Timer? _debounce;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void dispose() {
|
void dispose() {
|
||||||
_locationController.dispose();
|
_locationController.dispose();
|
||||||
|
_debounce?.cancel();
|
||||||
super.dispose();
|
super.dispose();
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Adds the current text from the controller as a location.
|
void _onSearchChanged(String query) {
|
||||||
void _addLocation() {
|
if (_debounce?.isActive ?? false) _debounce!.cancel();
|
||||||
final String loc = _locationController.text.trim();
|
_debounce = Timer(const Duration(milliseconds: 300), () {
|
||||||
if (loc.isNotEmpty && !widget.preferredLocations.contains(loc)) {
|
context
|
||||||
final List<String> updatedList = List<String>.from(widget.preferredLocations)
|
.read<ProfileSetupBloc>()
|
||||||
..add(loc);
|
.add(ProfileSetupLocationQueryChanged(query));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Adds the selected location.
|
||||||
|
void _addLocation(String location) {
|
||||||
|
if (location.isNotEmpty && !widget.preferredLocations.contains(location)) {
|
||||||
|
final List<String> updatedList =
|
||||||
|
List<String>.from(widget.preferredLocations)..add(location);
|
||||||
widget.onLocationsChanged(updatedList);
|
widget.onLocationsChanged(updatedList);
|
||||||
_locationController.clear();
|
_locationController.clear();
|
||||||
|
context
|
||||||
|
.read<ProfileSetupBloc>()
|
||||||
|
.add(const ProfileSetupClearLocationSuggestions());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
/// Builds the location setup step UI.
|
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return Column(
|
return Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
@@ -62,37 +76,55 @@ class _ProfileSetupLocationState extends State<ProfileSetupLocation> {
|
|||||||
),
|
),
|
||||||
const SizedBox(height: UiConstants.space8),
|
const SizedBox(height: UiConstants.space8),
|
||||||
|
|
||||||
// Add Location input
|
// Search Input
|
||||||
Row(
|
UiTextField(
|
||||||
crossAxisAlignment: CrossAxisAlignment.end,
|
label: t.staff_authentication.profile_setup_page.location
|
||||||
spacing: UiConstants.space2,
|
.add_location_label,
|
||||||
children: <Widget>[
|
controller: _locationController,
|
||||||
Expanded(
|
hintText: t.staff_authentication.profile_setup_page.location
|
||||||
child: UiTextField(
|
.add_location_hint,
|
||||||
label: t
|
onChanged: _onSearchChanged,
|
||||||
.staff_authentication
|
),
|
||||||
.profile_setup_page
|
|
||||||
.location
|
// Suggestions List
|
||||||
.add_location_label,
|
BlocBuilder<ProfileSetupBloc, ProfileSetupState>(
|
||||||
controller: _locationController,
|
buildWhen: (previous, current) =>
|
||||||
hintText: t
|
previous.locationSuggestions != current.locationSuggestions,
|
||||||
.staff_authentication
|
builder: (context, state) {
|
||||||
.profile_setup_page
|
if (state.locationSuggestions.isEmpty) {
|
||||||
.location
|
return const SizedBox.shrink();
|
||||||
.add_location_hint,
|
}
|
||||||
onSubmitted: (_) => _addLocation(),
|
return Container(
|
||||||
|
constraints: const BoxConstraints(maxHeight: 200),
|
||||||
|
margin: const EdgeInsets.only(top: UiConstants.space2),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: Theme.of(context).cardColor,
|
||||||
|
borderRadius: BorderRadius.circular(UiConstants.radiusMd),
|
||||||
|
boxShadow: [
|
||||||
|
BoxShadow(
|
||||||
|
color: Colors.black.withOpacity(0.1),
|
||||||
|
blurRadius: 4,
|
||||||
|
offset: const Offset(0, 2),
|
||||||
|
),
|
||||||
|
],
|
||||||
),
|
),
|
||||||
),
|
child: ListView.separated(
|
||||||
UiButton.secondary(
|
shrinkWrap: true,
|
||||||
text:
|
padding: EdgeInsets.zero,
|
||||||
t.staff_authentication.profile_setup_page.location.add_button,
|
itemCount: state.locationSuggestions.length,
|
||||||
onPressed: _addLocation,
|
separatorBuilder: (context, index) => const Divider(height: 1),
|
||||||
style: OutlinedButton.styleFrom(
|
itemBuilder: (context, index) {
|
||||||
minimumSize: const Size(0, 48),
|
final suggestion = state.locationSuggestions[index];
|
||||||
maximumSize: const Size(double.infinity, 48),
|
return ListTile(
|
||||||
|
title: Text(suggestion, style: UiTypography.body2m),
|
||||||
|
leading: const Icon(UiIcons.mapPin, size: 16),
|
||||||
|
onTap: () => _addLocation(suggestion),
|
||||||
|
visualDensity: VisualDensity.compact,
|
||||||
|
);
|
||||||
|
},
|
||||||
),
|
),
|
||||||
),
|
);
|
||||||
],
|
},
|
||||||
),
|
),
|
||||||
|
|
||||||
const SizedBox(height: UiConstants.space4),
|
const SizedBox(height: UiConstants.space4),
|
||||||
@@ -134,18 +166,12 @@ class _ProfileSetupLocationState extends State<ProfileSetupLocation> {
|
|||||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
children: <Widget>[
|
children: <Widget>[
|
||||||
Text(
|
Text(
|
||||||
t
|
t.staff_authentication.profile_setup_page.location
|
||||||
.staff_authentication
|
|
||||||
.profile_setup_page
|
|
||||||
.location
|
|
||||||
.min_dist_label,
|
.min_dist_label,
|
||||||
style: UiTypography.footnote1r.textSecondary,
|
style: UiTypography.footnote1r.textSecondary,
|
||||||
),
|
),
|
||||||
Text(
|
Text(
|
||||||
t
|
t.staff_authentication.profile_setup_page.location
|
||||||
.staff_authentication
|
|
||||||
.profile_setup_page
|
|
||||||
.location
|
|
||||||
.max_dist_label,
|
.max_dist_label,
|
||||||
style: UiTypography.footnote1r.textSecondary,
|
style: UiTypography.footnote1r.textSecondary,
|
||||||
),
|
),
|
||||||
@@ -158,8 +184,8 @@ class _ProfileSetupLocationState extends State<ProfileSetupLocation> {
|
|||||||
|
|
||||||
/// Removes the specified [location] from the list.
|
/// Removes the specified [location] from the list.
|
||||||
void _removeLocation({required String location}) {
|
void _removeLocation({required String location}) {
|
||||||
final List<String> updatedList = List<String>.from(widget.preferredLocations)
|
final List<String> updatedList =
|
||||||
..remove(location);
|
List<String>.from(widget.preferredLocations)..remove(location);
|
||||||
widget.onLocationsChanged(updatedList);
|
widget.onLocationsChanged(updatedList);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,6 +11,9 @@ import 'package:staff_authentication/src/domain/usecases/verify_otp_usecase.dart
|
|||||||
import 'package:staff_authentication/src/domain/repositories/profile_setup_repository.dart';
|
import 'package:staff_authentication/src/domain/repositories/profile_setup_repository.dart';
|
||||||
import 'package:staff_authentication/src/data/repositories_impl/profile_setup_repository_impl.dart';
|
import 'package:staff_authentication/src/data/repositories_impl/profile_setup_repository_impl.dart';
|
||||||
import 'package:staff_authentication/src/domain/usecases/submit_profile_setup_usecase.dart';
|
import 'package:staff_authentication/src/domain/usecases/submit_profile_setup_usecase.dart';
|
||||||
|
import 'package:staff_authentication/src/domain/repositories/place_repository.dart';
|
||||||
|
import 'package:staff_authentication/src/data/repositories_impl/place_repository_impl.dart';
|
||||||
|
import 'package:staff_authentication/src/domain/usecases/search_cities_usecase.dart';
|
||||||
import 'package:staff_authentication/src/presentation/blocs/auth_bloc.dart';
|
import 'package:staff_authentication/src/presentation/blocs/auth_bloc.dart';
|
||||||
import 'package:staff_authentication/src/presentation/blocs/profile_setup/profile_setup_bloc.dart';
|
import 'package:staff_authentication/src/presentation/blocs/profile_setup/profile_setup_bloc.dart';
|
||||||
import 'package:staff_authentication/src/presentation/pages/get_started_page.dart';
|
import 'package:staff_authentication/src/presentation/pages/get_started_page.dart';
|
||||||
@@ -44,11 +47,13 @@ class StaffAuthenticationModule extends Module {
|
|||||||
dataConnect: ExampleConnector.instance,
|
dataConnect: ExampleConnector.instance,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
i.addLazySingleton<PlaceRepository>(PlaceRepositoryImpl.new);
|
||||||
|
|
||||||
// UseCases
|
// UseCases
|
||||||
i.addLazySingleton(SignInWithPhoneUseCase.new);
|
i.addLazySingleton(SignInWithPhoneUseCase.new);
|
||||||
i.addLazySingleton(VerifyOtpUseCase.new);
|
i.addLazySingleton(VerifyOtpUseCase.new);
|
||||||
i.addLazySingleton(SubmitProfileSetup.new);
|
i.addLazySingleton(SubmitProfileSetup.new);
|
||||||
|
i.addLazySingleton(SearchCitiesUseCase.new);
|
||||||
|
|
||||||
// BLoCs
|
// BLoCs
|
||||||
i.addLazySingleton<AuthBloc>(
|
i.addLazySingleton<AuthBloc>(
|
||||||
@@ -60,6 +65,7 @@ class StaffAuthenticationModule extends Module {
|
|||||||
i.add<ProfileSetupBloc>(
|
i.add<ProfileSetupBloc>(
|
||||||
() => ProfileSetupBloc(
|
() => ProfileSetupBloc(
|
||||||
submitProfileSetup: i.get<SubmitProfileSetup>(),
|
submitProfileSetup: i.get<SubmitProfileSetup>(),
|
||||||
|
searchCities: i.get<SearchCitiesUseCase>(),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ dependencies:
|
|||||||
firebase_core: ^4.2.1
|
firebase_core: ^4.2.1
|
||||||
firebase_auth: ^6.1.2 # Updated for compatibility
|
firebase_auth: ^6.1.2 # Updated for compatibility
|
||||||
firebase_data_connect: ^0.2.2+1
|
firebase_data_connect: ^0.2.2+1
|
||||||
|
http: ^1.2.0
|
||||||
|
|
||||||
# Architecture Packages
|
# Architecture Packages
|
||||||
krow_domain:
|
krow_domain:
|
||||||
|
|||||||
Reference in New Issue
Block a user